+ eq = line.index('=')
+ return line[:eq], line[eq + 1:]
+
+ def _parse_file(me, file, magic = None):
+ """
+ Parse a FILE.
+
+ Specifically:
+
+ * Raise `StorageBackendRefusal' if that the first line doesn't match
+ MAGIC (if provided). MAGIC should not contain the terminating
+ newline.
+
+ * Ignore comments (beginning `#') and blank lines.
+
+ * Call `_parse_line' (provided by the subclass) for other lines.
+ """
+ with open(file, 'r') as f:
+ if magic is not None:
+ if f.readline().rstrip('\n') != magic: raise StorageBackendRefusal
+ for line in f:
+ line = line.rstrip('\n')
+ if not line or line.startswith('#'): continue
+ me._parse_line(line)
+
+ def _write_file(me, file, writebody, mode = _M600, magic = None):
+ """
+ Update FILE atomically.
+
+ The newly created file will have the given MODE. If MAGIC is given, then
+ write that as the first line. Calls WRITEBODY(F) to write the main body
+ of the file where F is a file object for the new file.
+ """
+ new = file + '.new'
+ with me._create_file(new, mode) as f:
+ if magic is not None: f.write(magic + '\n')
+ writebody(f)
+ _OS.rename(new, file)
+
+ def _parse_meta(me, line):
+ """Parse LINE as a metadata NAME=VALUE pair, and updates `_meta'."""
+ k, v = me._eqsplit(line)
+ me._meta[_dec_metaname(k)] = _dec_metaval(v)
+
+ def _write_meta(me, f, prefix = ''):
+ """Write the metadata records to F, each with the given PREFIX."""
+ f.write('\n## Metadata.\n')
+ for k, v in _iteritems(me._meta):
+ f.write('%s%s=%s\n' % (prefix, _enc_metaname(k), _enc_metaval(v)))
+
+ def _get_meta(me, name, default):
+ return me._meta.get(name, default)
+ def _put_meta(me, name, value):
+ me._mark_dirty()
+ me._meta[name] = value
+ def _del_meta(me, name):
+ me._mark_dirty()
+ del me._meta[name]
+ def _iter_meta(me):
+ return _iteritems(me._meta)
+
+ def _parse_passwd(me, line):
+ """Parse LINE as a password LABEL=PAYLOAD pair, and updates `_pw'."""
+ k, v = me._eqsplit(line)
+ me._pw[_unb64(k)] = _unb64(v)
+
+ def _write_passwd(me, f, prefix = ''):
+ """Write the password records to F, each with the given PREFIX."""
+ f.write('\n## Password data.\n')
+ for k, v in _iteritems(me._pw):
+ f.write('%s%s=%s\n' % (prefix, _b64(k), _b64(v)))
+
+ def _get_passwd(me, label):
+ return me._pw[str(label)]
+ def _put_passwd(me, label, payload):
+ me._mark_dirty()
+ me._pw[str(label)] = payload
+ def _del_passwd(me, label):
+ me._mark_dirty()
+ del me._pw[str(label)]
+ def _iter_passwds(me):
+ return _iteritems(me._pw)
+
+class FlatFileStorageBackend (PlainTextBackend):
+ """
+ I maintain a password database in a plain text file.
+
+ The text file consists of lines, as follows.
+
+ * Empty lines, and lines beginning with `#' (in the leftmost column only)
+ are ignored.
+
+ * Lines of the form `$LABEL=PAYLOAD' store password data. Both LABEL and
+ PAYLOAD are base64-encoded, without `=' padding.
+
+ * Lines of the form `NAME=VALUE' store metadata. If the NAME contains
+ characters other than alphanumerics, hyphens, underscores, and colons,
+ then it is form-urlencoded, and prefixed wth `!'. If the VALUE
+ contains such characters, then it is base64-encoded, without `='
+ padding, and prefixed with `?'.
+
+ * Other lines are erroneous.
+
+ The file is rewritten from scratch when it's changed: any existing
+ commentary is lost, and items may be reordered. There is no file locking,
+ but the file is updated atomically, by renaming.
+
+ It is expected that the FlatFileStorageBackend is used mostly for
+ diagnostics and transfer, rather than for a live system.
+ """
+
+ NAME = 'flat'
+ PRIO = 0
+ MAGIC = '### pwsafe password database'
+
+ def _open(me, file, writep):
+ if not _OS.path.isfile(file): raise StorageBackendRefusal
+ me._parse_file(file, magic = me.MAGIC)
+ def _parse_line(me, line):
+ if line.startswith('$'): me._parse_passwd(line[1:])
+ else: me._parse_meta(line)
+
+ def _create(me, file):
+ with me._create_file(file, freshp = True) as f: pass
+ me._file = file
+ me._mark_dirty()
+
+ def _close(me, abruptp):
+ if not abruptp and me._dirtyp:
+ me._write_file(me._file, me._write_body, magic = me.MAGIC)
+
+ def _write_body(me, f):
+ me._write_meta(f)
+ me._write_passwd(f, '$')
+
+class DirectoryStorageBackend (PlainTextBackend):
+ """
+ I maintain a password database in a directory, with one file per password.
+
+ This makes password databases easy to maintain in a revision-control system
+ such as Git.
+
+ The directory is structured as follows.
+
+ dir/meta Contains metadata, similar to the `FlatFileBackend'.
+
+ dir/pw/LABEL Contains the (raw binary) payload for the given password
+ LABEL (base64-encoded, without the useless `=' padding, and
+ with `/' replaced by `.').
+
+ dir/tmp/ Contains temporary files used by the implementation.
+ """
+
+ NAME = 'dir'
+ METAMAGIC = '### pwsafe password directory metadata'
+
+ def _open(me, file, writep):
+ if not _OS.path.isdir(file) or \
+ not _OS.path.isdir(_OS.path.join(file, 'pw')) or \
+ not _OS.path.isdir(_OS.path.join(file, 'tmp')) or \
+ not _OS.path.isfile(_OS.path.join(file, 'meta')):
+ raise StorageBackendRefusal
+ me._dir = file
+ me._parse_file(_OS.path.join(file, 'meta'), magic = me.METAMAGIC)
+ def _parse_line(me, line):
+ me._parse_meta(line)
+
+ def _create(me, file):
+ _OS.mkdir(file, _M700)
+ _OS.mkdir(_OS.path.join(file, 'pw'), _M700)
+ _OS.mkdir(_OS.path.join(file, 'tmp'), _M700)
+ me._mark_dirty()
+ me._dir = file
+
+ def _close(me, abruptp):
+ if not abruptp and me._dirtyp:
+ me._write_file(_OS.path.join(me._dir, 'meta'),
+ me._write_meta, magic = me.METAMAGIC)
+
+ def _pwfile(me, label, dir = 'pw'):
+ return _OS.path.join(me._dir, dir, _b64(label).replace('/', '.'))
+ def _get_passwd(me, label):
+ try:
+ f = open(me._pwfile(label), 'rb')
+ except (OSError, IOError):
+ if _excval().errno == _E.ENOENT: raise KeyError(label)
+ else: raise
+ with f: return f.read()
+ def _put_passwd(me, label, payload):
+ new = me._pwfile(label, 'tmp')
+ fd = _OS.open(new, _OS.O_WRONLY | _OS.O_CREAT | _OS.O_TRUNC, _M600)
+ _OS.close(fd)
+ with open(new, 'wb') as f: f.write(payload)
+ _OS.rename(new, me._pwfile(label))
+ def _del_passwd(me, label):
+ try:
+ _OS.remove(me._pwfile(label))
+ except (OSError, IOError):
+ if _excval().errno == _E.ENOENT: raise KeyError(label)
+ else: raise
+ def _iter_passwds(me):
+ pw = _OS.path.join(me._dir, 'pw')
+ for i in _OS.listdir(pw):
+ with open(_OS.path.join(pw, i), 'rb') as f: pld = f.read()
+ yield _unb64(i.replace('.', '/')), pld
+
+###--------------------------------------------------------------------------
+### Password storage.