sqlite.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. # ============================================================================
  2. # This file is part of Pwman3.
  3. #
  4. # Pwman3 is free software; you can redistribute iut and/or modify
  5. # it under the terms of the GNU General Public License, version 2
  6. # as published by the Free Software Foundation;
  7. #
  8. # Pwman3 is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with Pwman3; if not, write to the Free Software
  15. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  16. # ============================================================================
  17. # Copyright (C) 2012, 2013, 2014 Oz Nahum Tiram <nahumoz@gmail.com>
  18. # ============================================================================
  19. # Copyright (C) 2006 Ivan Kelly <ivan@ivankelly.net>
  20. # ============================================================================
  21. """SQLite Database implementation."""
  22. from pwman.data.database import Database, DatabaseException
  23. from pwman.data.database import __DB_FORMAT__
  24. from pwman.data.nodes import NewNode
  25. from pwman.data.tags import TagNew
  26. from pwman.util.crypto_engine import CryptoEngine
  27. import sqlite3 as sqlite
  28. import itertools
  29. class SQLiteDatabaseNewForm(Database):
  30. """SQLite Database implementation"""
  31. @classmethod
  32. def check_db_version(cls, fname):
  33. """
  34. check the data base version.
  35. """
  36. con = sqlite.connect(fname)
  37. cur = con.cursor()
  38. cur.execute("PRAGMA TABLE_INFO(DBVERSION)")
  39. row = cur.fetchone()
  40. if not row:
  41. return "0.3" # pragma: no cover
  42. try:
  43. return row[-2]
  44. except IndexError: # pragma: no cover
  45. raise DatabaseException("Something seems fishy with the DB")
  46. def __init__(self, filename, dbformat=__DB_FORMAT__):
  47. """Initialise SQLitePwmanDatabase instance."""
  48. super(SQLiteDatabaseNewForm, self).__init__()
  49. self._filename = filename
  50. self.dbformat = dbformat
  51. def _open(self):
  52. try:
  53. self._con = sqlite.connect(self._filename)
  54. self._cur = self._con.cursor()
  55. self._checktables()
  56. except sqlite.DatabaseError as e: # pragma: no cover
  57. raise DatabaseException("SQLite: %s" % (e))
  58. def close(self):
  59. self._cur.close()
  60. self._con.close()
  61. def listtags(self, alltags=False):
  62. sql = ''
  63. params = []
  64. if not self._filtertags or alltags:
  65. sql = "SELECT DATA FROM TAGS ORDER BY DATA ASC"
  66. else:
  67. sql = ("SELECT TAGS.DATA FROM LOOKUP"
  68. " INNER JOIN TAGS ON LOOKUP.TAG = TAGS.ID"
  69. " WHERE NODE IN (")
  70. first = True
  71. for t in self._filtertags:
  72. if not first:
  73. sql += " INTERSECT " # pragma: no cover
  74. else:
  75. first = False
  76. sql += ("SELECT NODE FROM LOOKUP LEFT JOIN TAGS ON TAG = "
  77. " TAGS.ID WHERE TAGS.DATA LIKE ?")
  78. params.append(t._name.decode()+u'%')
  79. sql += ") EXCEPT SELECT DATA FROM TAGS WHERE "
  80. first = True
  81. for t in self._filtertags:
  82. if not first:
  83. sql += " OR " # pragma: no cover
  84. else:
  85. first = False
  86. sql += "TAGS.DATA = ?"
  87. params.append(t.name)
  88. try:
  89. self._cur.execute(sql, params)
  90. tags = [str(t[0]) for t in self._cur.fetchall()]
  91. return tags
  92. except sqlite.DatabaseError as e: # pragma: no cover
  93. raise DatabaseException("SQLite: %s" % (e))
  94. except sqlite.InterfaceError as e: # pragma: no cover
  95. raise e
  96. def parse_node_string(self, string):
  97. nodestring = string.split("##")
  98. keyvals = {}
  99. for pair in nodestring[:-1]:
  100. key, val = pair.split(":")
  101. keyvals[key.lstrip('##')] = val
  102. tags = nodestring[-1]
  103. tags = tags.split("tags:", 1)[1]
  104. tags = tags.split("tag:")
  105. tags = [tag.split('**endtag**')[0] for tag in tags]
  106. return keyvals, tags
  107. def getnodes(self, ids):
  108. """
  109. object should always be: (ipwman.data.nodes
  110. """
  111. nodes = []
  112. for i in ids:
  113. sql = "SELECT DATA FROM NODES WHERE ID = ?"
  114. self._cur.execute(sql, [i])
  115. row = self._cur.fetchone()
  116. if row is not None:
  117. nodestring = str(row[0])
  118. args, tags = self.parse_node_string(nodestring)
  119. node = NewNode()
  120. node._password = args['password']
  121. node._username = args['username']
  122. node._url = args['url']
  123. node._notes = args['notes']
  124. node.tags = tags
  125. node._id = i
  126. nodes.append(node)
  127. return nodes
  128. def editnode(self, id, node):
  129. try:
  130. sql = "UPDATE NODES SET DATA = ? WHERE ID = ?"
  131. self._cur.execute(sql, [node.dump_edit_to_db()[0], id])
  132. except sqlite.DatabaseError as e: # pragma: no cover
  133. raise DatabaseException("SQLite: %s" % (e))
  134. self._setnodetags(node)
  135. self._checktags()
  136. self._commit()
  137. def addnodes(self, nodes):
  138. """
  139. This method writes the data as an ecrypted string to
  140. the database
  141. """
  142. for n in nodes:
  143. sql = "INSERT INTO NODES(DATA) VALUES(?)"
  144. value = n.dump_edit_to_db()
  145. try:
  146. self._cur.execute(sql, value)
  147. except sqlite.DatabaseError as e: # pragma: no cover
  148. raise DatabaseException("SQLite: %s" % (e))
  149. idx = self._cur.lastrowid
  150. n._id = idx
  151. self._setnodetags(n)
  152. self._commit()
  153. def removenodes(self, nodes):
  154. for n in nodes:
  155. # if not isinstance(n, Node): raise DatabaseException(
  156. # "Tried to delete foreign object from database [%s]", n)
  157. try:
  158. sql = "DELETE FROM NODES WHERE ID = ?"
  159. self._cur.execute(sql, [n._id])
  160. except sqlite.DatabaseError as e: # pragma: no cover
  161. raise DatabaseException("SQLite: %s" % (e))
  162. self._deletenodetags(n)
  163. self._checktags()
  164. self._commit()
  165. def listnodes(self):
  166. sql = ''
  167. params = []
  168. if not self._filtertags:
  169. sql = "SELECT ID FROM NODES ORDER BY ID ASC"
  170. else:
  171. first = True
  172. for t in self._filtertags:
  173. if not first:
  174. sql += " INTERSECT " # pragma: no cover
  175. else:
  176. first = False
  177. sql += ("SELECT NODE FROM LOOKUP LEFT JOIN TAGS ON TAG = "
  178. " TAGS.ID WHERE TAGS.DATA LIKE ? ")
  179. # this is correct if tags are ciphertext
  180. p = t._name.strip()
  181. # this is wrong, it will work when tags are stored as plain
  182. # text
  183. # p = t.name.strip()
  184. p = '%'+p+'%'
  185. params = [p]
  186. try:
  187. self._cur.execute(sql, params)
  188. rows = self._cur.fetchall()
  189. ids = [row[0] for row in rows]
  190. return ids
  191. except sqlite.DatabaseError as e: # pragma: no cover
  192. raise DatabaseException("SQLite: %s" % (e))
  193. def _commit(self):
  194. try:
  195. self._con.commit()
  196. except sqlite.DatabaseError as e: # pragma: no cover
  197. self._con.rollback()
  198. raise DatabaseException(
  199. "SQLite: Error commiting data to db [%s]" % (e))
  200. def _create_tag(self, tag):
  201. """add tags to db"""
  202. # sql = "INSERT OR REPLACE INTO TAGS(DATA) VALUES(?)"
  203. sql = "INSERT OR IGNORE INTO TAGS(DATA) VALUES(?)"
  204. if isinstance(tag, str):
  205. self._cur.execute(sql, [tag])
  206. elif isinstance(tag, TagNew):
  207. self._cur.execute(sql, [tag._name])
  208. else:
  209. self._cur.execute(sql, [tag.decode()])
  210. def _deletenodetags(self, node):
  211. try:
  212. sql = "DELETE FROM LOOKUP WHERE NODE = ?"
  213. self._cur.execute(sql, [node._id])
  214. except sqlite.DatabaseError as e: # pragma: no cover
  215. raise DatabaseException("SQLite: %s" % (e))
  216. self._commit()
  217. def _update_tag_lookup(self, node, tag_id):
  218. sql = "INSERT OR REPLACE INTO LOOKUP VALUES(?, ?)"
  219. params = [node._id, tag_id]
  220. try:
  221. self._cur.execute(sql, params)
  222. except sqlite.DatabaseError as e: # pragma: no cover
  223. raise DatabaseException("SQLite: %s" % (e))
  224. def _tagids(self, tags):
  225. ids = []
  226. sql = "SELECT ID FROM TAGS WHERE DATA LIKE ?"
  227. for tag in tags:
  228. try:
  229. if isinstance(tag, str):
  230. enc = CryptoEngine.get()
  231. tag = enc.encrypt(tag)
  232. self._cur.execute(sql, [tag])
  233. elif isinstance(tag, TagNew):
  234. self._cur.execute(sql, [tag._name.decode()+u'%'])
  235. else:
  236. self._cur.execute(sql, [tag.decode()+u'%'])
  237. values = self._cur.fetchall()
  238. if values: # tags already exist in the database
  239. ids.extend(list(itertools.chain(*values)))
  240. else:
  241. self._create_tag(tag)
  242. ids.append(self._cur.lastrowid)
  243. except sqlite.DatabaseError as e: # pragma: no cover
  244. raise DatabaseException("SQLite: %s" % (e))
  245. return ids
  246. def _setnodetags(self, node):
  247. ids = self._tagids(node.tags)
  248. for tagid in ids:
  249. self._update_tag_lookup(node, tagid)
  250. self._commit()
  251. def _checktags(self):
  252. try:
  253. sql = "DELETE FROM TAGS WHERE ID NOT IN (SELECT TAG FROM" \
  254. + " LOOKUP GROUP BY TAG)"
  255. self._cur.execute(sql)
  256. except sqlite.DatabaseError as e: # pragma: no cover
  257. raise DatabaseException("SQLite: %s" % (e))
  258. self._commit()
  259. def _checktables(self):
  260. """
  261. Check if the Pwman tables exist.
  262. TODO: This method should check the version of the
  263. database. If it finds an old format it should
  264. exis, and prompt the user to convert the database
  265. to the new version with a designated script.
  266. """
  267. self._cur.execute("PRAGMA TABLE_INFO(NODES)")
  268. if self._cur.fetchone() is None:
  269. # table doesn't exist, create it
  270. # SQLite does have constraints implemented at the moment
  271. # so datatype will just be a string
  272. self._cur.execute("CREATE TABLE NODES (ID INTEGER PRIMARY KEY"
  273. " AUTOINCREMENT,DATA BLOB NOT NULL)")
  274. self._cur.execute("CREATE TABLE TAGS"
  275. "(ID INTEGER PRIMARY KEY AUTOINCREMENT,"
  276. "DATA BLOB NOT NULL UNIQUE)")
  277. self._cur.execute("CREATE TABLE LOOKUP"
  278. "(NODE INTEGER NOT NULL, TAG INTEGER NOT NULL,"
  279. " PRIMARY KEY(NODE, TAG))")
  280. self._cur.execute("CREATE TABLE KEY"
  281. "(THEKEY TEXT NOT NULL DEFAULT '')")
  282. self._cur.execute("INSERT INTO KEY VALUES('')")
  283. # create a table to hold DB version info
  284. self._cur.execute("CREATE TABLE DBVERSION"
  285. "(DBVERSION TEXT NOT NULL DEFAULT '%s')" %
  286. self.dbformat)
  287. self._cur.execute("INSERT INTO DBVERSION VALUES('%s')" %
  288. self.dbformat)
  289. try:
  290. self._con.commit()
  291. except DatabaseException as e: # pragma: no cover
  292. self._con.rollback()
  293. raise e
  294. def savekey(self, key):
  295. """
  296. This function is saving the key to table KEY.
  297. The key already arrives as an encrypted string.
  298. It is the same self._keycrypted from
  299. crypto py (check with id(self._keycrypted) and
  300. id(key) here.
  301. """
  302. sql = "UPDATE KEY SET THEKEY = ?"
  303. values = [key]
  304. self._cur.execute(sql, values)
  305. try:
  306. self._con.commit()
  307. except sqlite.DatabaseError as e: # pragma: no cover
  308. self._con.rollback()
  309. raise DatabaseException(
  310. "SQLite: Error saving key [%s]" % (e))
  311. def loadkey(self):
  312. """
  313. fetch the key to database. the key is also stored
  314. encrypted.
  315. """
  316. self._cur.execute("SELECT THEKEY FROM KEY")
  317. keyrow = self._cur.fetchone()
  318. if (keyrow[0] == ''):
  319. return None
  320. else:
  321. return keyrow[0]
  322. class SQLite(SQLiteDatabaseNewForm):
  323. def __init__(self, filename, dbformat=0.6):
  324. """Initialise SQLitePwmanDatabase instance."""
  325. self._filename = filename
  326. self.dbformat = dbformat
  327. def _open(self):
  328. self._con = sqlite.connect(self._filename)
  329. self._cur = self._con.cursor()
  330. def listnodes(self, filter=None):
  331. if not filter:
  332. sql_all = "SELECT ID FROM NODE"
  333. self._cur.execute(sql_all)
  334. ids = self._cur.fetchall()
  335. return ids
  336. else:
  337. tagid = self._get_tag(filter)
  338. sql_filter = "SELECT NODEID FROM LOOKUP WHERE TAGID = ? "
  339. self._cur.execute(sql_filter, (tagid))
  340. ids = self._cur.fetchall()
  341. return ids
  342. def listtags(self):
  343. self._clean_orphands()
  344. get_tags = "select data from tag"
  345. self._cur.execute(get_tags)
  346. tags = self._cur.fetchall()
  347. if tags:
  348. return tags
  349. return []
  350. def _create_tables(self):
  351. self._cur.execute("PRAGMA TABLE_INFO(NODE)")
  352. if self._cur.fetchone() is not None:
  353. return
  354. self._cur.execute("CREATE TABLE NODE (ID INTEGER PRIMARY KEY "
  355. "AUTOINCREMENT, "
  356. "USER TEXT NOT NULL, "
  357. "PASSWORD TEXT NOT NULL, "
  358. "URL TEXT NOT NULL,"
  359. "NOTES TEXT NOT NULL)")
  360. self._cur.execute("CREATE TABLE TAG"
  361. "(ID INTEGER PRIMARY KEY AUTOINCREMENT,"
  362. "DATA BLOB NOT NULL UNIQUE)")
  363. self._cur.execute("CREATE TABLE LOOKUP ("
  364. "nodeid INTEGER NOT NULL, "
  365. "tagid INTEGER NOT NULL, "
  366. "FOREIGN KEY(nodeid) REFERENCES NODE(ID),"
  367. "FOREIGN KEY(tagid) REFERENCES TAG(ID))")
  368. self._cur.execute("CREATE TABLE CRYPTO"
  369. "(SEED TEXT,"
  370. " DIGEST TEXT)")
  371. # create a table to hold DB version info
  372. self._cur.execute("CREATE TABLE DBVERSION"
  373. "(DB VERSION TEXT NOT NULL DEFAULT '%s')" %
  374. self.dbformat)
  375. self._cur.execute("INSERT INTO DBVERSION VALUES('%s')" %
  376. self.dbformat)
  377. try:
  378. self._con.commit()
  379. except DatabaseException as e: # pragma: no cover
  380. self._con.rollback()
  381. raise e
  382. def fetch_crypto_info(self):
  383. self._cur.execute("SELECT * FROM CRYPTO")
  384. keyrow = self._cur.fetchone()
  385. return keyrow
  386. def save_crypto_info(self, seed, digest):
  387. """save the random seed and the digested key"""
  388. self._cur.execute("DELETE FROM CRYPTO")
  389. self._cur.execute("INSERT INTO CRYPTO VALUES(?, ?)", [seed, digest])
  390. self._con.commit()
  391. def add_node(self, node):
  392. sql = ("INSERT INTO NODE(USER, PASSWORD, URL, NOTES)"
  393. "VALUES(?, ?, ?, ?)")
  394. node_tags = list(node)
  395. node, tags = node_tags[:4], node_tags[-1]
  396. self._cur.execute(sql, (node))
  397. self._setnodetags(self._cur.lastrowid, tags)
  398. self._con.commit()
  399. def _get_tag(self, tagcipher):
  400. sql_search = "SELECT ID FROM TAG WHERE DATA = ?"
  401. self._cur.execute(sql_search, ([tagcipher]))
  402. rv = self._cur.fetchone()
  403. return rv
  404. def _get_or_create_tag(self, tagcipher):
  405. rv = self._get_tag(tagcipher)
  406. if rv:
  407. return rv[0]
  408. else:
  409. sql_insert = "INSERT INTO TAG(DATA) VALUES(?)"
  410. self._cur.execute(sql_insert, ([tagcipher]))
  411. return self._cur.lastrowid
  412. def _update_tag_lookup(self, nodeid, tid):
  413. sql_lookup = "INSERT INTO LOOKUP(nodeid, tagid) VALUES(?,?)"
  414. self._cur.execute(sql_lookup, (nodeid, tid))
  415. self._con.commit()
  416. def _setnodetags(self, nodeid, tags):
  417. for tag in tags:
  418. tid = self._get_or_create_tag(tag)
  419. self._update_tag_lookup(nodeid, tid)
  420. def getnodes(self, ids):
  421. sql = "SELECT * FROM NODE WHERE ID IN (%s)" % ','.join('?'*len(ids))
  422. self._cur.execute(sql, (ids))
  423. nodes = self._cur.fetchall()
  424. return nodes
  425. def editnode(self, nid, **kwargs):
  426. tags = kwargs.pop('tags', None)
  427. sql = ("UPDATE NODE SET %s WHERE ID = ? "
  428. "" % ','.join('%s=?' % k for k in list(kwargs)))
  429. self._cur.execute(sql, (list(kwargs.values()) + [nid]))
  430. if tags:
  431. # update all old node entries in lookup
  432. # create new entries
  433. # clean all old tags
  434. sql_clean = "DELETE FROM LOOKUP WHERE NODEID=?"
  435. self._cur.execute(sql_clean, str(nid))
  436. self._setnodetags(nid, tags)
  437. self._con.commit()
  438. def removenodes(self, nids):
  439. sql_rm = "delete from node where id in (%s)" % ','.join('?'*len(nids))
  440. self._cur.execute(sql_rm, (nids))
  441. def _clean_orphands(self):
  442. clean = ("delete from tag where not exists "
  443. "(select 'x' from lookup l where l.tagid = tag.id)")
  444. self._cur.execute(clean)
  445. self._con.commit()
  446. def savekey(self, key):
  447. salt, digest = key.split('$6$')
  448. sql = "INSERT INTO CRYPTO(SEED, DIGEST) VALUES(?,?)"
  449. self._cur.execute("DELETE FROM CRYPTO")
  450. self._cur.execute(sql, (salt, digest))
  451. self._digest = digest.encode('utf-8')
  452. self._salt = salt.encode('utf-8')
  453. def loadkey(self):
  454. # TODO: rename this method!
  455. """
  456. return _keycrypted
  457. """
  458. sql = "SELECT * FROM CRYPTO"
  459. try:
  460. seed, digest = self._cur.execute(sql).fetchone()
  461. return seed + u'$6$' + digest
  462. except TypeError:
  463. return None
  464. def close(self):
  465. self._clean_orphands()
  466. self._cur.close()
  467. self._con.close()