database.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943
  1. # -*- coding: utf-8 -*-
  2. """
  3. #
  4. # Simple password manager
  5. # Encrypted database
  6. #
  7. # Copyright (c) 2011-2023 Michael Büsch <m@bues.ch>
  8. # Licensed under the GNU/GPL version 2 or later.
  9. #
  10. """
  11. from libpwman.cryptsql import *
  12. from libpwman.exception import *
  13. from libpwman.util import *
  14. import libpwman.otp
  15. import csv
  16. import io
  17. import os
  18. import pathlib
  19. import sys
  20. from copy import deepcopy
  21. from dataclasses import dataclass
  22. __all__ = [
  23. "CSQLError",
  24. "PWManDatabase",
  25. "PWManEntry",
  26. "PWManEntryAttr",
  27. "PWManEntryBulk",
  28. "PWManEntryTOTP",
  29. "getDefaultDatabase",
  30. ]
  31. def getDefaultDatabase():
  32. """Get the default database path.
  33. Returns a pathlib.Path() instance.
  34. """
  35. db = os.getenv("PWMAN_DATABASE")
  36. if db:
  37. return pathlib.Path(db)
  38. home = pathlib.Path.home()
  39. if home:
  40. return home / ".pwman.db"
  41. return pathlib.Path(".pwman.db")
  42. @dataclass
  43. class PWManEntry:
  44. """Database entry data structure.
  45. """
  46. category : str
  47. title : str
  48. user : str = None
  49. pw : str = None
  50. entryId : int = None
  51. @dataclass
  52. class PWManEntryAttr:
  53. """Entry attribute data structure.
  54. """
  55. name : str
  56. data : str = None
  57. entry : PWManEntry = None
  58. attrId : int = None
  59. @dataclass
  60. class PWManEntryBulk:
  61. """Entry bulk-data data structure.
  62. """
  63. data : str = None
  64. entry : PWManEntry = None
  65. bulkId : int = None
  66. @dataclass
  67. class PWManEntryTOTP:
  68. """Entry TOTP-data data structure.
  69. """
  70. key : str
  71. digits : int = 6
  72. hmacHash : str = "SHA1"
  73. entry : PWManEntry = None
  74. totpId : int = None
  75. def generate(self):
  76. return libpwman.otp.totp(key=self.key,
  77. nrDigits=self.digits,
  78. hmacHash=self.hmacHash)
  79. class PWManDatabase(CryptSQL):
  80. """Encrypted pwman database.
  81. """
  82. DB_TYPE = "PWMan database"
  83. DB_VER = ("0", "1")
  84. def __init__(self, filename, passphrase, key=None, readOnly=True, silent=False):
  85. """filename: Path to the database file.
  86. If it does not exist, a new file is created.
  87. passphrase: The passphrase string for the database file.
  88. key: An optional key to use instead of the passphrase. Don't use it.
  89. readOnly: Open the filename read-only. Commits will raise an exception.
  90. silent: Do not print information messages to the console.
  91. """
  92. try:
  93. super().__init__(readOnly=readOnly)
  94. self.__silent = silent
  95. self.__dirty = False
  96. self.__openFile(filename, passphrase, key)
  97. except (CSQLError) as e:
  98. raise PWManError(str(e))
  99. def __openFile(self, filename, passphrase, key):
  100. """Open the database file and parse the contents.
  101. """
  102. super().setPassphrase(passphrase)
  103. self.setKey(key)
  104. self.open(filename)
  105. self.__setDirty(False)
  106. initDBVer = False
  107. if self.sqlIsEmpty():
  108. initDBVer = True
  109. else:
  110. dbType = self.getGlobalAttr("db_type")
  111. dbVer = self.getGlobalAttr("db_version")
  112. if dbType is None and dbVer is None: # Compat v0
  113. dbType = self.DB_TYPE
  114. dbVer = self.DB_VER[0]
  115. if (dbType != self.DB_TYPE or
  116. dbVer not in self.DB_VER):
  117. raise PWManError("Unsupported database version '%s / %s'. "
  118. "Expected '%s / %s'" % (
  119. str(dbType),
  120. str(dbVer),
  121. self.DB_TYPE,
  122. ", ".join(self.DB_VER)))
  123. if dbVer != self.DB_VER[-1]:
  124. self.__migrateVersion(dbVer)
  125. initDBVer = True
  126. self.__initTables()
  127. if initDBVer:
  128. self.setGlobalAttr("db_type", self.DB_TYPE, setDirty=False)
  129. self.setGlobalAttr("db_version", self.DB_VER[-1], setDirty=False)
  130. def __migrateVersion(self, dbVer):
  131. """Migrate the database format to the latest version.
  132. """
  133. if dbVer == self.DB_VER[0]:
  134. if not self.__silent:
  135. print("Migrating database from version %s to version %s..." % (
  136. dbVer, self.DB_VER[-1]),
  137. file=sys.stderr)
  138. self.__initTables()
  139. c = self.sqlExec("SELECT DISTINCT category FROM pw ORDER BY category;")
  140. categories = c.fetchAll()
  141. for (category, ) in categories:
  142. c = self.sqlExec("SELECT title FROM pw WHERE category=? ORDER BY title;",
  143. (category,))
  144. titles = c.fetchAll()
  145. for (title, ) in titles:
  146. c = self.sqlExec("SELECT category, title, user, pw, bulk FROM pw "
  147. "WHERE category=? AND title=? "
  148. "LIMIT 1;",
  149. (category, title))
  150. data = c.fetchOne()
  151. if not data:
  152. continue
  153. c = self.sqlExec("INSERT INTO entries(category, title, user, pw) "
  154. "VALUES(?,?,?,?);",
  155. (data[0], data[1], data[2], data[3]))
  156. entryId = c.lastRowID()
  157. if data[4]:
  158. c = self.sqlExec("INSERT INTO bulk(entry, data) "
  159. "VALUES(?,?);",
  160. (entryId, data[4]))
  161. c = self.sqlExec("SELECT name, data FROM info;")
  162. infos = c.fetchAll()
  163. for name, data in infos:
  164. c = self.sqlExec("INSERT INTO globalattr(name, data) VALUES(?,?);",
  165. (name, data))
  166. c = self.sqlExec("DROP TABLE IF EXISTS pw;")
  167. c = self.sqlExec("DROP TABLE IF EXISTS info;")
  168. self.sqlVacuum()
  169. else:
  170. assert(0)
  171. def __initTables(self):
  172. """Create the SQL tables, if they don't exist.
  173. """
  174. c = self.sqlExecScript("""
  175. CREATE TABLE IF NOT EXISTS
  176. globalattr(id INTEGER PRIMARY KEY AUTOINCREMENT,
  177. name TEXT, data TEXT);
  178. CREATE TABLE IF NOT EXISTS
  179. entries(id INTEGER PRIMARY KEY AUTOINCREMENT,
  180. category TEXT, title TEXT, user TEXT, pw TEXT);
  181. CREATE TABLE IF NOT EXISTS
  182. bulk(id INTEGER PRIMARY KEY AUTOINCREMENT,
  183. entry INTEGER, data TEXT);
  184. CREATE TABLE IF NOT EXISTS
  185. entryattr(id INTEGER PRIMARY KEY AUTOINCREMENT,
  186. entry INTEGER, name TEXT, data TEXT);
  187. CREATE TABLE IF NOT EXISTS
  188. totp(id INTEGER PRIMARY KEY AUTOINCREMENT,
  189. entry INTEGER, key TEXT, digits INTEGER, hash TEXT);
  190. """)
  191. def __garbageCollect(self):
  192. """Remove rows from the SQL database that are not needed anymore.
  193. """
  194. # Do not use sqlExecScript here, as that would commit transactions.
  195. c = self.sqlExec("DELETE FROM bulk WHERE entry NOT IN (SELECT id FROM entries);")
  196. c = self.sqlExec("DELETE FROM entryattr WHERE entry NOT IN (SELECT id FROM entries);")
  197. c = self.sqlExec("DELETE FROM totp WHERE entry NOT IN (SELECT id FROM entries);")
  198. def setPassphrase(self, passphrase):
  199. super().setPassphrase(passphrase)
  200. self.__setDirty()
  201. def categoryExists(self, category):
  202. """Returns True, if a category exists in the database.
  203. category: The name string of the category.
  204. """
  205. c = self.sqlExec("SELECT EXISTS(SELECT 1 FROM entries "
  206. "WHERE category=? "
  207. "LIMIT 1);",
  208. (category,))
  209. data = c.fetchOne()
  210. return bool(data) and bool(data[0])
  211. def getCategoryNames(self):
  212. """Get all category names in the database.
  213. Returns a sorted list of strings.
  214. """
  215. c = self.sqlExec("SELECT DISTINCT category FROM entries "
  216. "ORDER BY category;")
  217. return [ data[0] for data in c.fetchAll() ]
  218. def getEntryTitles(self, category):
  219. """Get all titles from one category in the database.
  220. category: The category name string.
  221. Returns a sorted list of strings.
  222. """
  223. c = self.sqlExec("SELECT title FROM entries "
  224. "WHERE category=? "
  225. "ORDER BY title;",
  226. (category,))
  227. return [ data[0] for data in c.fetchAll() ]
  228. def getEntry(self, category, title):
  229. """Get an entry from the database.
  230. category: The name string of the category to get an entry from.
  231. title: The title string of the entry to get.
  232. Returns a PWManEntry() instance.
  233. """
  234. c = self.sqlExec("SELECT id, category, title, user, pw FROM entries "
  235. "WHERE category=? AND title=? "
  236. "LIMIT 1;",
  237. (category,
  238. title))
  239. data = c.fetchOne()
  240. if not data:
  241. return None
  242. return PWManEntry(category=data[1],
  243. title=data[2],
  244. user=data[3],
  245. pw=data[4],
  246. entryId=data[0])
  247. def findEntries(self, pattern,
  248. useRegexp=False,
  249. search=True,
  250. inCategory=None,
  251. matchCategory=False,
  252. matchTitle=False,
  253. matchUser=False,
  254. matchPw=False,
  255. matchBulk=False,
  256. matchAttrName=False,
  257. matchAttrData=False):
  258. """Search the database for entries that match a pattern.
  259. useRegexp: If True, then the pattern is a regular expression string.
  260. If False, then the pattern is a SQL LIKE pattern string.
  261. inCategory: If specified as non-zero length string, then only search
  262. the category with this name.
  263. matchCategory: Match the pattern to the category name string of an entry.
  264. matchTitle: Match the pattern to the title string of an entry.
  265. matchUser: Match the pattern to the user string of an entry.
  266. matchPw: Match the pattern to the password string of an entry.
  267. matchBulk: Match the pattern to the bulk data string of an entry.
  268. matchAttrName: Match the pattern to all attribute name strings of an entry.
  269. matchAttrData: Match the pattern to all attribute data strings of an entry.
  270. Returns a list of PWManEntry() instances that match the pattern.
  271. """
  272. if useRegexp:
  273. self.setRegexpFlags(search=search,
  274. ignoreCase=True,
  275. multiLine=True,
  276. dotAll=True)
  277. else:
  278. if search:
  279. pattern = "%" + pattern + "%"
  280. def dump(sql, params):
  281. pass
  282. # print(sql, "\nparams =", params)
  283. def match(leftHand):
  284. if useRegexp:
  285. return "%s REGEXP ?" % leftHand
  286. return "%s LIKE ?" % leftHand
  287. IDs = set()
  288. if matchCategory or matchTitle or matchUser or matchPw:
  289. conditions = []
  290. if matchCategory:
  291. conditions.append( (match("entries.category"), pattern) )
  292. if matchTitle:
  293. conditions.append( (match("entries.title"), pattern) )
  294. if matchUser:
  295. conditions.append( (match("entries.user"), pattern) )
  296. if matchPw:
  297. conditions.append( (match("entries.pw"), pattern) )
  298. sql = "SELECT id FROM entries WHERE "
  299. params = []
  300. if inCategory:
  301. sql += "category=? AND "
  302. params.append(inCategory)
  303. sql += "( " + (" OR ".join(c[0] for c in conditions)) + " );"
  304. params.extend(c[1] for c in conditions)
  305. dump(sql, params)
  306. c = self.sqlExec(sql, params)
  307. IDs.update(entryId[0] for entryId in (c.fetchAll() or []))
  308. if matchBulk:
  309. conditions = [ (match("bulk.data"), pattern) ]
  310. sql = "SELECT entries.id "\
  311. "FROM entries, bulk "\
  312. "WHERE bulk.entry = entries.id AND "
  313. params = []
  314. if inCategory:
  315. sql += "entries.category = ? AND "
  316. params.append(inCategory)
  317. sql += match("bulk.data") + ";"
  318. params.append(pattern)
  319. dump(sql, params)
  320. c = self.sqlExec(sql, params)
  321. IDs.update(entryId[0] for entryId in (c.fetchAll() or []))
  322. if matchAttrName or matchAttrData:
  323. conditions = []
  324. if matchAttrName:
  325. conditions.append( (match("entryattr.name"), pattern) )
  326. if matchAttrData:
  327. conditions.append( (match("entryattr.data"), pattern) )
  328. sql = "SELECT entries.id "\
  329. "FROM entries, entryattr "\
  330. "WHERE entryattr.entry = entries.id AND "
  331. params = []
  332. if inCategory:
  333. sql += "entries.category = ? AND "
  334. params.append(inCategory)
  335. sql += "( " + (" OR ".join(c[0] for c in conditions)) + " );"
  336. params.extend(c[1] for c in conditions)
  337. dump(sql, params)
  338. c = self.sqlExec(sql, params)
  339. IDs.update(entryId[0] for entryId in (c.fetchAll() or []))
  340. if not IDs:
  341. return []
  342. IDs = sorted(IDs) # stable sorting
  343. sql = "SELECT entries.id, entries.category, "\
  344. "entries.title, entries.user, entries.pw "\
  345. "FROM entries "\
  346. "WHERE entries.id IN ( "
  347. sql += ", ".join("?" for ID in IDs)
  348. sql += " ) "
  349. sql += "ORDER BY entries.category, entries.title;"
  350. params = [ str(ID) for ID in IDs ]
  351. dump(sql, params)
  352. c = self.sqlExec(sql, params)
  353. dataSet = c.fetchAll()
  354. if not dataSet:
  355. return []
  356. return [ PWManEntry(category=data[1],
  357. title=data[2],
  358. user=data[3],
  359. pw=data[4],
  360. entryId=data[0])
  361. for data in dataSet ]
  362. def entryExists(self, category, title):
  363. """Returns True, if an entry exists in the database.
  364. category: The name string of the category.
  365. title: The title string of the entry.
  366. """
  367. c = self.sqlExec("SELECT EXISTS(SELECT 1 FROM entries "
  368. "WHERE category=? AND title=? "
  369. "LIMIT 1);",
  370. (category,
  371. title))
  372. data = c.fetchOne()
  373. return bool(data) and bool(data[0])
  374. def addEntry(self, entry):
  375. """Create a new entry in the database.
  376. entry: A PWManEntry() instance.
  377. """
  378. if self.entryExists(entry.category, entry.title):
  379. raise PWManError("Entry does already exist")
  380. c = self.sqlExec("INSERT INTO entries(category, title, user, pw) "
  381. "VALUES(?,?,?,?);",
  382. (entry.category,
  383. entry.title,
  384. entry.user,
  385. entry.pw))
  386. entry.entryId = c.lastRowID()
  387. self.__setDirty()
  388. def editEntry(self, entry):
  389. """Update the contents of an existing entry.
  390. entry: A PWManEntry() containing the new data of the entry/
  391. """
  392. oldEntry = self.getEntry(entry.category, entry.title)
  393. if not oldEntry:
  394. raise PWManError("Entry does not exist")
  395. if entry.user is None:
  396. entry.user = oldEntry.user
  397. if entry.pw is None:
  398. entry.pw = oldEntry.pw
  399. entry.entryId = oldEntry.entryId
  400. c = self.sqlExec("UPDATE entries SET "
  401. "category=?, title=?, user=?, pw=? "
  402. "WHERE id=?;",
  403. (entry.category,
  404. entry.title,
  405. entry.user,
  406. entry.pw,
  407. entry.entryId))
  408. self.__setDirty()
  409. def moveEntry(self, entry, newCategory, newTitle, toDb=None, copy=False):
  410. """Move or copy an existing entry to a new category and/or set a new entry title.
  411. entry: The PWManEntry() instance to move/copy.
  412. newCategory: The target category name string.
  413. newTitle: The target title string.
  414. toDb: The target database. Defaults to self.
  415. copy: If False, then move. If True, then copy.
  416. """
  417. toDb = toDb or self
  418. if toDb.entryExists(newCategory, newTitle):
  419. raise PWManError("Entry does already exist.")
  420. oldEntry = self.getEntry(entry.category, entry.title)
  421. if not oldEntry:
  422. raise PWManError("Entry does not exist.")
  423. if toDb is self and not copy:
  424. entry.category = newCategory
  425. entry.title = newTitle
  426. c = self.sqlExec("UPDATE entries SET "
  427. "category=?, title=? "
  428. "WHERE id=?;",
  429. (entry.category,
  430. entry.title,
  431. oldEntry.entryId))
  432. self.__setDirty()
  433. else:
  434. newEntry = deepcopy(oldEntry)
  435. bulk = self.getEntryBulk(newEntry)
  436. attrs = self.getEntryAttrs(newEntry)
  437. totp = self.getEntryTotp(newEntry)
  438. newEntry.entryId = None
  439. newEntry.category = newCategory
  440. newEntry.title = newTitle
  441. toDb.addEntry(newEntry)
  442. if bulk:
  443. bulk.bulkId = None
  444. toDb.setEntryBulk(bulk)
  445. for attr in attrs:
  446. attr.attrId = None
  447. toDb.setEntryAttr(attr)
  448. if totp:
  449. totp.totpId = None
  450. toDb.setEntryTotp(totp)
  451. if not copy:
  452. entry.entryId = newEntry.entryId
  453. entry.category = newEntry.category
  454. entry.title = newEntry.title
  455. self.delEntry(oldEntry)
  456. def moveEntries(self, fromCategory, toCategory, toDb=None, copy=False):
  457. """Move or copy all entries from one category to another category.
  458. fromCategory: The category to move all entries from.
  459. toCategory: The (new) category to move all entries to.
  460. toDb: The target database. Defaults to self.
  461. copy: If False, then move. If True, then copy.
  462. """
  463. toDb = toDb or self
  464. if not self.categoryExists(fromCategory):
  465. raise PWManError("Source category does not exist.")
  466. if toDb is self and fromCategory == toCategory:
  467. return
  468. fromTitles = self.getEntryTitles(fromCategory)
  469. for fromTitle in fromTitles:
  470. if toDb.entryExists(toCategory, fromTitle):
  471. raise PWManError("Target entry %s/%s does already exist." % (
  472. toCategory, fromTitle))
  473. if toDb is self and not copy:
  474. c = self.sqlExec("UPDATE entries SET category=? "
  475. "WHERE category=?;",
  476. (toCategory,
  477. fromCategory))
  478. self.__setDirty()
  479. else:
  480. for fromTitle in fromTitles:
  481. entry = self.getEntry(fromCategory, fromTitle)
  482. bulk = self.getEntryBulk(entry)
  483. attrs = self.getEntryAttrs(entry)
  484. totp = self.getEntryTotp(entry)
  485. entry.entryId = None
  486. entry.category = toCategory
  487. toDb.addEntry(entry)
  488. if bulk:
  489. bulk.bulkId = None
  490. toDb.setEntryBulk(bulk)
  491. for attr in attrs:
  492. attr.attrId = None
  493. toDb.setEntryAttr(attr)
  494. if totp:
  495. totp.totpId = None
  496. toDb.setEntryTotp(totp)
  497. if not copy:
  498. for fromTitle in fromTitles:
  499. entry = self.getEntry(fromCategory, fromTitle)
  500. self.delEntry(entry)
  501. def delEntry(self, entry):
  502. """Delete an existing entry from the database.
  503. entry: The PWManEntry() instance to delete from the database.
  504. """
  505. c = self.sqlExec("SELECT id FROM entries "
  506. "WHERE category=? AND title=? "
  507. "LIMIT 1;",
  508. (entry.category,
  509. entry.title))
  510. entryId = c.fetchOne()
  511. if entryId is None:
  512. raise PWManError("Entry does not exist")
  513. entryId = entryId[0]
  514. c = self.sqlExec("DELETE FROM entries WHERE id=?;",
  515. (entryId,))
  516. self.__garbageCollect()
  517. self.__setDirty()
  518. def getEntryBulk(self, entry):
  519. """Get the bulk data associated with an entry.
  520. entry: The PWManEntry() to get the bulk data for.
  521. Returns a PWManEntryBulk() instance or None, if there is no bulk data.
  522. """
  523. c = self.sqlExec("SELECT bulk.id, bulk.data "
  524. "FROM bulk, entries "
  525. "WHERE entries.category=? AND entries.title=? AND "
  526. "bulk.entry = entries.id "
  527. "LIMIT 1;",
  528. (entry.category,
  529. entry.title))
  530. data = c.fetchOne()
  531. if not data:
  532. return None
  533. return PWManEntryBulk(data=data[1],
  534. entry=entry,
  535. bulkId=data[0])
  536. def setEntryBulk(self, entryBulk):
  537. """Set the bulk data associated with an entry.
  538. entryBulk: The new PWManEntryBulk() instance to write to the database.
  539. If entryBulk.data is None, then the bulk data is deleted.
  540. """
  541. entry = entryBulk.entry
  542. if not entry or entry.entryId is None:
  543. raise PWManError("Bulk: Entry does not exist.")
  544. if entryBulk.data:
  545. c = self.sqlExec("SELECT id FROM bulk WHERE entry=? LIMIT 1;",
  546. (entry.entryId, ))
  547. bulkId = c.fetchOne()
  548. if bulkId is None:
  549. c = self.sqlExec("INSERT INTO bulk(entry, data) "
  550. "VALUES(?,?);",
  551. (entry.entryId,
  552. entryBulk.data))
  553. else:
  554. bulkId = bulkId[0]
  555. c = self.sqlExec("UPDATE bulk "
  556. "SET entry=?, data=? "
  557. "WHERE id=?;",
  558. (entry.entryId,
  559. entryBulk.data,
  560. bulkId))
  561. else:
  562. c = self.sqlExec("DELETE FROM bulk WHERE id=?;",
  563. (entryBulk.bulkId,))
  564. self.__setDirty()
  565. def getEntryTotp(self, entry):
  566. """Get the TOTP parameters associated with an entry.
  567. entry: The PWManEntry() to get the TOTP parameters for.
  568. Returns a PWManEntryTOTP() instance, or None if there is no TOTP data.
  569. """
  570. c = self.sqlExec("SELECT totp.id, totp.key, totp.digits, totp.hash "
  571. "FROM totp, entries "
  572. "WHERE entries.category=? AND entries.title=? AND "
  573. "totp.entry = entries.id "
  574. "LIMIT 1;",
  575. (entry.category,
  576. entry.title))
  577. data = c.fetchOne()
  578. if not data:
  579. return None
  580. return PWManEntryTOTP(key=data[1],
  581. digits=data[2],
  582. hmacHash=data[3],
  583. entry=entry,
  584. totpId=data[0])
  585. def setEntryTotp(self, entryTotp):
  586. """Set the TOTP data associated with an entry.
  587. entryTotp: The new PWManEntryTOTP() instance to write to the database.
  588. If entryTotp.key is None, then the TOTP data is deleted.
  589. """
  590. entry = entryTotp.entry
  591. if not entry or entry.entryId is None:
  592. raise PWManError("TOTP: Entry does not exist.")
  593. if entryTotp.key:
  594. c = self.sqlExec("SELECT id FROM totp WHERE entry=? LIMIT 1;",
  595. (entry.entryId, ))
  596. totpId = c.fetchOne()
  597. if totpId is None:
  598. c = self.sqlExec("INSERT INTO totp(entry, key, digits, hash) "
  599. "VALUES(?,?,?,?);",
  600. (entry.entryId,
  601. entryTotp.key,
  602. entryTotp.digits,
  603. entryTotp.hmacHash))
  604. else:
  605. totpId = totpId[0]
  606. c = self.sqlExec("UPDATE totp "
  607. "SET entry=?, key=?, digits=?, hash=? "
  608. "WHERE id=?;",
  609. (entry.entryId,
  610. entryTotp.key,
  611. entryTotp.digits,
  612. entryTotp.hmacHash,
  613. totpId))
  614. else:
  615. c = self.sqlExec("DELETE FROM totp WHERE id=?;",
  616. (entryTotp.totpId,))
  617. self.__setDirty()
  618. def getEntryAttr(self, entry, attrName):
  619. """Get an attribute associated with an entry.
  620. entry: The PWManEntry() to get the attribute for.
  621. attrName: The name string of the attribute to get.
  622. Returns a PWManEntryAttr() instance, or None if there is such attribute.
  623. """
  624. c = self.sqlExec("SELECT entryattr.id, entryattr.name, entryattr.data "
  625. "FROM entryattr, entries "
  626. "WHERE entries.category=? AND entries.title=? AND "
  627. "entryattr.entry = entries.id AND entryattr.name=? "
  628. "LIMIT 1;",
  629. (entry.category,
  630. entry.title,
  631. attrName))
  632. data = c.fetchOne()
  633. if not data:
  634. return None
  635. return PWManEntryAttr(name=data[1],
  636. data=data[2],
  637. entry=entry,
  638. attrId=data[0])
  639. def getEntryAttrs(self, entry):
  640. """Get all attributes associated with an entry.
  641. entry: The PWManEntry() to get the attributes for.
  642. Returns a list of PWManEntryAttr() instances,
  643. or an empty list if there are no attributes.
  644. """
  645. c = self.sqlExec("SELECT entryattr.id, entryattr.name, entryattr.data "
  646. "FROM entryattr, entries "
  647. "WHERE entries.category=? AND entries.title=? AND "
  648. "entryattr.entry = entries.id "
  649. "ORDER BY entryattr.name;",
  650. (entry.category,
  651. entry.title))
  652. dataSet = c.fetchAll()
  653. if not dataSet:
  654. return []
  655. return [ PWManEntryAttr(name=data[1],
  656. data=data[2],
  657. entry=entry,
  658. attrId=data[0])
  659. for data in dataSet ]
  660. def setEntryAttr(self, entryAttr):
  661. """Set an attribute associated with an entry.
  662. entryAttr: The new PWManEntryAttr() instance to write to the database.
  663. If entryAttr.data is None, then the attribute is deleted.
  664. """
  665. entry = entryAttr.entry
  666. if not entry or entry.entryId is None:
  667. raise PWManError("Attr: Entry does not exist.")
  668. if entryAttr.data:
  669. c = self.sqlExec("SELECT id FROM entryattr "
  670. "WHERE entry=? AND name=? "
  671. "LIMIT 1;",
  672. (entry.entryId,
  673. entryAttr.name))
  674. attrId = c.fetchOne()
  675. if attrId is None:
  676. c = self.sqlExec("INSERT INTO entryattr(entry, name, data) "
  677. "VALUES(?,?,?);",
  678. (entry.entryId,
  679. entryAttr.name,
  680. entryAttr.data))
  681. else:
  682. attrId = attrId[0]
  683. c = self.sqlExec("UPDATE entryattr "
  684. "SET entry=?, name=?, data=? "
  685. "WHERE id=?;",
  686. (entry.entryId,
  687. entryAttr.name,
  688. entryAttr.data,
  689. attrId))
  690. else:
  691. c = self.sqlExec("DELETE FROM entryattr WHERE id=?;",
  692. (entryAttr.attrId,))
  693. self.__setDirty()
  694. def getGlobalAttr(self, name):
  695. """Get a global attribute.
  696. A global attribute is not associated with an entry.
  697. Returns None, if the attribute does not exist.
  698. """
  699. try:
  700. c = self.sqlExec("SELECT id, data FROM globalattr "
  701. "WHERE name=? "
  702. "LIMIT 1;",
  703. (name,))
  704. data = c.fetchOne()
  705. return data[1] if data else None
  706. except (CSQLError) as e:
  707. return None
  708. def setGlobalAttr(self, name, data, setDirty=True):
  709. """Set a global attribute.
  710. A global attribute is not associated with an entry.
  711. If data is None or empty, the attribute is deleted from the database.
  712. """
  713. if data:
  714. c = self.sqlExec("SELECT id FROM globalattr "
  715. "WHERE name=? "
  716. "LIMIT 1;",
  717. (name,))
  718. attrId = c.fetchOne()
  719. if attrId is None:
  720. c = self.sqlExec("INSERT INTO globalattr(name, data) "
  721. "VALUES(?,?);",
  722. (name, data))
  723. else:
  724. attrId = attrId[0]
  725. c = self.sqlExec("UPDATE globalattr "
  726. "SET data=? "
  727. "WHERE name=?;",
  728. (data, name))
  729. else:
  730. c = self.sqlExec("DELETE FROM globalattr WHERE name=?;",
  731. (name,))
  732. if setDirty:
  733. self.__setDirty()
  734. def __setDirty(self, d=True):
  735. """Set the flag for uncommitted data.
  736. """
  737. self.__dirty = d
  738. def isDirty(self):
  739. """Returns True, if the database contains uncommitted data.
  740. """
  741. return self.__dirty
  742. def flunkDirty(self):
  743. """Print a warning, if the database contains uncommitted data.
  744. Then set the flag for uncommitted data to False.
  745. """
  746. if self.isDirty():
  747. print("WARNING: Dropping uncommitted data",
  748. file=sys.stderr)
  749. self.__setDirty(False)
  750. def dropUncommitted(self):
  751. super().dropUncommitted()
  752. self.__setDirty(False)
  753. def commit(self):
  754. self.__garbageCollect()
  755. super().commit()
  756. self.__setDirty(False)
  757. def importSqlScript(self, *args, **kwargs):
  758. self.__setDirty()
  759. super().importSqlScript(*args, **kwargs)
  760. def getOnDiskDb(self):
  761. """Get a read-only instance of PWManDatabase that contains
  762. the current on-disk data. The on-disk data is the data
  763. at the last commit.
  764. """
  765. db = self.__class__(filename=self.getFilename(),
  766. passphrase=self.getPassphrase(),
  767. key=self.getKey(),
  768. readOnly=True,
  769. silent=True)
  770. return db
  771. def dumpEntry(self, entry, totp="hide"):
  772. """Returns a human readable dump string of an entry.
  773. """
  774. res = []
  775. res.append("=== %s ===" % entry.category)
  776. res.append("\t--- %s ---" % entry.title)
  777. if entry.user:
  778. res.append("\tUser:\t\t%s" % entry.user)
  779. if entry.pw:
  780. res.append("\tPassword:\t%s" % entry.pw)
  781. entryBulk = self.getEntryBulk(entry)
  782. if entryBulk:
  783. res.append("\tBulk data:\t%s" % entryBulk.data)
  784. entryTotp = self.getEntryTotp(entry)
  785. if entryTotp:
  786. if totp == "show":
  787. res.append("\tTOTP key:\t%s" % entryTotp.key)
  788. res.append("\tTOTP digits:\t%d" % entryTotp.digits)
  789. res.append("\tTOTP hash:\t%s" % entryTotp.hmacHash)
  790. elif totp == "gen":
  791. try:
  792. token = entryTotp.generate()
  793. except libpwman.otp.OtpError as e:
  794. raise PWManError("Failed to generate TOTP token: "
  795. "%s" % str(e))
  796. res.append(token)
  797. elif totp == "hide":
  798. res.append("\tTOTP:\t\tavailable")
  799. else:
  800. assert False
  801. entryAttrs = self.getEntryAttrs(entry)
  802. if entryAttrs:
  803. res.append("\tAttributes:")
  804. maxLen = max(len(a.name) for a in entryAttrs)
  805. for entryAttr in entryAttrs:
  806. align = maxLen - len(entryAttr.name)
  807. res.append("\t %s:%s %s" % (
  808. entryAttr.name,
  809. align * " ",
  810. entryAttr.data))
  811. return "\n".join(res) + "\n"
  812. def dumpEntries(self, totp="hide"):
  813. """Returns a human readable dump string of all entries.
  814. """
  815. ret = []
  816. for category in self.getCategoryNames():
  817. for title in self.getEntryTitles(category):
  818. entry = self.getEntry(category, title)
  819. dump = self.dumpEntry(entry, totp)
  820. ret.append(dump)
  821. return "\n".join(ret)
  822. def dumpEntriesCsv(self, totp="hide"):
  823. """Returns a CSV format dump string of all entries.
  824. """
  825. csvHeads = [
  826. "Category",
  827. "Title",
  828. "User",
  829. "Password",
  830. "Bulk data",
  831. "TOTP key",
  832. "TOTP digits",
  833. "TOTP hash",
  834. ]
  835. rows = []
  836. attrNames = set()
  837. for category in self.getCategoryNames():
  838. for title in self.getEntryTitles(category):
  839. entry = self.getEntry(category, title)
  840. row = {
  841. "Category" : entry.category,
  842. "Title" : entry.title,
  843. "User" : entry.user,
  844. "Password" : entry.pw,
  845. }
  846. entryBulk = self.getEntryBulk(entry)
  847. if entryBulk:
  848. row["Bulk data"] = entryBulk.data
  849. entryTotp = self.getEntryTotp(entry)
  850. if entryTotp:
  851. if totp == "show":
  852. row["TOTP key"] = entryTotp.key
  853. row["TOTP digits"] = entryTotp.digits
  854. row["TOTP hash"] = entryTotp.hmacHash
  855. elif totp == "gen":
  856. try:
  857. token = entryTotp.generate()
  858. except libpwman.otp.OtpError as e:
  859. raise PWManError("Failed to generate TOTP token: "
  860. "%s" % str(e))
  861. row["TOTP"] = token
  862. elif totp == "hide":
  863. row["TOTP key"] = "available"
  864. else:
  865. assert False
  866. entryAttrs = self.getEntryAttrs(entry)
  867. if entryAttrs:
  868. for entryAttr in entryAttrs:
  869. attrNames.add(entryAttr.name)
  870. row[entryAttr.name] = entryAttr.data
  871. rows.append(row)
  872. csvHeads.extend(sorted(attrNames))
  873. f = io.StringIO()
  874. w = csv.DictWriter(f, fieldnames=csvHeads, dialect="excel")
  875. w.writeheader()
  876. for r in rows:
  877. w.writerow(r)
  878. return f.getvalue()