pass2bitwarden/pass2bitwarden.py

130 lines
3.9 KiB
Python
Raw Permalink Normal View History

2024-08-12 01:10:29 +02:00
#!/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}))