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