Initial commit
commit
381f7ac453
|
@ -0,0 +1,5 @@
|
|||
/.eggs/
|
||||
/.venv/
|
||||
*.egg-info/
|
||||
__pycache__/
|
||||
*.py[co]
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"python.pythonPath": ".venv/bin/python3",
|
||||
"files.watcherExclude": {
|
||||
"**/.venv/**": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name='pass2bw',
|
||||
use_scm_version=True,
|
||||
description='Import passwords from pass into Bitwarden using bw',
|
||||
author='Dustin C. Hatch',
|
||||
author_email='dustin@hatch.name',
|
||||
license='Apache-2',
|
||||
py_modules=['pass2bw'],
|
||||
package_dir={'': 'src'},
|
||||
setup_requires=['setuptools_scm'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'pass2bw=pass2bw:main',
|
||||
],
|
||||
},
|
||||
)
|
|
@ -0,0 +1,206 @@
|
|||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
)
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
log = logging.getLogger('pass2bw')
|
||||
|
||||
|
||||
class BitwardenError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Vault:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.folders: List[Dict[str, str]] = []
|
||||
self.items: List[Dict[str, Any]] = []
|
||||
|
||||
def create_folder(self, name) -> Dict[str, str]:
|
||||
log.info('Creating folder %s', name)
|
||||
data = {
|
||||
'name': name,
|
||||
}
|
||||
cmd = ['bw', 'create', 'folder']
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
res = p.communicate(
|
||||
base64.b64encode(json.dumps(data).encode())
|
||||
)[0]
|
||||
if p.returncode == 0:
|
||||
folder = json.loads(res)
|
||||
self.folders.append(folder)
|
||||
return folder
|
||||
else:
|
||||
raise BitwardenError(res.rstrip(b'\n').decode())
|
||||
|
||||
def create_item(self, data) -> Dict[str, Any]:
|
||||
cmd = ['bw', 'create', 'item']
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
res = p.communicate(
|
||||
base64.b64encode(json.dumps(data).encode())
|
||||
)[0]
|
||||
if p.returncode == 0:
|
||||
item = json.loads(res)
|
||||
self.items.append(item)
|
||||
return item
|
||||
else:
|
||||
raise BitwardenError(res.rstrip(b'\n').decode())
|
||||
|
||||
def get_folders(self) -> None:
|
||||
cmd = ['bw', 'list', 'folders']
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
data = p.communicate()[0]
|
||||
if p.returncode == 0:
|
||||
return json.loads(data)
|
||||
else:
|
||||
raise BitwardenError(data.rstrip(b'\n').decode())
|
||||
|
||||
def get_items(self) -> None:
|
||||
cmd = ['bw', 'list', 'items']
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
data = p.communicate()[0]
|
||||
if p.returncode == 0:
|
||||
return json.loads(data)
|
||||
else:
|
||||
raise BitwardenError(data.rstrip(b'\n').decode())
|
||||
|
||||
def load(self) -> None:
|
||||
self.folders = self.get_folders()
|
||||
self.items = self.get_items()
|
||||
|
||||
def sync(self) -> None:
|
||||
cmd = ['bw', 'sync']
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
res = p.communicate()[0]
|
||||
if p.returncode != 0:
|
||||
raise BitwardenError(res.rstrip(b'\n').decode())
|
||||
|
||||
|
||||
class PassEntry:
|
||||
|
||||
def __init__(self, key: str) -> None:
|
||||
self.key = key
|
||||
self.name = key.rsplit(os.sep, 1)[-1]
|
||||
self.password: str
|
||||
self.username: Optional[str] = None
|
||||
self.custom_fields: Dict[str, str] = {}
|
||||
|
||||
@classmethod
|
||||
def from_key(cls, key: str) -> 'PassEntry':
|
||||
self = cls(key)
|
||||
log.debug('Loading %s', key)
|
||||
cmd = ['pass', 'show', key]
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
try:
|
||||
self.password = p.stdout.readline().rstrip(b'\n').decode()
|
||||
for line in iter(p.stdout.readline, b''):
|
||||
if b':' in line:
|
||||
key, value = line.decode().split(':')
|
||||
if key == 'login':
|
||||
self.username = value
|
||||
else:
|
||||
self.custom_fields[key] = value
|
||||
finally:
|
||||
p.stdout.close()
|
||||
p.wait()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def list_all(cls) -> Iterator['PassEntry']:
|
||||
topdir = os.path.expanduser('~/.password-store')
|
||||
for basedir, dirnames, filenames in os.walk(topdir):
|
||||
for dirname in dirnames:
|
||||
if dirname.startswith('.'):
|
||||
log.debug('Ignoring %s', dirname)
|
||||
dirnames.remove(dirname)
|
||||
for filename in filenames:
|
||||
basename, ext = os.path.splitext(filename)
|
||||
if ext != '.gpg':
|
||||
log.debug('Skipping %s', filename)
|
||||
continue
|
||||
path = os.path.join(basedir, basename)
|
||||
key = os.path.relpath(path, topdir)
|
||||
yield cls.from_key(key)
|
||||
|
||||
def migrate(self, vault: Vault, folder: str = None) -> None:
|
||||
if folder:
|
||||
for f in vault.folders:
|
||||
if f['name'] == folder:
|
||||
folder_id = f['id']
|
||||
break
|
||||
else:
|
||||
f = vault.create_folder(folder)
|
||||
folder_id = f['id']
|
||||
else:
|
||||
folder_id = None
|
||||
log.info(
|
||||
'Migrating %s to Bitwarden (folder %s [%s])',
|
||||
self.key, folder, folder_id,
|
||||
)
|
||||
data = self.to_json()
|
||||
data['folderId'] = folder_id
|
||||
vault.create_item(data)
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
data = {
|
||||
'name': self.key,
|
||||
'type': 1,
|
||||
'login': {
|
||||
'uris': [],
|
||||
'password': self.password,
|
||||
}
|
||||
}
|
||||
if self.username:
|
||||
data['login']['username']= self.username
|
||||
return data
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
vault = Vault()
|
||||
vault.sync()
|
||||
vault.load()
|
||||
for entry in PassEntry.list_all():
|
||||
for item in vault.items:
|
||||
if item['name'] == entry.key:
|
||||
log.warning('Skiping %s', entry.key)
|
||||
break
|
||||
else:
|
||||
entry.migrate(vault, 'Imported')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Reference in New Issue