forked from tobbez/pass2csv
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpass2csv.py
More file actions
executable file
·178 lines (145 loc) · 5.9 KB
/
pass2csv.py
File metadata and controls
executable file
·178 lines (145 loc) · 5.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/usr/bin/env python3
import csv
import logging
import os
import re
from argparse import ArgumentParser
import gnupg
class CSVExporter:
def __init__(self, kpx_format):
logging.basicConfig(level=logging.INFO)
self.logger = logging.getLogger("pass2csv")
# Set to True to allow for alternate password csv to be created
# See README for differences
self.kpx_format = kpx_format
# A list of possible fields (in order) that could be converted to login fields
self.login_fields = ["login", "user", "username", "email"]
# Set to True to extract url fields
self.get_url = True
# A regular expression list of lines that should be excluded from the notes field
self.exclude_rows = ["^---$", "^autotype ?: ?"]
self.logger.info("Using KPX format: %s", self.kpx_format)
def traverse(self, path):
for root, dirs, files in os.walk(path):
if ".git" in dirs:
dirs.remove(".git")
for name in files:
yield os.path.join(root, name)
def getMetadata(self, notes_raw):
lines = notes_raw.split("\n")
# A list of lines to keep as notes (will be joined by newline)
notes = []
# The extracted user field
user = ""
# The extracted URL field
url = ""
# This will extract each field name (for example, if a line in notes was `user: user1`, fields should contain 'user')
all_fields = set()
for line in lines:
field_search = re.search("^(.*) ?: ?.*$", line, re.I)
if field_search:
all_fields.add(field_search.group(1))
# Check if any of the fields match the login names
login_fields = [field for field in self.login_fields if field in all_fields]
# Get the field to use for the login. Since self.login_fields is in order, the 0th element will contain the first match
login_field = None if not login_fields else login_fields[0]
# Iterate through the file again to build the return array
for line in lines:
# If any of the exclusion patterns match, ignore the line
if [
pattern
for pattern in self.exclude_rows
if re.search(pattern, line, re.I)
]:
continue
if login_field:
user_search = re.search("^" + login_field + " ?: ?(.*)$", line, re.I)
if user_search:
user = user_search.group(1)
# The user was matched, don't add it to notes
continue
if self.get_url:
url_search = re.search("^url ?: ?(.*)$", line, re.I)
if url_search:
url = url_search.group(1)
# The url was matched, don't add it to notes
continue
notes.append(line)
return user, url, "\n".join(notes).strip()
def parse(self, basepath, path, data):
name = os.path.splitext(os.path.basename(path))[0]
group = os.path.dirname(os.path.relpath(path, basepath))
split_data = data.split("\n", maxsplit=1)
password = (
split_data[0]
if split_data[0] != "GOPASS-SECRET-1.0"
else split_data[1].split(":")[1].strip()
)
# Perform if/else in case there are no notes for a field
notes = split_data[1] if len(split_data) > 1 else ""
self.logger.info("Processing %s", os.path.join(group, name))
# We are using the advanced format; try extracting user and url
user, url, notes = self.getMetadata(notes)
user = os.path.basename(name).replace(".gpg", "")
url = group
notes = ""
# Return in 1password-compatible format
if url == "" or group == "" or user == "" or password == "":
print("Failed to parse %s!" % path)
return None
return [group, url, user, password, notes]
def main(kpx_format, gpgbinary, use_agent, pass_path):
"""Main script entrypoint."""
exporter = CSVExporter(kpx_format)
gpg = gnupg.GPG(use_agent=use_agent, gpgbinary=gpgbinary)
gpg.encoding = "utf-8"
csv_data = []
for file_path in exporter.traverse(pass_path):
if os.path.splitext(file_path)[1] == ".gpg":
with open(file_path, "rb") as f:
data = str(gpg.decrypt_file(f))
if len(data) == 0:
print("Warning: Could not read password file %s" % file_path)
continue
export = exporter.parse(pass_path, file_path, data)
if export is not None:
csv_data.append(export)
with open("pass.csv", "w", newline="") as csv_file:
writer = csv.writer(csv_file, delimiter=",")
writer.writerows(csv_data)
class OptionsParser(ArgumentParser):
"""Regular ArgumentParser with the script's options."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.add_argument(
"pass_path",
metavar="path",
type=str,
help="Path to the PasswordStore folder to use",
)
self.add_argument(
"-a",
"--agent",
action="store_true",
help="Use this option to ask gpg to use it's auth agent",
dest="use_agent",
)
self.add_argument(
"-b",
"--gpgbinary",
type=str,
help="Path to the gpg binary you wish to use",
dest="gpgbinary",
default="gpg",
)
self.add_argument(
"-x",
"--kpx",
action="store_true",
help="Use this option to format the CSV for KeePassXC",
dest="kpx_format",
)
if __name__ == "__main__":
PARSER = OptionsParser()
ARGS = PARSER.parse_args()
main(**vars(ARGS))