database.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. # ============================================================================
  2. # This file is part of Pwman3.
  3. #
  4. # Pwman3 is free software; you can redistribute it 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 Oz Nahum <nahumoz@gmail.com>
  18. # ============================================================================
  19. # Copyright (C) 2006 Ivan Kelly <ivan@ivankelly.net>
  20. # ============================================================================
  21. from pwman.util.crypto_engine import CryptoEngine
  22. __DB_FORMAT__ = 0.6
  23. class DatabaseException(Exception):
  24. pass # pragma: no cover
  25. class Database(object):
  26. def open(self, dbver=None):
  27. """
  28. Open the database, by calling the _open method of the
  29. class inherited for the specific database.
  30. When done validation that the file is OK, check if it has
  31. encryption key, by calling
  32. enc = CryptoEngine.get()
  33. key = self.loadkey()
  34. """
  35. self._open()
  36. enc = CryptoEngine.get()
  37. key = self.loadkey()
  38. if key is not None:
  39. enc.set_cryptedkey(key)
  40. else:
  41. self.get_user_password()
  42. def _check_tables(self):
  43. try:
  44. self._cur.execute("SELECT 1 from DBVERSION")
  45. version = self._cur.fetchone()
  46. return version
  47. except Exception:
  48. self._con.rollback()
  49. return 0
  50. def _create_tables(self):
  51. if self._check_tables():
  52. return
  53. try:
  54. self._cur.execute("CREATE TABLE NODE(ID SERIAL PRIMARY KEY, "
  55. "USERNAME TEXT NOT NULL, "
  56. "PASSWORD TEXT NOT NULL, "
  57. "URL TEXT NOT NULL, "
  58. "NOTES TEXT NOT NULL"
  59. ")")
  60. self._cur.execute("CREATE TABLE TAG"
  61. "(ID SERIAL PRIMARY KEY,"
  62. "DATA TEXT NOT NULL)")
  63. self._cur.execute("CREATE TABLE LOOKUP ("
  64. "nodeid INTEGER NOT NULL REFERENCES NODE(ID),"
  65. "tagid INTEGER NOT NULL REFERENCES TAG(ID)"
  66. ")")
  67. self._cur.execute("CREATE TABLE CRYPTO "
  68. "(SEED TEXT, DIGEST TEXT)")
  69. self._cur.execute("CREATE TABLE DBVERSION("
  70. "VERSION TEXT NOT NULL)")
  71. self._cur.execute("INSERT INTO DBVERSION VALUES(%s)",
  72. (self.dbversion,))
  73. self._con.commit()
  74. except Exception: # pragma: no cover
  75. self._con.rollback()
  76. def get_user_password(self):
  77. """
  78. get the databases password from the user
  79. """
  80. enc = CryptoEngine.get()
  81. newkey = enc.changepassword()
  82. return self.savekey(newkey)
  83. def _clean_orphans(self):
  84. clean = ("delete from TAG where not exists "
  85. "(select 'x' from LOOKUP l where l.TAGID = TAG.ID)")
  86. self._cur.execute(clean)
  87. self._con.commit()
  88. def _get_node_tags(self, node):
  89. sql = "SELECT tagid FROM LOOKUP WHERE NODEID = {}".format(self._sub)
  90. self._cur.execute(sql, (str(node[0]),))
  91. tagids = self._cur.fetchall()
  92. if tagids:
  93. sql = ("SELECT DATA FROM TAG WHERE ID IN"
  94. " ({})".format(','.join([self._sub]*len(tagids))))
  95. tagids = [str(id[0]) for id in tagids]
  96. self._cur.execute(sql, (tagids))
  97. tags = self._cur.fetchall()
  98. for t in tags:
  99. yield t[0]
  100. def _setnodetags(self, nodeid, tags):
  101. for tag in tags:
  102. tid = self._get_or_create_tag(tag)
  103. self._update_tag_lookup(nodeid, tid)
  104. def _get_tag(self, tagcipher):
  105. sql_search = "SELECT * FROM TAG"
  106. self._cur.execute(sql_search)
  107. ce = CryptoEngine.get()
  108. try:
  109. tag = ce.decrypt(tagcipher)
  110. encrypted = True
  111. except Exception:
  112. tag = tagcipher
  113. encrypted = False
  114. rv = self._cur.fetchall()
  115. for idx, cipher in rv:
  116. if encrypted and tag == ce.decrypt(cipher):
  117. return idx
  118. elif tag == cipher:
  119. return idx
  120. def _get_or_create_tag(self, tagcipher):
  121. rv = self._get_tag(tagcipher)
  122. if rv:
  123. return rv
  124. else:
  125. self._cur.execute(self._insert_tag_sql, list(map(self._data_wrapper, (tagcipher,)))) # noqa
  126. try:
  127. return self._cur.fetchone()[0]
  128. except TypeError:
  129. return self._cur.lastrowid
  130. def _update_tag_lookup(self, nodeid, tid):
  131. sql_lookup = "INSERT INTO LOOKUP(nodeid, tagid) VALUES({}, {})".format(
  132. self._sub, self._sub)
  133. self._cur.execute(sql_lookup, (nodeid, tid))
  134. self._con.commit()
  135. def getnodes(self, ids):
  136. if ids:
  137. sql = ("SELECT * FROM NODE WHERE ID IN ({})"
  138. "".format(','.join(self._sub for i in ids)))
  139. else:
  140. sql = "SELECT * FROM NODE"
  141. self._cur.execute(sql, (ids))
  142. nodes = self._cur.fetchall()
  143. if not nodes:
  144. return []
  145. # sqlite returns nodes as bytes, postgresql returns them as str
  146. if isinstance(nodes[0][1], str):
  147. nodes = [node for node in nodes]
  148. nodes_w_tags = []
  149. for node in nodes:
  150. tags = [t for t in self._get_node_tags(node)]
  151. nodes_w_tags.append(list(node) + tags)
  152. return nodes_w_tags
  153. def listnodes(self, filter=None):
  154. """return a list of node ids"""
  155. if not filter:
  156. sql_all = "SELECT ID FROM NODE"
  157. self._cur.execute(sql_all)
  158. ids = self._cur.fetchall()
  159. return [id[0] for id in ids]
  160. else:
  161. tagid = self._get_tag(filter)
  162. if not tagid:
  163. return [] # pragma: no cover
  164. # will this work for many nodes??? with the same tag?
  165. self._cur.execute(self._list_nodes_sql, (tagid,))
  166. self._con.commit()
  167. ids = self._cur.fetchall()
  168. return [id[0] for id in ids]
  169. def add_node(self, node):
  170. node_tags = list(node)
  171. node, tags = node_tags[:4], node_tags[-1]
  172. self._cur.execute(self._add_node_sql, list(map(self._data_wrapper, (node)))) # noqa
  173. try:
  174. nid = self._cur.fetchone()[0]
  175. except TypeError:
  176. nid = self._cur.lastrowid
  177. self._setnodetags(nid, tags)
  178. self._con.commit()
  179. def listtags(self):
  180. self._clean_orphans()
  181. get_tags = "select DATA from TAG"
  182. self._cur.execute(get_tags)
  183. tags = self._cur.fetchall()
  184. if tags:
  185. return [t[0] for t in tags]
  186. return [] # pragma: no cover
  187. # TODO: add this to test of postgresql and mysql!
  188. def editnode(self, nid, **kwargs):
  189. tags = kwargs.pop('tags', None)
  190. sql = ("UPDATE NODE SET {} WHERE ID = {} ".format(
  191. ','.join(['{}={}'.format(k, self._sub) for k in list(kwargs)]),
  192. self._sub))
  193. self._cur.execute(sql, (list(kwargs.values()) + [nid]))
  194. if tags:
  195. # update all old node entries in lookup
  196. # create new entries
  197. # clean all old tags
  198. sql_clean = "DELETE FROM LOOKUP WHERE NODEID={}".format(self._sub)
  199. self._cur.execute(sql_clean, (str(nid),))
  200. self._setnodetags(nid, tags)
  201. self._con.commit()
  202. def removenodes(self, nid):
  203. # shall we do this also in the sqlite driver?
  204. sql_clean = "DELETE FROM LOOKUP WHERE NODEID={}".format(self._sub)
  205. self._cur.execute(sql_clean, nid)
  206. sql_rm = "delete from NODE where ID = {}".format(self._sub)
  207. self._cur.execute(sql_rm, nid)
  208. self._con.commit()
  209. self._con.commit()
  210. def fetch_crypto_info(self):
  211. self._cur.execute("SELECT * FROM CRYPTO")
  212. row = self._cur.fetchone()
  213. return row
  214. def save_crypto_info(self, seed, digest):
  215. """save the random seed and the digested key"""
  216. self._cur.execute("DELETE FROM CRYPTO")
  217. self._cur.execute("INSERT INTO CRYPTO VALUES({}, {})".format(self._sub,
  218. self._sub), # noqa
  219. list(map(self._data_wrapper, (seed, digest))))
  220. self._con.commit()
  221. def loadkey(self):
  222. """
  223. return _keycrypted
  224. """
  225. sql = "SELECT * FROM CRYPTO"
  226. try:
  227. self._cur.execute(sql)
  228. seed, digest = self._cur.fetchone()
  229. return seed + '$6$' + digest
  230. except TypeError: # pragma: no cover
  231. return None
  232. def savekey(self, key):
  233. salt, digest = key.split('$6$')
  234. sql = "INSERT INTO CRYPTO(SEED, DIGEST) VALUES({},{})".format(self._sub, # noqa
  235. self._sub) # noqa
  236. self._cur.execute("DELETE FROM CRYPTO")
  237. self._cur.execute(sql, list(map(self._data_wrapper, (salt, digest))))
  238. self._digest = digest.encode()
  239. self._salt = salt.encode()
  240. self._con.commit()
  241. def close(self): # pragma: no cover
  242. self._clean_orphans()
  243. self._cur.close()
  244. self._con.close()