First version.
This commit is contained in:
commit
8feb06cc19
129
pass2bitwarden.py
Executable file
129
pass2bitwarden.py
Executable file
@ -0,0 +1,129 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime, UTC
|
||||||
|
import re
|
||||||
|
from tqdm import tqdm
|
||||||
|
import json
|
||||||
|
|
||||||
|
def run(*cmd, **kwargs):
|
||||||
|
return subprocess.run(cmd, capture_output=True, **kwargs).stdout
|
||||||
|
|
||||||
|
pass_dir = Path.home() / '.password-store'
|
||||||
|
files = pass_dir.rglob('*.gpg')
|
||||||
|
|
||||||
|
def decode_pass_file(contents: str, fn: str):
|
||||||
|
if contents.startswith('-----BEGIN PGP') or fn == 'www/gh-2fa-rec.gpg':
|
||||||
|
return {
|
||||||
|
'password': None,
|
||||||
|
'totp': None,
|
||||||
|
'username': None,
|
||||||
|
'url': None,
|
||||||
|
'notes': contents,
|
||||||
|
'is_pure_note': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = contents.splitlines()
|
||||||
|
password = lines[0] if len(lines) > 0 else ''
|
||||||
|
notes: list[str] = []
|
||||||
|
username = None
|
||||||
|
totp = None
|
||||||
|
url = None
|
||||||
|
fields: dict[str, str] = {}
|
||||||
|
for line in lines[1:]:
|
||||||
|
if line.startswith('otpauth:'):
|
||||||
|
if totp is None:
|
||||||
|
totp = line
|
||||||
|
continue
|
||||||
|
if m := re.match('^(user|username): (.*)$', line):
|
||||||
|
if username is None:
|
||||||
|
username = m[2]
|
||||||
|
continue
|
||||||
|
if m := re.match('^(url): (.*)$', line):
|
||||||
|
if url is None:
|
||||||
|
url = m[2]
|
||||||
|
continue
|
||||||
|
notes.append(line)
|
||||||
|
return {
|
||||||
|
'password': password,
|
||||||
|
'totp': totp,
|
||||||
|
'username': username,
|
||||||
|
'url': url,
|
||||||
|
'notes': '\n'.join(notes) if len(notes) > 0 else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def stripstart(s, prefix):
|
||||||
|
return s[len(prefix):] if s.startswith(prefix) else s
|
||||||
|
|
||||||
|
def stripend(s, suffix):
|
||||||
|
return s[:-len(suffix)] if s.endswith(suffix) else s
|
||||||
|
|
||||||
|
def dtfmt(d: datetime):
|
||||||
|
return d.strftime('%Y-%m-%dT%H:%M:%S.000Z')
|
||||||
|
|
||||||
|
def passwd(s):
|
||||||
|
# if s is not None: return '****'
|
||||||
|
return s
|
||||||
|
|
||||||
|
def pass2bitwarden(hist: list[tuple[str, datetime]], filename: str):
|
||||||
|
contents = decode_pass_file(hist[0][0], filename)
|
||||||
|
name = stripend(filename, '.gpg')
|
||||||
|
name = stripstart(name, 'www/')
|
||||||
|
url = contents['url']
|
||||||
|
if url is None and re.match(r'^[^/]+\.(com|coffee|org|at|nl|de|jp|st|com|net|uk)$', name):
|
||||||
|
url = f'https://{name}'
|
||||||
|
|
||||||
|
password_history = []
|
||||||
|
last_passwd = contents['password']
|
||||||
|
for c, t in hist[1:]:
|
||||||
|
p = decode_pass_file(c, filename)['password']
|
||||||
|
if p == '':
|
||||||
|
continue # bitwarden does not support empty passwords....
|
||||||
|
if p != last_passwd: # skip duplicate history entries
|
||||||
|
password_history.append({
|
||||||
|
'lastUsedDate': dtfmt(t),
|
||||||
|
'password': passwd(p),
|
||||||
|
})
|
||||||
|
last_passwd = p
|
||||||
|
|
||||||
|
if 'is_pure_note' in contents:
|
||||||
|
return {
|
||||||
|
'type': 2, # note
|
||||||
|
'creationDate': dtfmt(hist[-1][1]),
|
||||||
|
'revisionDate': dtfmt(hist[0][1]),
|
||||||
|
'name': name,
|
||||||
|
'notes': contents['notes'],
|
||||||
|
'secureNote': {'type': 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
notes = contents['notes']
|
||||||
|
notes = '' if notes is None else notes + '\n'
|
||||||
|
notes += f'last updated: {hist[0][1]}'
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 1, # password
|
||||||
|
'creationDate': dtfmt(hist[-1][1]),
|
||||||
|
'revisionDate': dtfmt(hist[0][1]),
|
||||||
|
'name': name,
|
||||||
|
'login': {
|
||||||
|
'uris': [ { 'match': None, 'uri': url } for url in [url] if url is not None ],
|
||||||
|
'username': contents['username'],
|
||||||
|
'password': passwd(contents['password']),
|
||||||
|
'totp': passwd(contents['totp']),
|
||||||
|
},
|
||||||
|
'notes': notes,
|
||||||
|
'passwordHistory': password_history,
|
||||||
|
}
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for file in tqdm(list(files)):
|
||||||
|
rfile = file.relative_to(pass_dir)
|
||||||
|
|
||||||
|
hist = run('git', 'log', '--pretty=%H %at', str(rfile), cwd=str(pass_dir)).decode('UTF-8').splitlines()
|
||||||
|
hist = [ (run('gpg', '--decrypt', '--quiet', input=run('git', 'show', f'{h}:{str(rfile)}', cwd=str(pass_dir))).decode('UTF-8'),
|
||||||
|
datetime.fromtimestamp(int(t), tz=UTC)) for h in hist for h, t in (h.split(' '),) ]
|
||||||
|
items.append(pass2bitwarden(hist, str(rfile)))
|
||||||
|
|
||||||
|
print(json.dumps({'items': items}))
|
||||||
|
|
Loading…
Reference in New Issue
Block a user