├── .gitignore ├── .mailmap ├── LICENSE ├── README.md ├── dev-requirements.in ├── dev-requirements.txt ├── pass2csv.py ├── pyproject.toml ├── requirements.in ├── requirements.txt ├── setup.py └── tests ├── README.txt ├── alice.gpg ├── pass └── password-store ├── .gpg-id ├── latin1.gpg ├── sites └── example │ └── login.gpg └── unicode.gpg /.gitignore: -------------------------------------------------------------------------------- 1 | pass.csv 2 | 3 | __pycache__/ 4 | *.egg-info/ 5 | build/ 6 | dist/ 7 | venv/ 8 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Rupus Reinefjord 2 | Rupus Reinefjord 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 The pass2csv authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pass2csv - SOURCE HAS MOVED 2 | 3 | **The source for this project has moved to** [Codeberg](https://codeberg.org/svartstare/pass2csv)! Please report issues and make pull request over there. 4 | 5 | --- 6 | 7 | pass2csv is a tool that exports a 8 | [pass(1)](https://www.passwordstore.org/) password store to a CSV. It 9 | can export whole stores, subdirectories of a store, and single password 10 | files. It can use regexp patterns to find and group data after the first 11 | line of each password into additional CSV columns, as well as exclude 12 | data from the export. 13 | 14 | Source is available [at GitHub](https://github.com/reinefjord/pass2csv), 15 | and the package is published to the 16 | [Python Package Index](https://pypi.org/project/pass2csv/). 17 | 18 | You can install the package with [pipx](https://pypa.github.io/pipx/) 19 | (recommended): 20 | 21 | pipx install pass2csv 22 | 23 | or with pip: 24 | 25 | python3 -m pip install --user pass2csv 26 | 27 | 28 | ## Usage 29 | 30 | ``` 31 | $ pass2csv --help 32 | usage: pass2csv [-h] [-b path] [-g executable] [-a] 33 | [--encodings encodings] [-e pattern] [-f name pattern] 34 | [-l name pattern] [--version] 35 | STOREPATH OUTFILE 36 | 37 | positional arguments: 38 | STOREPATH path to the password-store to export 39 | OUTFILE file to write exported data to, use - for stdout 40 | 41 | options: 42 | -h, --help show this help message and exit 43 | -b path, --base path path to use as base for grouping passwords 44 | -g executable, --gpg executable 45 | path to the gpg binary you wish to use (default: 46 | 'gpg') 47 | -a, --use-agent ask gpg to use its auth agent 48 | --encodings encodings 49 | comma-separated text encodings to try, in order, 50 | when decoding gpg output (default: 'utf-8') 51 | -e pattern, --exclude pattern 52 | regexp for lines which should not be exported, can 53 | be specified multiple times 54 | -f name pattern, --get-field name pattern 55 | a name and a regexp, the part of the line matching 56 | the regexp will be removed and the remaining line 57 | will be added to a field with the chosen name. 58 | only one match per password, matching stops after 59 | the first match 60 | -l name pattern, --get-line name pattern 61 | a name and a regexp for which all lines that match 62 | are included in a field with the chosen name 63 | --version show program's version number and exit 64 | ``` 65 | 66 | 67 | ### Format 68 | 69 | The output format is 70 | 71 | Group(/),Title,Password,[custom fields...],Notes 72 | 73 | You may add custom fields with `--get-field` or `--get-line`. You supply 74 | a *name* for the field and a regexp *pattern*. The field name is used for 75 | the header of the output CSV and to group multiple patterns for the same 76 | field; you may specify multiple patterns for the same field by 77 | specifying `--get-field` or`--get-line` multiple times with the same 78 | name. Regexp patterns are case-insensitive. 79 | 80 | 81 | ### Example usage 82 | 83 | ``` 84 | $ gpg -d ~/.password-store/sites/example/login.gpg 85 | password123 86 | --- 87 | username: user_name 88 | email user@example.com 89 | url:example.com 90 | Some note 91 | 92 | $ pass2csv ~/.password-store - \ 93 | --exclude '^---$' \ 94 | --get-field Username '(username|email):?' \ 95 | --get-field URL 'url:?' 96 | 97 | "Group(/)","Title","Password","URL","Username","Notes" 98 | "sites/example","login","password123","example.com","user_name","email user@example.com 99 | 100 | Some note" 101 | ``` 102 | 103 | 104 | ### Grouping 105 | 106 | The group is relative to the path, or the `--base` if given. 107 | Given the password `~/.password-store/sites/example/login.gpg`: 108 | 109 | $ pass2csv ~/.password-store/sites 110 | # Password will have group "example" 111 | 112 | $ pass2csv ~/.password-store/sites --base ~/.password-store 113 | # Password will have group "sites/example" 114 | 115 | 116 | ## gpg-agent password timeout 117 | 118 | If your private key is protected by a password, `gpg` will ask for it 119 | with the `pinentry` program if you haven't set it to something else. If 120 | using `gpg2` or the `-a` option with `gpg`, by default, the password is 121 | cached for 10 minutes but the timer is reset when using a key. After 2 122 | hours the cache will be cleared even if it has been accessed recently. 123 | 124 | You can set these values in your `~/.gnupg/gpg-agent.conf`: 125 | 126 | ``` 127 | default-cache-ttl 600 128 | max-cache-ttl 7200 129 | ``` 130 | 131 | 132 | ## Development 133 | 134 | Create a virtual environment: 135 | 136 | python3 -m venv venv 137 | 138 | Activate the environment: 139 | 140 | . venv/bin/activate 141 | 142 | Now you may either use `pip` directly to install the dependencies, or 143 | you can install `pip-tools`. The latter is recommended. 144 | 145 | 146 | ### pip 147 | 148 | pip install -r requirements.txt 149 | 150 | 151 | ### pip-tools 152 | 153 | [pip-tools](https://github.com/jazzband/pip-tools) can keep your virtual 154 | environment in sync with the `requirements.txt` file, as well as 155 | compiling a new `requirements.txt` when adding/removing a dependency in 156 | `requirements.in`. 157 | 158 | It is recommended that pip-tools is installed within the virtual 159 | environment. 160 | 161 | pip install pip-tools 162 | pip-compile # only necessary when adding/removing a dependency 163 | pip-sync 164 | 165 | 166 | ### Packaging 167 | 168 | See [Python Packaging User Guide](https://packaging.python.org/tutorials/packaging-projects/) for detailed info. 169 | 170 | 0. `pip-sync requirements.txt dev-requirements.txt` 171 | 1. Increment `__version__` in `pass2csv.py`. 172 | 2. `rm -rf dist/* && python3 -m build` 173 | 4. `python3 -m twine upload dist/*` 174 | -------------------------------------------------------------------------------- /dev-requirements.in: -------------------------------------------------------------------------------- 1 | -c requirements.txt 2 | build 3 | twine 4 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with python 3.11 3 | # To update, run: 4 | # 5 | # pip-compile dev-requirements.in 6 | # 7 | bleach==5.0.1 8 | # via readme-renderer 9 | build==0.9.0 10 | # via -r dev-requirements.in 11 | certifi==2023.7.22 12 | # via requests 13 | cffi==1.15.1 14 | # via cryptography 15 | charset-normalizer==2.1.1 16 | # via requests 17 | commonmark==0.9.1 18 | # via rich 19 | cryptography==41.0.2 20 | # via secretstorage 21 | docutils==0.19 22 | # via readme-renderer 23 | idna==3.4 24 | # via requests 25 | importlib-metadata==5.0.0 26 | # via 27 | # keyring 28 | # twine 29 | jaraco-classes==3.2.3 30 | # via keyring 31 | jeepney==0.8.0 32 | # via 33 | # keyring 34 | # secretstorage 35 | keyring==23.11.0 36 | # via twine 37 | more-itertools==9.0.0 38 | # via jaraco-classes 39 | packaging==21.3 40 | # via build 41 | pep517==0.13.0 42 | # via build 43 | pkginfo==1.8.3 44 | # via twine 45 | pycparser==2.21 46 | # via cffi 47 | pygments==2.15.0 48 | # via 49 | # readme-renderer 50 | # rich 51 | pyparsing==3.0.9 52 | # via packaging 53 | readme-renderer==37.3 54 | # via twine 55 | requests==2.31.0 56 | # via 57 | # requests-toolbelt 58 | # twine 59 | requests-toolbelt==0.10.1 60 | # via twine 61 | rfc3986==2.0.0 62 | # via twine 63 | rich==12.6.0 64 | # via twine 65 | secretstorage==3.3.3 66 | # via keyring 67 | six==1.16.0 68 | # via bleach 69 | twine==4.0.1 70 | # via -r dev-requirements.in 71 | urllib3==1.26.12 72 | # via 73 | # requests 74 | # twine 75 | webencodings==0.5.1 76 | # via bleach 77 | zipp==3.10.0 78 | # via importlib-metadata 79 | -------------------------------------------------------------------------------- /pass2csv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import csv 4 | import pathlib 5 | import re 6 | import sys 7 | 8 | import gnupg 9 | 10 | __version__ = '1.1.1' 11 | 12 | 13 | def stderr(s, *args, **kwargs): 14 | print(s, *args, file=sys.stderr, **kwargs) 15 | 16 | 17 | def set_meta(entry, path, grouping_base): 18 | pure_path = pathlib.PurePath(path) 19 | group = pure_path.relative_to(grouping_base).parent 20 | if group.name == '': 21 | group = '' 22 | entry['group'] = group 23 | entry['title'] = pure_path.stem 24 | 25 | 26 | def set_data(entry, data, exclude, get_fields, get_lines): 27 | lines = data.splitlines() 28 | tail = lines[1:] 29 | entry['password'] = lines[0] 30 | 31 | filtered_tail = [] 32 | for line in tail: 33 | for exclude_pattern in exclude: 34 | if exclude_pattern.search(line): 35 | break 36 | else: 37 | filtered_tail.append(line) 38 | 39 | matching_indices = set() 40 | fields = entry.setdefault('fields', {}) 41 | 42 | for i, line in enumerate(filtered_tail): 43 | for name, pattern in get_fields: 44 | if name in fields: 45 | # multiple patterns with same name, we've already found a match 46 | continue 47 | match = pattern.search(line) 48 | if not match: 49 | continue 50 | inverse_match = line[0:match.start()] + line[match.end():] 51 | value = inverse_match.strip() 52 | fields[name] = value 53 | matching_indices.add(i) 54 | break 55 | 56 | matching_lines = {} 57 | for i, line in enumerate(filtered_tail): 58 | for name, pattern in get_lines: 59 | match = pattern.search(line) 60 | if not match: 61 | continue 62 | matches = matching_lines.setdefault(name, []) 63 | matches.append(line) 64 | matching_indices.add(i) 65 | break 66 | for name, matches in matching_lines.items(): 67 | fields[name] = '\n'.join(matches) 68 | 69 | final_tail = [] 70 | for i, line in enumerate(filtered_tail): 71 | if i not in matching_indices: 72 | final_tail.append(line) 73 | 74 | entry['notes'] = '\n'.join(final_tail).strip() 75 | 76 | 77 | def write(file, entries, get_fields, get_lines): 78 | get_field_names = set(x[0] for x in get_fields) 79 | get_line_names = set(x[0] for x in get_lines) 80 | field_names = get_field_names | get_line_names 81 | header = ["Group(/)", "Title", "Password", *field_names, "Notes"] 82 | csvw = csv.writer(file, dialect='unix') 83 | stderr(f"\nWriting data to {file.name}\n") 84 | csvw.writerow(header) 85 | for entry in entries: 86 | fields = [entry['fields'].get(name) for name in field_names] 87 | columns = [ 88 | entry['group'], entry['title'], entry['password'], 89 | *fields, 90 | entry['notes'] 91 | ] 92 | csvw.writerow(columns) 93 | 94 | 95 | def main(store_path, outfile, grouping_base, gpgbinary, use_agent, encodings, 96 | exclude, get_fields, get_lines): 97 | entries = [] 98 | failures = [] 99 | path = pathlib.Path(store_path) 100 | grouping_path = pathlib.Path(grouping_base) 101 | gpg = gnupg.GPG(gpgbinary=gpgbinary, use_agent=use_agent) 102 | files = path.glob('**/*.gpg') 103 | if not path.is_dir(): 104 | if path.is_file(): 105 | files = [path] 106 | else: 107 | stderr(f"No such file or directory: {path}") 108 | sys.exit(1) 109 | for file in files: 110 | stderr(f"Processing {file}") 111 | with open(file, 'rb') as fp: 112 | decrypted = gpg.decrypt_file(fp) 113 | if not decrypted.ok: 114 | err = f"Could not decrypt {file}: {decrypted.status}" 115 | stderr(err) 116 | failures.append(err) 117 | continue 118 | for i, encoding in enumerate(encodings): 119 | try: 120 | # decrypted.data is bytes 121 | decrypted_data = decrypted.data.decode(encoding) 122 | except Exception as e: 123 | stderr(f"Could not decode {file} with encoding {encoding}: {e}") 124 | continue 125 | if i > 0: 126 | # don't log if the first encoding worked 127 | stderr(f"Decoded {file} with encoding {encoding}") 128 | break 129 | else: 130 | err = "Could not decode {}, see messages above for more info.".format(file) 131 | failures.append(err) 132 | continue 133 | entry = {} 134 | set_meta(entry, file, grouping_path) 135 | set_data(entry, decrypted_data, exclude, get_fields, get_lines) 136 | entries.append(entry) 137 | if failures: 138 | stderr("\nGot errors while processing files:") 139 | for err in failures: 140 | stderr(err) 141 | if not entries: 142 | stderr("\nNothing to write.") 143 | sys.exit(1) 144 | write(outfile, entries, get_fields, get_lines) 145 | 146 | 147 | def parse_args(args=None): 148 | parser = argparse.ArgumentParser() 149 | parser.add_argument( 150 | 'store_path', 151 | metavar='STOREPATH', 152 | type=str, 153 | help="path to the password-store to export", 154 | ) 155 | 156 | parser.add_argument( 157 | 'outfile', 158 | metavar='OUTFILE', 159 | type=argparse.FileType('w'), 160 | help="file to write exported data to, use - for stdout", 161 | ) 162 | 163 | parser.add_argument( 164 | '-b', '--base', 165 | metavar='path', 166 | type=str, 167 | help="path to use as base for grouping passwords", 168 | dest='base_path' 169 | ) 170 | 171 | parser.add_argument( 172 | '-g', '--gpg', 173 | metavar='executable', 174 | type=str, 175 | default="gpg", 176 | help="path to the gpg binary you wish to use (default: '%(default)s')", 177 | dest='gpgbinary' 178 | ) 179 | 180 | parser.add_argument( 181 | '-a', '--use-agent', 182 | action='store_true', 183 | default=False, 184 | help="ask gpg to use its auth agent", 185 | dest='use_agent' 186 | ) 187 | 188 | parser.add_argument( 189 | '--encodings', 190 | metavar='encodings', 191 | type=str, 192 | default="utf-8", 193 | help=( 194 | "comma-separated text encodings to try, in order, when decoding" 195 | " gpg output (default: '%(default)s')" 196 | ), 197 | dest='encodings' 198 | ) 199 | 200 | parser.add_argument( 201 | '-e', '--exclude', 202 | metavar='pattern', 203 | action='append', 204 | type=str, 205 | default=[], 206 | help=( 207 | "regexp for lines which should not be exported, can be specified" 208 | " multiple times" 209 | ), 210 | dest='exclude' 211 | ) 212 | 213 | parser.add_argument( 214 | '-f', '--get-field', 215 | metavar=('name', 'pattern'), 216 | action='append', 217 | nargs=2, 218 | type=str, 219 | default=[], 220 | help=( 221 | "a name and a regexp, the part of the line matching the regexp" 222 | " will be removed and the remaining line will be added to a field" 223 | " with the chosen name. only one match per password, matching" 224 | " stops after the first match" 225 | ), 226 | dest='get_fields' 227 | ) 228 | 229 | parser.add_argument( 230 | '-l', '--get-line', 231 | metavar=('name', 'pattern'), 232 | action='append', 233 | nargs=2, 234 | type=str, 235 | default=[], 236 | help=( 237 | "a name and a regexp for which all lines that match are included" 238 | " in a field with the chosen name" 239 | ), 240 | dest='get_lines' 241 | ) 242 | 243 | parser.add_argument( 244 | '--version', 245 | action='version', 246 | version='%(prog)s ' + __version__ 247 | ) 248 | 249 | return parser.parse_args(args) 250 | 251 | 252 | def compile_regexp(pattern): 253 | try: 254 | regexp = re.compile(pattern, re.I) 255 | except re.error as e: 256 | escaped = pattern.replace("'", "\\'") 257 | stderr(f"Could not compile pattern '{escaped}', {e.msg} at position {e.pos}") 258 | return None 259 | return regexp 260 | 261 | 262 | def cli(): 263 | parsed = parse_args() 264 | 265 | failed = False 266 | exclude_patterns = [] 267 | for pattern in parsed.exclude: 268 | regexp = compile_regexp(pattern) 269 | if not regexp: 270 | failed = True 271 | exclude_patterns.append(regexp) 272 | 273 | get_fields = [] 274 | for name, pattern in parsed.get_fields: 275 | regexp = compile_regexp(pattern) 276 | if not regexp: 277 | failed = True 278 | get_fields.append((name, regexp)) 279 | 280 | get_lines = [] 281 | for name, pattern in parsed.get_lines: 282 | regexp = compile_regexp(pattern) 283 | if not regexp: 284 | failed = True 285 | get_lines.append((name, regexp)) 286 | 287 | if failed: 288 | sys.exit(1) 289 | 290 | if parsed.base_path: 291 | grouping_base = parsed.base_path 292 | else: 293 | grouping_base = parsed.store_path 294 | 295 | encodings = [e for e in parsed.encodings.split(',') if e] 296 | if not encodings: 297 | stderr(f"Did not understand '--encodings {parsed.encoding}'") 298 | sys.exit(1) 299 | 300 | kwargs = { 301 | 'store_path': parsed.store_path, 302 | 'outfile': parsed.outfile, 303 | 'grouping_base': grouping_base, 304 | 'gpgbinary': parsed.gpgbinary, 305 | 'use_agent': parsed.use_agent, 306 | 'encodings': encodings, 307 | 'exclude': exclude_patterns, 308 | 'get_fields': get_fields, 309 | 'get_lines': get_lines 310 | } 311 | 312 | main(**kwargs) 313 | 314 | if __name__ == '__main__': 315 | cli() 316 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | requires = ["setuptools"] 4 | 5 | [project] 6 | classifiers = [ 7 | "Development Status :: 5 - Production/Stable", 8 | "Environment :: Console", 9 | "Intended Audience :: End Users/Desktop", 10 | "License :: OSI Approved :: MIT License", 11 | "Programming Language :: Python", 12 | "Programming Language :: Python :: 3", 13 | "Programming Language :: Python :: 3.6", 14 | "Topic :: Utilities" 15 | ] 16 | dependencies = ["python-gnupg"] 17 | description = 'Export pass(1), "the standard unix password manager", to CSV' 18 | dynamic = ["version"] 19 | keywords = ["csv", "export", "pass2csv"] 20 | maintainers = [ 21 | {name = "Rupus Reinefjord", email = "rupus@reinefjord.net"} 22 | ] 23 | name = "pass2csv" 24 | readme = "README.md" 25 | requires-python = ">=3.6" 26 | 27 | [project.scripts] 28 | pass2csv = "pass2csv:cli" 29 | 30 | [project.urls] 31 | Changelog = "https://github.com/reinefjord/pass2csv/releases" 32 | Homepage = "https://github.com/reinefjord/pass2csv" 33 | 34 | [tool.setuptools.dynamic] 35 | version = {attr = "pass2csv.__version__"} 36 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | python-gnupg 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with python 3.6 3 | # To update, run: 4 | # 5 | # pip-compile 6 | # 7 | python-gnupg==0.5.2 8 | # via -r requirements.in 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | if __name__ == "__main__": 4 | setup() 5 | -------------------------------------------------------------------------------- /tests/README.txt: -------------------------------------------------------------------------------- 1 | alice.gpg contains the private key which has been used to encrypt the 2 | passwords in password-store/ 3 | 4 | The passphrase for the key is `foobar`. 5 | -------------------------------------------------------------------------------- /tests/alice.gpg: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PRIVATE KEY BLOCK----- 2 | 3 | lQIGBGEBmioBBADN644Nv7b45cuHk4ctcE4Yxo9x3AT1tEpC6fCcOFK7PQ/M2T7Y 4 | tiZdKFnAz/9SR6ns0ERbYz+Fb0AcbVQUT9j+mAmQ0QJVQVnoj2BzX0yK0MFWGg2f 5 | uR/NMP/Pzgx1kYQdV0ecIKi5ulGBLb4GtzxQGmogfSbh58FJ5cTqv2GXPQARAQAB 6 | /gcDAu81AiOuzNxk/d1Rrp8f9/3Gqt5VSaeioFel16rdYcbO4ddgxjdqGAePF5US 7 | hXIppexspsDYZo/zJyt3gLMPZF9mBYGe3DW9UkXQOaXoQWSsGc2vDswSf3rDIl+e 8 | awKn8+fjv7uvrGalT/It1uQLPpBk+abnzyAI8GrtQwt1zAt9/7/dZKxql2WHP3rc 9 | 5kGMGt+q0oEor0rgKZlzRwtZKkfQarVYSDj1H5yKvjPqYJPbhYicsYottNvATDc+ 10 | u81Mumb4D7jfV2YOsBL5yRetuVBrlLZB0Ukv/GmKDj+5W24N6QkgjI48ZvAtkQiY 11 | 7rHgXlDuJZQsGto/Mu3WsV4yx7rxx1eJIMzZjLnw3IDwihgU3r9ezwcNzBRE/9pY 12 | QR7rliriSITIDqihUv/6cAsAspi54q62b559xPSO4ZDEd5+baLb499hRV0Khc0M6 13 | GuW9HqVQUZ812qL+jHX+mnWDeeZIzCrqs1+9cctZC2ghUA+7mbAX0r20UkFsaWNl 14 | IChUZXN0IGtleSBmb3IgaHR0cHM6Ly9naXRodWIuY29tL3JlaW5lZmpvcmQvcGFz 15 | czJjc3YpIDxhbGljZUBkb21haW4uaW52YWxpZD6IzgQTAQgAOBYhBBoVsDwjmvRl 16 | P7M4T+vwrtD/nTl9BQJhAZoqAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJ 17 | EOvwrtD/nTl9DkwD/imBeCqwM9mQq/CgYMHW2kAAk+/04MZ9BUup5TDsxHxBQDrK 18 | WvOEH3MCu2c8qrEzCtOWFIRk+96Gw98g9CuZTmI7sIBmXk+amceSFE+w80PoRlKJ 19 | hKyekocX233MfmgFMUibKNXBY2hdDWjTArTWKwE0rqNRLCQfFpcaBbQ3cd8knQIG 20 | BGEBmioBBADj8o7yHQ+S7gucGIBB+e97XEX3coI7Nj2pAS6do5NrNKXflOIcCE6/ 21 | VskzX3SWWzNUfzkejyyQQ52wFzoGN6fmEM6xxd3lXHG+WxsLH/d//d4kAL4MhR2L 22 | Jg/25vIds1IelVsI2FzY48elIH1aaXLX7gM62uirX4TjPwu2QdgJTQARAQAB/gcD 23 | AvG5a3QT/qsn/UUg1HyLBanTU9m0ljYh7MZnkarpFZVlNN2pM8H8iBExb4JgAeBn 24 | aoGiQZJbbt7Bc6uqgpqOlX13CnidfXG24y7v9jZz6Aq/YQX5nfi/X3ifd7e2OOWm 25 | 7jdf4Hrdm9Wwgo9H4STQdLu7i9cw3UvpCyvNXCIBNh1PT2Xhketgbh1lQKmdmpaD 26 | Miu4qqFPJWO029WH1AIxJJfFRPqxhaAaomsOp6Rk9tLFcShuv6ctgwViEJQ99GMe 27 | lganKHidLj3ih+Kd4rvAcelVew14a2fogt71SQducyXnzwj+Emkmu074bbQNUyw+ 28 | 7atJPZ4hs6IOz7mT24eeTBdOKC9nTTTUw+W9H1UFKjhHmL0usqo2uE84zmtoLN2o 29 | wE9dO3hHEck/FsN81VOq39Xa+tZ8I4AqQRTsjkicW0v4LIF8Ok0M52491uvE5zv/ 30 | Uc03Z6SuyBQj/UsftiCj3tewF//sDlvKKrmpfxnXXEPI9NefZy2ItgQYAQgAIBYh 31 | BBoVsDwjmvRlP7M4T+vwrtD/nTl9BQJhAZoqAhsMAAoJEOvwrtD/nTl9Z/sD/Rqi 32 | WQ3XMzb8BXh+CWsvVENaTgEvsQusKOerHODLzILP9e1jwnnnqPiwY09GqQz/8gB2 33 | 6mL3/ydhgqZcgzmakK6vXo7tne/RgQrtXJ7DcpAVLrMvMHOZ+af2/jPjMaKL9sAO 34 | C4xr2NCDuliR860/R0domSOh8PWx1EKvQpOFKNVd 35 | =6Tlz 36 | -----END PGP PRIVATE KEY BLOCK----- 37 | -------------------------------------------------------------------------------- /tests/pass: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PASSWORD_STORE_DIR=./password-store pass "${@}" 4 | -------------------------------------------------------------------------------- /tests/password-store/.gpg-id: -------------------------------------------------------------------------------- 1 | 1A15B03C239AF4653FB3384FEBF0AED0FF9D397D 2 | -------------------------------------------------------------------------------- /tests/password-store/latin1.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reinefjord/pass2csv/e35ac09c7f0d552bbaec0dea77360d5562ada6ed/tests/password-store/latin1.gpg -------------------------------------------------------------------------------- /tests/password-store/sites/example/login.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reinefjord/pass2csv/e35ac09c7f0d552bbaec0dea77360d5562ada6ed/tests/password-store/sites/example/login.gpg -------------------------------------------------------------------------------- /tests/password-store/unicode.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reinefjord/pass2csv/e35ac09c7f0d552bbaec0dea77360d5562ada6ed/tests/password-store/unicode.gpg --------------------------------------------------------------------------------