Skip to content

Commit ab59b61

Browse files
committed
Initial release
0 parents  commit ab59b61

File tree

6 files changed

+895
-0
lines changed

6 files changed

+895
-0
lines changed

LICENSE

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# qip-pass-export
2+
3+
Script to export passwords from pass / password-store / the standard unix password manager.
4+
The exported passwords can be imported into Passbolt.
5+
6+
Is is very likely they can also be imported into other password managers like Bitwarden, Vaultwarden and more, but I have not tested this myself.
7+
8+
## Background
9+
10+
I love [pass](https://www.passwordstore.org/) but it doesn't get much approval from the low-tech members of my family because of the complex software setup, hokey browser integration and needing to manually maintain GPG keys.
11+
12+
I decided to move all our passwords to a self-hosted [Passbolt Pro](https://www.passbolt.com/pricing/pro) instance.
13+
14+
To help with the migration, I made this simple `pass2csv` script in Python to export all my "pass" passwords to a CSV file that can be imported directly into Passbolt.
15+
16+
In particular the output format is supposedly Bitwarden's own format. See "Csv (BitWarden)" from [How to import passwords in passbolt](https://help.passbolt.com/faq/start/import-passwords)
17+
18+
> [!NOTE]
19+
> This script can only decrypt password files for which you are the intended recipient, e.g., you posess the corresponding GPG private keys.
20+
> This is NOT a brute-force decrypter, you must *own* the data.
21+
22+
## Installation
23+
24+
Minimum requirements:
25+
26+
- Python 3.x (tested with 3.11)
27+
28+
Clone the repository:
29+
30+
```sh
31+
git clone https://github.com/qualIP/qip-pass-export.git
32+
cd qip-pass-export
33+
```
34+
35+
Required Python modules are in [requirements.txt](requirements.txt) -- Really just `python-gnupg`.
36+
37+
You can install `python-gnupg` in your Linux distribution's standard packages or with `pip`.
38+
39+
On Debian and derivative distributions based on `apt`, this should work:
40+
41+
```sh
42+
sudo apt install python3-gnupg
43+
```
44+
45+
Otherwise, it is recommended to create a simple Python virtual environment, preferably under the qip-pass-export directory itself:
46+
47+
```sh
48+
python3 -m venv venv
49+
source venv/bin/activate
50+
pip install -r requirements.txt
51+
```
52+
53+
You may need to install your distributions Python venv module, e.g, the `python3-venv` package.
54+
55+
## Usage
56+
57+
### Exporting pass passwords
58+
59+
Usage is simple, just provide paths to pass `.gpg` files and it will output a CSV file on its standard output which you can pipe to a file of your choice.
60+
Typically, pass stores your password files under the `~/.password-store` directory.
61+
62+
Example: Export top-level passwords
63+
64+
```sh
65+
./pass2csv ~/.password-store/*.gpg > pass.csv
66+
```
67+
68+
Example: Export passwords from a sub-directory
69+
70+
```sh
71+
./pass2csv ~/.password-store/sub-directory/*.gpg > pass.csv
72+
```
73+
74+
Example: Export all passwords at once
75+
76+
```sh
77+
find ~/.password-store -name '*.gpg' -print0 | xargs -r0 ./pass2csv > pass.csv
78+
```
79+
80+
Please be patient, it takes a while to decrypt each individual file.
81+
82+
> [!CAUTION]
83+
> The exported CSV file contains **plain text**, **unencrypted passwords**.
84+
> Once you no longer needed the CSV file, delete it and empty your trash before someone else gets a hold of it.
85+
> Better yet, use a tool like [wipe](https://github.com/berke/wipe) to perform a secure erase.
86+
87+
### Import passwords into Passbolt
88+
89+
Next, go to your Passbolt Web interface, and click on the Import button:
90+
91+
![Screenshow of Passbolt's Import button](doc/Passbolt-import-button.png)
92+
93+
In the Import dialog, select your CSV file.
94+
If you wish to preserve the original directory structure, make sure to check the "Import folders" checkbox.
95+
96+
![Screenshow of Passbolt's Import dialog](doc/Passbolt-import-dialog.png)
97+
98+
Hit "Import", you're done.
99+
100+
> [!CAUTION]
101+
> Remember to securely erase the CSV file!
102+
103+
## Future work
104+
105+
Now that I'm done migrating my passwords to Passbolt, I don't foresee making any change of consequence to this software.
106+
107+
With proper incentives :moneybag::coffee:, I may be inclined to export various other formats -- Let me know.
108+
109+
If you're having issues using this software, please let me know by [opening an issue](https://github.com/qualIP/qip-pass-export/issues) in GitHub.

doc/Passbolt-import-button.png

7.55 KB
Loading

doc/Passbolt-import-dialog.png

47.3 KB
Loading

pass2csv

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env python
2+
'''
3+
pass2csv: Exports passwords from pass to CSV format
4+
5+
This software is distributed by qualIP Software under the GNU GENERAL PUBLIC
6+
LICENSE v3 in the hope that others may find it useful too.
7+
8+
The original source and a copy of the GPLv3 license can be found here:
9+
https://github.com/qualIP/qip-pass-export
10+
'''
11+
12+
# flake8: noqa: E501
13+
14+
from pathlib import Path
15+
import argparse
16+
import csv
17+
import io
18+
import os
19+
import re
20+
import sys
21+
import types
22+
23+
24+
def main():
25+
parser = argparse.ArgumentParser(
26+
prog='pass2csv',
27+
description='Exports passwords from pass to CSV format',
28+
29+
)
30+
parser.add_argument('-V', '--version', action='version', version='%(prog)s 1.0')
31+
parser.add_argument('passfiles', metavar="passfile", nargs='+', type=Path, help='Password file(s) (*.gpg) to export')
32+
args = parser.parse_args()
33+
34+
try:
35+
import gnupg
36+
except ImportError as err:
37+
print(err, file=sys.stderr)
38+
print('The python-gnupg module is required to run this software.', file=sys.stderr)
39+
sys.exit(1)
40+
gpg = gnupg.GPG()
41+
gpg.encoding = 'utf-8'
42+
43+
# https://help.passbolt.com/faq/start/import-passwords
44+
# Using Csv (BitWarden) format
45+
csv_fields = (
46+
'description', # YES
47+
'folder', # YES
48+
'favorite', # no
49+
'type', # no
50+
'name', # YES
51+
'notes', # YES
52+
'fields', # no
53+
'reprompt', # no
54+
'login_uri', # YES
55+
'login_username', # YES
56+
'login_password', # YES
57+
'login_totp', # no
58+
)
59+
csvwriter = csv.DictWriter(sys.stdout, fieldnames=csv_fields)
60+
csvwriter.writeheader()
61+
62+
for passfile in args.passfiles:
63+
relfile = passfile
64+
try:
65+
relfile = relfile.absolute().relative_to(Path.home() / '.password-store')
66+
except ValueError:
67+
try:
68+
relfile = relfile.absolute().relative_to(Path.home())
69+
except ValueError:
70+
pass
71+
if passfile.suffix == '.gpg':
72+
with open(passfile, 'rb') as fp:
73+
indata = gpg.decrypt_file(fp)
74+
assert indata.ok
75+
indata = str(indata)
76+
indata = io.StringIO(indata)
77+
csvrow = types.SimpleNamespace(**{k: '' for k in csv_fields})
78+
csvrow.notes = []
79+
for iline, line in enumerate(indata, start=1):
80+
line = line.rstrip('\r\n')
81+
if iline == 1:
82+
csvrow.login_password = line
83+
continue
84+
m = not csvrow.login_uri \
85+
and re.match(r'(?i)^(?:URL|URI)(?: *1)? *: *(?P<content>.*)$', line)
86+
if m:
87+
content = m.group('content').strip()
88+
if content:
89+
if not re.match(r'(?i)^[a-z]+://', content):
90+
content = 'https://' + content
91+
csvrow.login_uri = content
92+
continue
93+
m = not csvrow.login_username \
94+
and re.match(r'(?i)^(?:Login|Username) *: *(?P<content>.*)$', line)
95+
if m:
96+
content = m.group('content').strip()
97+
if content:
98+
csvrow.login_username = content
99+
continue
100+
csvrow.notes.append(line)
101+
csvrow.name = relfile.stem
102+
csvrow.folder = os.fspath(relfile.parent)
103+
csvrow.notes = '\n'.join(csvrow.notes)
104+
csvwriter.writerow(csvrow.__dict__)
105+
csvrow = None
106+
else:
107+
raise NotImplementedError(os.fspath(passfile))
108+
109+
110+
if __name__ == '__main__':
111+
main()

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python-gnupg

0 commit comments

Comments
 (0)