/src/lib/datahandler/pwsafe.py

https://bitbucket.org/erikg/revelation/ · Python · 715 lines · 452 code · 223 blank · 40 comment · 80 complexity · 9f40f45d80a1b730e5cd8e64834c8c8a MD5 · raw file

  1. #
  2. # Revelation - a password manager for GNOME 2
  3. # http://oss.codepoet.no/revelation/
  4. # $Id$
  5. #
  6. # Module for handling PasswordSafe data
  7. #
  8. #
  9. # Copyright (c) 2003-2006 Erik Grinaker
  10. #
  11. # This program is free software; you can redistribute it and/or
  12. # modify it under the terms of the GNU General Public License
  13. # as published by the Free Software Foundation; either version 2
  14. # of the License, or (at your option) any later version.
  15. #
  16. # This program is distributed in the hope that it will be useful,
  17. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. # GNU General Public License for more details.
  20. #
  21. # You should have received a copy of the GNU General Public License
  22. # along with this program; if not, write to the Free Software
  23. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  24. #
  25. import base
  26. from revelation import data, entry, util
  27. import locale, re, struct
  28. from Crypto.Cipher import Blowfish
  29. FIELDTYPE_NAME = 0x00
  30. FIELDTYPE_UUID = 0x01
  31. FIELDTYPE_GROUP = 0x02
  32. FIELDTYPE_TITLE = 0x03
  33. FIELDTYPE_USER = 0x04
  34. FIELDTYPE_NOTES = 0x05
  35. FIELDTYPE_PASSWORD = 0x06
  36. FIELDTYPE_END = 0xff
  37. # We need our own SHA1-implementation, because Password Safe does
  38. # non-standard things we need to replicate. This implementation is
  39. # written by J. Hallen and L. Creighton for the Pypy project, with
  40. # slight modifications by Erik Grinaker.
  41. class SHA:
  42. K = [
  43. 0x5A827999L,
  44. 0x6ED9EBA1L,
  45. 0x8F1BBCDCL,
  46. 0xCA62C1D6L
  47. ]
  48. def __init__(self, input = None):
  49. self.count = [0, 0]
  50. self.init()
  51. if input != None:
  52. self.update(input)
  53. def __bytelist2longBigEndian(self, list):
  54. imax = len(list)/4
  55. hl = [0L] * imax
  56. j = 0
  57. i = 0
  58. while i < imax:
  59. b0 = long(ord(list[j])) << 24
  60. b1 = long(ord(list[j+1])) << 16
  61. b2 = long(ord(list[j+2])) << 8
  62. b3 = long(ord(list[j+3]))
  63. hl[i] = b0 | b1 | b2 | b3
  64. i = i+1
  65. j = j+4
  66. return hl
  67. def __long2bytesBigEndian(self, n, blocksize=0):
  68. s = ''
  69. pack = struct.pack
  70. while n > 0:
  71. s = pack('>I', n & 0xffffffffL) + s
  72. n = n >> 32
  73. for i in range(len(s)):
  74. if s[i] <> '\000':
  75. break
  76. else:
  77. s = '\000'
  78. i = 0
  79. s = s[i:]
  80. if blocksize > 0 and len(s) % blocksize:
  81. s = (blocksize - len(s) % blocksize) * '\000' + s
  82. return s
  83. def __rotateLeft(self, x, n):
  84. return (x << n) | (x >> (32-n))
  85. def __transform(self, W):
  86. for t in range(16, 80):
  87. W.append(self.__rotateLeft(
  88. W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16], 1) & 0xffffffffL)
  89. A = self.H0
  90. B = self.H1
  91. C = self.H2
  92. D = self.H3
  93. E = self.H4
  94. for t in range(0, 20):
  95. TEMP = self.__rotateLeft(A, 5) + ((B & C) | ((~ B) & D)) + E + W[t] + self.K[0]
  96. E = D
  97. D = C
  98. C = self.__rotateLeft(B, 30) & 0xffffffffL
  99. B = A
  100. A = TEMP & 0xffffffffL
  101. for t in range(20, 40):
  102. TEMP = self.__rotateLeft(A, 5) + (B ^ C ^ D) + E + W[t] + self.K[1]
  103. E = D
  104. D = C
  105. C = self.__rotateLeft(B, 30) & 0xffffffffL
  106. B = A
  107. A = TEMP & 0xffffffffL
  108. for t in range(40, 60):
  109. TEMP = self.__rotateLeft(A, 5) + ((B & C) | (B & D) | (C & D)) + E + W[t] + self.K[2]
  110. E = D
  111. D = C
  112. C = self.__rotateLeft(B, 30) & 0xffffffffL
  113. B = A
  114. A = TEMP & 0xffffffffL
  115. for t in range(60, 80):
  116. TEMP = self.__rotateLeft(A, 5) + (B ^ C ^ D) + E + W[t] + self.K[3]
  117. E = D
  118. D = C
  119. C = self.__rotateLeft(B, 30) & 0xffffffffL
  120. B = A
  121. A = TEMP & 0xffffffffL
  122. self.H0 = (self.H0 + A) & 0xffffffffL
  123. self.H1 = (self.H1 + B) & 0xffffffffL
  124. self.H2 = (self.H2 + C) & 0xffffffffL
  125. self.H3 = (self.H3 + D) & 0xffffffffL
  126. self.H4 = (self.H4 + E) & 0xffffffffL
  127. def digest(self):
  128. H0 = self.H0
  129. H1 = self.H1
  130. H2 = self.H2
  131. H3 = self.H3
  132. H4 = self.H4
  133. input = [] + self.input
  134. count = [] + self.count
  135. index = (self.count[1] >> 3) & 0x3fL
  136. if index < 56:
  137. padLen = 56 - index
  138. else:
  139. padLen = 120 - index
  140. padding = ['\200'] + ['\000'] * 63
  141. self.update(padding[:padLen])
  142. bits = self.__bytelist2longBigEndian(self.input[:56]) + count
  143. self.__transform(bits)
  144. digest = self.__long2bytesBigEndian(self.H0, 4) + \
  145. self.__long2bytesBigEndian(self.H1, 4) + \
  146. self.__long2bytesBigEndian(self.H2, 4) + \
  147. self.__long2bytesBigEndian(self.H3, 4) + \
  148. self.__long2bytesBigEndian(self.H4, 4)
  149. self.H0 = H0
  150. self.H1 = H1
  151. self.H2 = H2
  152. self.H3 = H3
  153. self.H4 = H4
  154. self.input = input
  155. self.count = count
  156. return digest
  157. def hexdigest(self):
  158. return ''.join(['%02x' % ord(c) for c in self.digest()])
  159. def init(self, H0 = 0x67452301L, H1 = 0xEFCDAB89L, H2 = 0x98BADCFEL, H3 = 0x10325476L, H4 = 0xC3D2E1F0L):
  160. self.length = 0L
  161. self.input = []
  162. self.H0 = H0
  163. self.H1 = H1
  164. self.H2 = H2
  165. self.H3 = H3
  166. self.H4 = H4
  167. def update(self, inBuf):
  168. leninBuf = long(len(inBuf))
  169. index = (self.count[1] >> 3) & 0x3FL
  170. self.count[1] = self.count[1] + (leninBuf << 3)
  171. if self.count[1] < (leninBuf << 3):
  172. self.count[0] = self.count[0] + 1
  173. self.count[0] = self.count[0] + (leninBuf >> 29)
  174. partLen = 64 - index
  175. if leninBuf >= partLen:
  176. self.input[index:] = list(inBuf[:partLen])
  177. self.__transform(self.__bytelist2longBigEndian(self.input))
  178. i = partLen
  179. while i + 63 < leninBuf:
  180. self.__transform(self.__bytelist2longBigEndian(list(inBuf[i:i+64])))
  181. i = i + 64
  182. else:
  183. self.input = list(inBuf[i:leninBuf])
  184. else:
  185. i = 0
  186. self.input = self.input + list(inBuf)
  187. # misc functions common to both datahandlers
  188. def decrypt(key, ciphertext, iv = None):
  189. "Decrypts data"
  190. if len(ciphertext) % 8 != 0:
  191. raise base.FormatError
  192. cipher = Blowfish.new(key)
  193. cbc = iv
  194. plaintext = ""
  195. for cipherblock in [ ciphertext[i * 8 : (i + 1) * 8] for i in range(len(ciphertext) / 8) ]:
  196. plainblock = decrypt_block(cipher, cipherblock)
  197. if cbc != None:
  198. plainblock = "".join([ chr(ord(plainblock[i]) ^ ord(cbc[i])) for i in range(len(plainblock)) ])
  199. cbc = cipherblock
  200. plaintext += plainblock
  201. return plaintext
  202. def decrypt_block(cipher, block):
  203. "Decrypts a block with the given cipher"
  204. block = block[3] + block[2] + block[1] + block[0] + block[7] + block[6] + block[5] + block[4]
  205. block = cipher.decrypt(block)
  206. block = block[3] + block[2] + block[1] + block[0] + block[7] + block[6] + block[5] + block[4]
  207. return block
  208. def encrypt(key, plaintext, iv = None):
  209. "Encrypts data"
  210. if len(plaintext) % 8 != 0:
  211. raise base.FormatError
  212. cipher = Blowfish.new(key)
  213. cbc = iv
  214. ciphertext = ""
  215. for plainblock in [ plaintext[i * 8 : (i + 1) * 8] for i in range(len(plaintext) / 8) ]:
  216. if cbc != None:
  217. plainblock = "".join([ chr(ord(plainblock[i]) ^ ord(cbc[i])) for i in range(len(plainblock)) ])
  218. cipherblock = encrypt_block(cipher, plainblock)
  219. ciphertext += cipherblock
  220. if cbc != None:
  221. cbc = cipherblock
  222. return ciphertext
  223. def encrypt_block(cipher, block):
  224. "Encrypts a block with the given cipher"
  225. block = block[3] + block[2] + block[1] + block[0] + block[7] + block[6] + block[5] + block[4]
  226. block = cipher.encrypt(block)
  227. block = block[3] + block[2] + block[1] + block[0] + block[7] + block[6] + block[5] + block[4]
  228. return block
  229. def generate_testhash(password, random):
  230. "Generates a testhash based on a password and a random string"
  231. key = SHA(random + "\x00\x00" + password).digest()
  232. cipher = Blowfish.new(key)
  233. for i in range(1000):
  234. random = encrypt_block(cipher, random)
  235. h = SHA()
  236. h.init(0L, 0L, 0L, 0L, 0L)
  237. h.update(random)
  238. h.update("\x00\x00")
  239. testhash = h.digest()
  240. return testhash
  241. def create_field(value, type = FIELDTYPE_NAME):
  242. "Creates a field"
  243. field = struct.pack("ii", len(value), type) + value
  244. if len(value) == 0 or len(value) % 8 != 0:
  245. field += "\x00" * (8 - len(value) % 8)
  246. return field
  247. def normalize_field(field):
  248. "Normalizes a field value"
  249. enc = locale.getpreferredencoding()
  250. field = field.replace("\x00", "")
  251. field = re.sub("\s+", " ", field)
  252. field = field.strip()
  253. field = field.decode(enc, "replace")
  254. field = field.encode("utf-8", "replace")
  255. return field
  256. def parse_field_header(header):
  257. "Parses field data, returns the length and type"
  258. if len(header) < 8:
  259. raise base.FormatError
  260. length, type = struct.unpack("ii", header[:8])
  261. if length == 0 or length % 8 != 0:
  262. length += 8 - length % 8
  263. return length, type
  264. class PasswordSafe1(base.DataHandler):
  265. "Data handler for PasswordSafe 1.x data"
  266. name = "Password Safe 1.x"
  267. importer = True
  268. exporter = True
  269. encryption = True
  270. def __init__(self):
  271. base.DataHandler.__init__(self)
  272. def check(self, input):
  273. "Checks if the data is valid"
  274. if input is None:
  275. raise base.FormatError
  276. if len(input) < 56:
  277. raise base.FormatError
  278. if (len(input) - 56) % 8 != 0:
  279. raise base.FormatError
  280. def export_data(self, entrystore, password):
  281. "Exports data from an entrystore"
  282. # serialize data
  283. enc = locale.getpreferredencoding()
  284. db = ""
  285. iter = entrystore.iter_children(None)
  286. while iter is not None:
  287. e = entrystore.get_entry(iter)
  288. if type(e) != entry.FolderEntry:
  289. e = e.convert_generic()
  290. edata = ""
  291. edata += create_field(e.name.encode(enc, "replace") + "\xAD" + e[entry.UsernameField].encode("iso-8859-1"))
  292. edata += create_field(e[entry.PasswordField].encode(enc, "replace"))
  293. edata += create_field(e.description.encode(enc, "replace"))
  294. db += edata
  295. iter = entrystore.iter_traverse_next(iter)
  296. # encrypt data
  297. random = util.random_string(8)
  298. salt = util.random_string(20)
  299. iv = util.random_string(8)
  300. testhash = generate_testhash(password, random)
  301. ciphertext = encrypt(SHA(password + salt).digest(), db, iv)
  302. return random + testhash + salt + iv + ciphertext
  303. def import_data(self, input, password):
  304. "Imports data into an entrystore"
  305. # read header and test password
  306. if password is None:
  307. raise base.PasswordError
  308. random = input[0:8]
  309. testhash = input[8:28]
  310. salt = input[28:48]
  311. iv = input[48:56]
  312. if testhash != generate_testhash(password, random):
  313. raise base.PasswordError
  314. # load data
  315. db = decrypt(SHA(password + salt).digest(), input[56:], iv)
  316. entrystore = data.EntryStore()
  317. while len(db) > 0:
  318. dbentry = { "name" : "", "username" : "", "password" : "", "note" : "" }
  319. for f in ( "name", "password", "note" ):
  320. flen, ftype = parse_field_header(db[:8])
  321. value = db[8:8 + flen]
  322. if f == "name" and "\xAD" in value:
  323. value, dbentry["username"] = value.split("\xAD", 1)
  324. dbentry[f] = value
  325. db = db[8 + flen:]
  326. e = entry.GenericEntry()
  327. e.name = normalize_field(dbentry["name"])
  328. e.description = normalize_field(dbentry["note"])
  329. e[entry.UsernameField] = normalize_field(dbentry["username"])
  330. e[entry.PasswordField] = normalize_field(dbentry["password"])
  331. entrystore.add_entry(e)
  332. return entrystore
  333. class PasswordSafe2(base.DataHandler):
  334. "Data handler for PasswordSafe 2.x data"
  335. name = "Password Safe 2.x"
  336. importer = True
  337. exporter = True
  338. encryption = True
  339. def __init__(self):
  340. base.DataHandler.__init__(self)
  341. def __get_group(self, entrystore, iter):
  342. "Returns the group path for an iter"
  343. path = []
  344. iter = entrystore.iter_parent(iter)
  345. while iter is not None:
  346. path.append(entrystore.get_entry(iter).name)
  347. iter = entrystore.iter_parent(iter)
  348. path.reverse()
  349. return ".".join(path)
  350. def __setup_group(self, entrystore, groupmap, group):
  351. "Sets up a group folder, or returns an existing one"
  352. if group in ( None, "" ):
  353. return None
  354. if groupmap.has_key(group):
  355. return groupmap[group]
  356. if "." in group:
  357. parent, groupname = group.rsplit(".", 1)
  358. parentiter = self.__setup_group(entrystore, groupmap, parent)
  359. else:
  360. groupname = group
  361. parentiter = None
  362. e = entry.FolderEntry()
  363. e.name = groupname
  364. iter = entrystore.add_entry(e, parentiter)
  365. groupmap[group] = iter
  366. return iter
  367. def check(self, input):
  368. "Checks if the data is valid"
  369. if input is None:
  370. raise base.FormatError
  371. if len(input) < 56:
  372. raise base.FormatError
  373. if (len(input) - 56) % 8 != 0:
  374. raise base.FormatError
  375. def export_data(self, entrystore, password):
  376. "Exports data from an entrystore"
  377. # set up magic entry at start of database
  378. db = ""
  379. db += "\x48\x00\x00\x00\x00\x00\x00\x00"
  380. db += " !!!Version 2 File Format!!! Please upgrade to PasswordSafe 2.0 or later"
  381. db += "\x03\x00\x00\x00\x06\x00\x00\x00"
  382. db += "2.0\x00\x00\x00\x00\x00"
  383. db += "\x00\x00\x00\x00\x06\x00\x00\x00"
  384. db += "\x00\x00\x00\x00\x00\x00\x00\x00"
  385. # serialize data
  386. uuids = []
  387. iter = entrystore.iter_children(None)
  388. enc = locale.getpreferredencoding()
  389. while iter is not None:
  390. e = entrystore.get_entry(iter)
  391. if type(e) != entry.FolderEntry:
  392. e = e.convert_generic()
  393. uuid = util.random_string(16)
  394. while uuid in uuids:
  395. uuid = util.random_string(16)
  396. edata = ""
  397. edata += create_field(uuid, FIELDTYPE_UUID)
  398. edata += create_field(self.__get_group(entrystore, iter), FIELDTYPE_GROUP)
  399. edata += create_field(e.name.encode(enc, "replace"), FIELDTYPE_TITLE)
  400. s = e[entry.UsernameField]
  401. if s is None:
  402. s = ""
  403. edata += create_field(s.encode(enc, "replace"), FIELDTYPE_USER)
  404. edata += create_field(e[entry.PasswordField].encode(enc, "replace"), FIELDTYPE_PASSWORD)
  405. edata += create_field(e.description.encode(enc, "replace"), FIELDTYPE_NOTES)
  406. edata += create_field("", FIELDTYPE_END)
  407. db += edata
  408. iter = entrystore.iter_traverse_next(iter)
  409. # encrypt data
  410. random = util.random_string(8)
  411. salt = util.random_string(20)
  412. iv = util.random_string(8)
  413. testhash = generate_testhash(password, random)
  414. ciphertext = encrypt(SHA(password + salt).digest(), db, iv)
  415. return random + testhash + salt + iv + ciphertext
  416. def import_data(self, input, password):
  417. "Imports data into an entrystore"
  418. # read header and test password
  419. if password is None:
  420. raise base.PasswordError
  421. random = input[0:8]
  422. testhash = input[8:28]
  423. salt = input[28:48]
  424. iv = input[48:56]
  425. if testhash != generate_testhash(password, random):
  426. raise base.PasswordError
  427. # load data
  428. db = decrypt(SHA(password + salt).digest(), input[56:], iv)
  429. entrystore = data.EntryStore()
  430. # read magic entry
  431. for f in "magic", "version", "prefs":
  432. flen, ftype = parse_field_header(db)
  433. value = db[8:8 + flen]
  434. if f == "magic" and value != " !!!Version 2 File Format!!! Please upgrade to PasswordSafe 2.0 or later":
  435. raise base.FormatError
  436. db = db[8 + flen:]
  437. # import entries
  438. e = entry.GenericEntry()
  439. group = None
  440. groupmap = {}
  441. while len(db) > 0:
  442. flen, ftype = parse_field_header(db)
  443. value = normalize_field(db[8:8 + flen])
  444. if ftype == FIELDTYPE_NAME:
  445. if "\xAD" not in value:
  446. e.name = value
  447. else:
  448. n, u = value.split("\xAD", 1)
  449. e.name = normalize_field(n)
  450. e[entry.UsernameField] = normalize_field(u)
  451. elif ftype == FIELDTYPE_TITLE:
  452. e.name = value
  453. elif ftype == FIELDTYPE_USER:
  454. e[entry.UsernameField] = value
  455. elif ftype == FIELDTYPE_PASSWORD:
  456. e[entry.PasswordField] = value
  457. elif ftype == FIELDTYPE_NOTES:
  458. e.description = value
  459. elif ftype == FIELDTYPE_UUID:
  460. pass
  461. elif ftype == FIELDTYPE_GROUP:
  462. group = value
  463. elif ftype == FIELDTYPE_END:
  464. if group not in ( None, "" ):
  465. parent = self.__setup_group(entrystore, groupmap, group)
  466. else:
  467. parent = None
  468. entrystore.add_entry(e, parent)
  469. e = entry.GenericEntry()
  470. group = None
  471. else:
  472. pass
  473. db = db[8 + flen:]
  474. return entrystore
  475. class MyPasswordSafe(PasswordSafe2):
  476. "Data handler for MyPasswordSafe data"
  477. name = "MyPasswordSafe"
  478. importer = True
  479. exporter = True
  480. encryption = True
  481. class MyPasswordSafeOld(PasswordSafe1):
  482. "Data handler for old MyPasswordSafe data"
  483. name = "MyPasswordSafe (old format)"
  484. importer = True
  485. exporter = True
  486. encryption = True
  487. class PasswordGorilla(PasswordSafe2):
  488. "Data handler for Password Gorilla data"
  489. name = "Password Gorilla"
  490. importer = True
  491. exporter = True
  492. encryption = True