├── .gitignore ├── COPYING ├── INSTALL ├── Makefile ├── README ├── contrib ├── dmenu │ ├── README.md │ └── passmenu ├── emacs │ ├── .gitignore │ ├── CHANGELOG.md │ ├── Cask │ ├── README.md │ └── password-store.el ├── importers │ ├── 1password2pass.rb │ ├── fpm2pass.pl │ ├── gorilla2pass.rb │ ├── kedpm2pass.py │ ├── keepass2csv2pass.py │ ├── keepass2pass.py │ ├── keepassx2pass.py │ ├── kwallet2pass.py │ ├── lastpass2pass.rb │ ├── password-exporter2pass.py │ ├── pwsafe2pass.py │ ├── pwsafe2pass.sh │ ├── revelation2pass.py │ └── roboform2pass.rb ├── pass.applescript └── vim │ ├── redact_pass.txt │ └── redact_pass.vim ├── man ├── example-filter.sh └── pass.1 ├── src ├── completion │ ├── pass.bash-completion │ ├── pass.fish-completion │ └── pass.zsh-completion ├── password-store.sh └── platform │ ├── cygwin.sh │ ├── darwin.sh │ ├── freebsd.sh │ └── openbsd.sh └── tests ├── .gitignore ├── TODO.txt ├── fake-editor-change-password.sh ├── gnupg ├── .gpg-v21-migrated ├── gpg.conf ├── private-keys-v1.d │ ├── 0606FE40527B8F47BFD30238709F895642EEF303.key │ ├── 06278846A35FE4416E8701DDCF6B60E93F8BCB63.key │ ├── 615FC2A5B2CBFD58B7FFA0A140D43B74AB9748B0.key │ ├── 63D607EC5C89163B473708E7B3E5115301CF06E4.key │ ├── A5CEE9554AA7090ADD97D97E0DA902764E6C2111.key │ ├── AD20D0B45D263DD5AE866FDB98E04A0D20070F68.key │ ├── C93858C40FA9E117DA4E7F336580B8B12354EB83.key │ ├── C93F70CA322D4F42E7FC7D54F6367E65C23E5CA3.key │ ├── CDA6EE91E62A15AB9F6A3041FE01CC123B7E9D23.key │ └── FFED3C5A6A52B200BCCE3F41593EA51D6054649F.key ├── pubring.gpg ├── secring.gpg └── trustdb.gpg ├── setup.sh ├── sharness.sh ├── t0001-sanity-checks.sh ├── t0010-generate-tests.sh ├── t0020-show-tests.sh ├── t0050-mv-tests.sh ├── t0060-rm-tests.sh ├── t0100-insert-tests.sh ├── t0200-edit-tests.sh ├── t0300-reencryption.sh ├── t0400-grep.sh └── t0500-find.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Password Store is Copyright (C) 2012-2016 Jason A. Donenfeld . All Rights Reserved. 2 | 3 | This program is free software; you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation; either version 2 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | 14 | 15 | GNU GENERAL PUBLIC LICENSE 16 | Version 2, June 1991 17 | 18 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 19 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 20 | Everyone is permitted to copy and distribute verbatim copies 21 | of this license document, but changing it is not allowed. 22 | 23 | Preamble 24 | 25 | The licenses for most software are designed to take away your 26 | freedom to share and change it. By contrast, the GNU General Public 27 | License is intended to guarantee your freedom to share and change free 28 | software--to make sure the software is free for all its users. This 29 | General Public License applies to most of the Free Software 30 | Foundation's software and to any other program whose authors commit to 31 | using it. (Some other Free Software Foundation software is covered by 32 | the GNU Lesser General Public License instead.) You can apply it to 33 | your programs, too. 34 | 35 | When we speak of free software, we are referring to freedom, not 36 | price. Our General Public Licenses are designed to make sure that you 37 | have the freedom to distribute copies of free software (and charge for 38 | this service if you wish), that you receive source code or can get it 39 | if you want it, that you can change the software or use pieces of it 40 | in new free programs; and that you know you can do these things. 41 | 42 | To protect your rights, we need to make restrictions that forbid 43 | anyone to deny you these rights or to ask you to surrender the rights. 44 | These restrictions translate to certain responsibilities for you if you 45 | distribute copies of the software, or if you modify it. 46 | 47 | For example, if you distribute copies of such a program, whether 48 | gratis or for a fee, you must give the recipients all the rights that 49 | you have. You must make sure that they, too, receive or can get the 50 | source code. And you must show them these terms so they know their 51 | rights. 52 | 53 | We protect your rights with two steps: (1) copyright the software, and 54 | (2) offer you this license which gives you legal permission to copy, 55 | distribute and/or modify the software. 56 | 57 | Also, for each author's protection and ours, we want to make certain 58 | that everyone understands that there is no warranty for this free 59 | software. If the software is modified by someone else and passed on, we 60 | want its recipients to know that what they have is not the original, so 61 | that any problems introduced by others will not reflect on the original 62 | authors' reputations. 63 | 64 | Finally, any free program is threatened constantly by software 65 | patents. We wish to avoid the danger that redistributors of a free 66 | program will individually obtain patent licenses, in effect making the 67 | program proprietary. To prevent this, we have made it clear that any 68 | patent must be licensed for everyone's free use or not licensed at all. 69 | 70 | The precise terms and conditions for copying, distribution and 71 | modification follow. 72 | 73 | GNU GENERAL PUBLIC LICENSE 74 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 75 | 76 | 0. This License applies to any program or other work which contains 77 | a notice placed by the copyright holder saying it may be distributed 78 | under the terms of this General Public License. The "Program", below, 79 | refers to any such program or work, and a "work based on the Program" 80 | means either the Program or any derivative work under copyright law: 81 | that is to say, a work containing the Program or a portion of it, 82 | either verbatim or with modifications and/or translated into another 83 | language. (Hereinafter, translation is included without limitation in 84 | the term "modification".) Each licensee is addressed as "you". 85 | 86 | Activities other than copying, distribution and modification are not 87 | covered by this License; they are outside its scope. The act of 88 | running the Program is not restricted, and the output from the Program 89 | is covered only if its contents constitute a work based on the 90 | Program (independent of having been made by running the Program). 91 | Whether that is true depends on what the Program does. 92 | 93 | 1. You may copy and distribute verbatim copies of the Program's 94 | source code as you receive it, in any medium, provided that you 95 | conspicuously and appropriately publish on each copy an appropriate 96 | copyright notice and disclaimer of warranty; keep intact all the 97 | notices that refer to this License and to the absence of any warranty; 98 | and give any other recipients of the Program a copy of this License 99 | along with the Program. 100 | 101 | You may charge a fee for the physical act of transferring a copy, and 102 | you may at your option offer warranty protection in exchange for a fee. 103 | 104 | 2. You may modify your copy or copies of the Program or any portion 105 | of it, thus forming a work based on the Program, and copy and 106 | distribute such modifications or work under the terms of Section 1 107 | above, provided that you also meet all of these conditions: 108 | 109 | a) You must cause the modified files to carry prominent notices 110 | stating that you changed the files and the date of any change. 111 | 112 | b) You must cause any work that you distribute or publish, that in 113 | whole or in part contains or is derived from the Program or any 114 | part thereof, to be licensed as a whole at no charge to all third 115 | parties under the terms of this License. 116 | 117 | c) If the modified program normally reads commands interactively 118 | when run, you must cause it, when started running for such 119 | interactive use in the most ordinary way, to print or display an 120 | announcement including an appropriate copyright notice and a 121 | notice that there is no warranty (or else, saying that you provide 122 | a warranty) and that users may redistribute the program under 123 | these conditions, and telling the user how to view a copy of this 124 | License. (Exception: if the Program itself is interactive but 125 | does not normally print such an announcement, your work based on 126 | the Program is not required to print an announcement.) 127 | 128 | These requirements apply to the modified work as a whole. If 129 | identifiable sections of that work are not derived from the Program, 130 | and can be reasonably considered independent and separate works in 131 | themselves, then this License, and its terms, do not apply to those 132 | sections when you distribute them as separate works. But when you 133 | distribute the same sections as part of a whole which is a work based 134 | on the Program, the distribution of the whole must be on the terms of 135 | this License, whose permissions for other licensees extend to the 136 | entire whole, and thus to each and every part regardless of who wrote it. 137 | 138 | Thus, it is not the intent of this section to claim rights or contest 139 | your rights to work written entirely by you; rather, the intent is to 140 | exercise the right to control the distribution of derivative or 141 | collective works based on the Program. 142 | 143 | In addition, mere aggregation of another work not based on the Program 144 | with the Program (or with a work based on the Program) on a volume of 145 | a storage or distribution medium does not bring the other work under 146 | the scope of this License. 147 | 148 | 3. You may copy and distribute the Program (or a work based on it, 149 | under Section 2) in object code or executable form under the terms of 150 | Sections 1 and 2 above provided that you also do one of the following: 151 | 152 | a) Accompany it with the complete corresponding machine-readable 153 | source code, which must be distributed under the terms of Sections 154 | 1 and 2 above on a medium customarily used for software interchange; or, 155 | 156 | b) Accompany it with a written offer, valid for at least three 157 | years, to give any third party, for a charge no more than your 158 | cost of physically performing source distribution, a complete 159 | machine-readable copy of the corresponding source code, to be 160 | distributed under the terms of Sections 1 and 2 above on a medium 161 | customarily used for software interchange; or, 162 | 163 | c) Accompany it with the information you received as to the offer 164 | to distribute corresponding source code. (This alternative is 165 | allowed only for noncommercial distribution and only if you 166 | received the program in object code or executable form with such 167 | an offer, in accord with Subsection b above.) 168 | 169 | The source code for a work means the preferred form of the work for 170 | making modifications to it. For an executable work, complete source 171 | code means all the source code for all modules it contains, plus any 172 | associated interface definition files, plus the scripts used to 173 | control compilation and installation of the executable. However, as a 174 | special exception, the source code distributed need not include 175 | anything that is normally distributed (in either source or binary 176 | form) with the major components (compiler, kernel, and so on) of the 177 | operating system on which the executable runs, unless that component 178 | itself accompanies the executable. 179 | 180 | If distribution of executable or object code is made by offering 181 | access to copy from a designated place, then offering equivalent 182 | access to copy the source code from the same place counts as 183 | distribution of the source code, even though third parties are not 184 | compelled to copy the source along with the object code. 185 | 186 | 4. You may not copy, modify, sublicense, or distribute the Program 187 | except as expressly provided under this License. Any attempt 188 | otherwise to copy, modify, sublicense or distribute the Program is 189 | void, and will automatically terminate your rights under this License. 190 | However, parties who have received copies, or rights, from you under 191 | this License will not have their licenses terminated so long as such 192 | parties remain in full compliance. 193 | 194 | 5. You are not required to accept this License, since you have not 195 | signed it. However, nothing else grants you permission to modify or 196 | distribute the Program or its derivative works. These actions are 197 | prohibited by law if you do not accept this License. Therefore, by 198 | modifying or distributing the Program (or any work based on the 199 | Program), you indicate your acceptance of this License to do so, and 200 | all its terms and conditions for copying, distributing or modifying 201 | the Program or works based on it. 202 | 203 | 6. Each time you redistribute the Program (or any work based on the 204 | Program), the recipient automatically receives a license from the 205 | original licensor to copy, distribute or modify the Program subject to 206 | these terms and conditions. You may not impose any further 207 | restrictions on the recipients' exercise of the rights granted herein. 208 | You are not responsible for enforcing compliance by third parties to 209 | this License. 210 | 211 | 7. If, as a consequence of a court judgment or allegation of patent 212 | infringement or for any other reason (not limited to patent issues), 213 | conditions are imposed on you (whether by court order, agreement or 214 | otherwise) that contradict the conditions of this License, they do not 215 | excuse you from the conditions of this License. If you cannot 216 | distribute so as to satisfy simultaneously your obligations under this 217 | License and any other pertinent obligations, then as a consequence you 218 | may not distribute the Program at all. For example, if a patent 219 | license would not permit royalty-free redistribution of the Program by 220 | all those who receive copies directly or indirectly through you, then 221 | the only way you could satisfy both it and this License would be to 222 | refrain entirely from distribution of the Program. 223 | 224 | If any portion of this section is held invalid or unenforceable under 225 | any particular circumstance, the balance of the section is intended to 226 | apply and the section as a whole is intended to apply in other 227 | circumstances. 228 | 229 | It is not the purpose of this section to induce you to infringe any 230 | patents or other property right claims or to contest validity of any 231 | such claims; this section has the sole purpose of protecting the 232 | integrity of the free software distribution system, which is 233 | implemented by public license practices. Many people have made 234 | generous contributions to the wide range of software distributed 235 | through that system in reliance on consistent application of that 236 | system; it is up to the author/donor to decide if he or she is willing 237 | to distribute software through any other system and a licensee cannot 238 | impose that choice. 239 | 240 | This section is intended to make thoroughly clear what is believed to 241 | be a consequence of the rest of this License. 242 | 243 | 8. If the distribution and/or use of the Program is restricted in 244 | certain countries either by patents or by copyrighted interfaces, the 245 | original copyright holder who places the Program under this License 246 | may add an explicit geographical distribution limitation excluding 247 | those countries, so that distribution is permitted only in or among 248 | countries not thus excluded. In such case, this License incorporates 249 | the limitation as if written in the body of this License. 250 | 251 | 9. The Free Software Foundation may publish revised and/or new versions 252 | of the General Public License from time to time. Such new versions will 253 | be similar in spirit to the present version, but may differ in detail to 254 | address new problems or concerns. 255 | 256 | Each version is given a distinguishing version number. If the Program 257 | specifies a version number of this License which applies to it and "any 258 | later version", you have the option of following the terms and conditions 259 | either of that version or of any later version published by the Free 260 | Software Foundation. If the Program does not specify a version number of 261 | this License, you may choose any version ever published by the Free Software 262 | Foundation. 263 | 264 | 10. If you wish to incorporate parts of the Program into other free 265 | programs whose distribution conditions are different, write to the author 266 | to ask for permission. For software which is copyrighted by the Free 267 | Software Foundation, write to the Free Software Foundation; we sometimes 268 | make exceptions for this. Our decision will be guided by the two goals 269 | of preserving the free status of all derivatives of our free software and 270 | of promoting the sharing and reuse of software generally. 271 | 272 | NO WARRANTY 273 | 274 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 275 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 276 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 277 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 278 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 279 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 280 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 281 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 282 | REPAIR OR CORRECTION. 283 | 284 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 285 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 286 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 287 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 288 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 289 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 290 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 291 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 292 | POSSIBILITY OF SUCH DAMAGES. 293 | 294 | END OF TERMS AND CONDITIONS 295 | 296 | How to Apply These Terms to Your New Programs 297 | 298 | If you develop a new program, and you want it to be of the greatest 299 | possible use to the public, the best way to achieve this is to make it 300 | free software which everyone can redistribute and change under these terms. 301 | 302 | To do so, attach the following notices to the program. It is safest 303 | to attach them to the start of each source file to most effectively 304 | convey the exclusion of warranty; and each file should have at least 305 | the "copyright" line and a pointer to where the full notice is found. 306 | 307 | 308 | Copyright (C) 309 | 310 | This program is free software; you can redistribute it and/or modify 311 | it under the terms of the GNU General Public License as published by 312 | the Free Software Foundation; either version 2 of the License, or 313 | (at your option) any later version. 314 | 315 | This program is distributed in the hope that it will be useful, 316 | but WITHOUT ANY WARRANTY; without even the implied warranty of 317 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 318 | GNU General Public License for more details. 319 | 320 | You should have received a copy of the GNU General Public License along 321 | with this program; if not, write to the Free Software Foundation, Inc., 322 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 323 | 324 | Also add information on how to contact you by electronic and paper mail. 325 | 326 | If the program is interactive, make it output a short notice like this 327 | when it starts in an interactive mode: 328 | 329 | Gnomovision version 69, Copyright (C) year name of author 330 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 331 | This is free software, and you are welcome to redistribute it 332 | under certain conditions; type `show c' for details. 333 | 334 | The hypothetical commands `show w' and `show c' should show the appropriate 335 | parts of the General Public License. Of course, the commands you use may 336 | be called something other than `show w' and `show c'; they could even be 337 | mouse-clicks or menu items--whatever suits your program. 338 | 339 | You should also get your employer (if you work as a programmer) or your 340 | school, if any, to sign a "copyright disclaimer" for the program, if 341 | necessary. Here is a sample; alter the names: 342 | 343 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 344 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 345 | 346 | , 1 April 1989 347 | Ty Coon, President of Vice 348 | 349 | This General Public License does not permit incorporating your program into 350 | proprietary programs. If your program is a subroutine library, you may 351 | consider it more useful to permit linking proprietary applications with the 352 | library. If this is what you want to do, use the GNU Lesser General 353 | Public License instead of this License. 354 | 355 | 356 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | Simply typing 2 | 3 | make install 4 | 5 | should install pass to the standard locations. 6 | 7 | The makefile is aware of the following environment variables: 8 | 9 | PREFIX default: /usr 10 | DESTDIR default: 11 | BINDIR default: $(PREFIX)/bin 12 | LIBDIR default: $(PREFIX)/lib 13 | MANDIR default: $(PREFIX)/share/man 14 | SYSCONFDIR default: /etc 15 | 16 | -- Completion Files -- 17 | 18 | The install target will automatically determine the existance 19 | of bash, zsh, and fish, and install the completion files as 20 | needed. If you'd like to choose manually, you may set WITH_ALLCOMP, 21 | WITH_BASHCOMP, WITH_ZSHCOMP, or WITH_FISHCOMP to "yes" or "no". The 22 | exact paths of the completions can be controlled with BASHCOMPDIR, 23 | ZSHCOMPDIR, and FISHCOMPDIR. 24 | 25 | -- Test Suite -- 26 | 27 | Pass has a test suite which uses Sharness: 28 | 29 | 30 | To run all tests, run 'make test'. 31 | 32 | To debug an individual test, run it directly with '-v', e.g.: 33 | $ tests/t0001-sanity-checks.sh -v 34 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= /usr 2 | DESTDIR ?= 3 | BINDIR ?= $(PREFIX)/bin 4 | LIBDIR ?= $(PREFIX)/lib 5 | MANDIR ?= $(PREFIX)/share/man 6 | 7 | PLATFORMFILE := src/platform/$(shell uname | cut -d _ -f 1 | tr '[:upper:]' '[:lower:]').sh 8 | 9 | BASHCOMPDIR ?= $(PREFIX)/share/bash-completion/completions 10 | ZSHCOMPDIR ?= $(PREFIX)/share/zsh/site-functions 11 | FISHCOMPDIR ?= $(PREFIX)/share/fish/vendor_completions.d 12 | 13 | ifneq ($(WITH_ALLCOMP),) 14 | WITH_BASHCOMP := $(WITH_ALLCOMP) 15 | WITH_ZSHCOMP := $(WITH_ALLCOMP) 16 | WITH_FISHCOMP := $(WITH_ALLCOMP) 17 | endif 18 | ifeq ($(WITH_BASHCOMP),) 19 | ifneq ($(strip $(wildcard $(BASHCOMPDIR))),) 20 | WITH_BASHCOMP := yes 21 | endif 22 | endif 23 | ifeq ($(WITH_ZSHCOMP),) 24 | ifneq ($(strip $(wildcard $(ZSHCOMPDIR))),) 25 | WITH_ZSHCOMP := yes 26 | endif 27 | endif 28 | ifeq ($(WITH_FISHCOMP),) 29 | ifneq ($(strip $(wildcard $(FISHCOMPDIR))),) 30 | WITH_FISHCOMP := yes 31 | endif 32 | endif 33 | 34 | all: 35 | @echo "Password store is a shell script, so there is nothing to do. Try \"make install\" instead." 36 | 37 | install-common: 38 | @install -v -d "$(DESTDIR)$(MANDIR)/man1" && install -m 0644 -v man/pass.1 "$(DESTDIR)$(MANDIR)/man1/pass.1" 39 | @[ "$(WITH_BASHCOMP)" = "yes" ] || exit 0; install -v -d "$(DESTDIR)$(BASHCOMPDIR)" && install -m 0644 -v src/completion/pass.bash-completion "$(DESTDIR)$(BASHCOMPDIR)/pass" 40 | @[ "$(WITH_ZSHCOMP)" = "yes" ] || exit 0; install -v -d "$(DESTDIR)$(ZSHCOMPDIR)" && install -m 0644 -v src/completion/pass.zsh-completion "$(DESTDIR)$(ZSHCOMPDIR)/_pass" 41 | @[ "$(WITH_FISHCOMP)" = "yes" ] || exit 0; install -v -d "$(DESTDIR)$(FISHCOMPDIR)" && install -m 0644 -v src/completion/pass.fish-completion "$(DESTDIR)$(FISHCOMPDIR)/pass.fish" 42 | 43 | 44 | ifneq ($(strip $(wildcard $(PLATFORMFILE))),) 45 | install: install-common 46 | @install -v -d "$(DESTDIR)$(LIBDIR)/password-store" && install -m 0644 -v "$(PLATFORMFILE)" "$(DESTDIR)$(LIBDIR)/password-store/platform.sh" 47 | @install -v -d "$(DESTDIR)$(LIBDIR)/password-store/extensions" 48 | @install -v -d "$(DESTDIR)$(BINDIR)/" 49 | @trap 'rm -f src/.pass' EXIT; sed 's:.*PLATFORM_FUNCTION_FILE.*:source "$(LIBDIR)/password-store/platform.sh":;s:^SYSTEM_EXTENSION_DIR=.*:SYSTEM_EXTENSION_DIR="$(LIBDIR)/password-store/extensions":' src/password-store.sh > src/.pass && \ 50 | install -v -d "$(DESTDIR)$(BINDIR)/" && install -m 0755 -v src/.pass "$(DESTDIR)$(BINDIR)/pass" 51 | else 52 | install: install-common 53 | @install -v -d "$(DESTDIR)$(LIBDIR)/password-store/extensions" 54 | @trap 'rm -f src/.pass' EXIT; sed '/PLATFORM_FUNCTION_FILE/d;s:^SYSTEM_EXTENSION_DIR=.*:SYSTEM_EXTENSION_DIR="$(LIBDIR)/password-store/extensions":' src/password-store.sh > src/.pass && \ 55 | install -v -d "$(DESTDIR)$(BINDIR)/" && install -m 0755 -v src/.pass "$(DESTDIR)$(BINDIR)/pass" 56 | endif 57 | 58 | uninstall: 59 | @rm -vrf \ 60 | "$(DESTDIR)$(BINDIR)/pass" \ 61 | "$(DESTDIR)$(LIBDIR)/password-store" \ 62 | "$(DESTDIR)$(MANDIR)/man1/pass.1" \ 63 | "$(DESTDIR)$(BASHCOMPDIR)/pass" \ 64 | "$(DESTDIR)$(ZSHCOMPDIR)/_pass" \ 65 | "$(DESTDIR)$(FISHCOMPDIR)/pass.fish" 66 | 67 | TESTS = $(sort $(wildcard tests/t[0-9][0-9][0-9][0-9]-*.sh)) 68 | 69 | test: $(TESTS) 70 | 71 | $(TESTS): 72 | @$@ $(PASS_TEST_OPTS) 73 | 74 | clean: 75 | $(RM) -rf tests/test-results/ tests/trash\ directory.*/ tests/gnupg/random_seed 76 | 77 | .PHONY: install uninstall install-common test clean $(TESTS) 78 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ====================== 2 | Simple Password Store 3 | by Jason Donenfeld 4 | Jason@zx2c4.com 5 | ====================== 6 | 7 | This is a very simple password store that encrypts passwords using gpg and 8 | places the encrypted password in a directory. It can generate new passwords 9 | and keep track of old ones. 10 | 11 | Visit the project page for more information: http://www.passwordstore.org/ 12 | 13 | Please see the man page for documentation and examples. 14 | 15 | Depends on: 16 | - bash 17 | http://www.gnu.org/software/bash/ 18 | - GnuPG2 19 | http://www.gnupg.org/ 20 | - git 21 | http://www.git-scm.com/ 22 | - xclip (for X11 environments) 23 | http://sourceforge.net/projects/xclip/ 24 | - wl-clipboard (for wlroots Wayland-based environments) 25 | https://github.com/bugaevc/wl-clipboard 26 | - tree >= 1.7.0 27 | http://mama.indstate.edu/users/ice/tree/ 28 | - GNU getopt 29 | http://www.kernel.org/pub/linux/utils/util-linux/ 30 | http://software.frodo.looijaard.name/getopt/ 31 | - qrencode 32 | https://fukuchi.org/works/qrencode/ 33 | -------------------------------------------------------------------------------- /contrib/dmenu/README.md: -------------------------------------------------------------------------------- 1 | `passmenu` is a [dmenu][]-based interface to [pass][], the standard Unix 2 | password manager. This design allows you to quickly copy a password to the 3 | clipboard without having to open up a terminal window if you don't already have 4 | one open. If `--type` is specified, the password is typed using [xdotool][] 5 | instead of copied to the clipboard. 6 | 7 | On wayland [dmenu-wl][] is used to replace dmenu and [ydotool][] to replace xdotool. 8 | Note that the latter requires access to the [uinput][] device, so you'll probably 9 | need to add an extra udev rule or similar to give certain non-root users permission. 10 | 11 | # Usage 12 | 13 | passmenu [--type] [dmenu arguments...] 14 | 15 | [dmenu]: http://tools.suckless.org/dmenu/ 16 | [xdotool]: http://www.semicomplete.com/projects/xdotool/ 17 | [pass]: http://www.zx2c4.com/projects/password-store/ 18 | [dmenu-wl]: https://github.com/nyyManni/dmenu-wayland 19 | [ydotool]: https://github.com/ReimuNotMoe/ydotool 20 | [uinput]: https://www.kernel.org/doc/html/v4.12/input/uinput.html 21 | -------------------------------------------------------------------------------- /contrib/dmenu/passmenu: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | shopt -s nullglob globstar 4 | 5 | typeit=0 6 | if [[ $1 == "--type" ]]; then 7 | typeit=1 8 | shift 9 | fi 10 | 11 | if [[ -n $WAYLAND_DISPLAY ]]; then 12 | dmenu=dmenu-wl 13 | xdotool="ydotool type --file -" 14 | elif [[ -n $DISPLAY ]]; then 15 | dmenu=dmenu 16 | xdotool="xdotool type --clearmodifiers --file -" 17 | else 18 | echo "Error: No Wayland or X11 display detected" >&2 19 | exit 1 20 | fi 21 | 22 | prefix=${PASSWORD_STORE_DIR-~/.password-store} 23 | password_files=( "$prefix"/**/*.gpg ) 24 | password_files=( "${password_files[@]#"$prefix"/}" ) 25 | password_files=( "${password_files[@]%.gpg}" ) 26 | 27 | password=$(printf '%s\n' "${password_files[@]}" | "$dmenu" "$@") 28 | 29 | [[ -n $password ]] || exit 30 | 31 | if [[ $typeit -eq 0 ]]; then 32 | pass show -c "$password" 2>/dev/null 33 | else 34 | pass show "$password" | { IFS= read -r pass; printf %s "$pass"; } | $xdotool 35 | fi 36 | -------------------------------------------------------------------------------- /contrib/emacs/.gitignore: -------------------------------------------------------------------------------- 1 | .cask 2 | -------------------------------------------------------------------------------- /contrib/emacs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.3.3 2 | 3 | * (cleanup) Avoid double decryption and reduce the scope of 4 | `inhibit-message` in internal functions. 5 | 6 | # 2.3.2 7 | 8 | * (bugfix) Ensure the system clipboard is cleared after 9 | the timeout expired. 10 | 11 | # 2.3.1 12 | 13 | * (bug) Drop dependency on s library. 14 | 15 | # 2.3.0 16 | 17 | * (bug) Drop auth-source-pass dependency. 18 | Bump Emacs minor version requirement to emacs 26. 19 | 20 | # 2.2.0 21 | 22 | * (feature) Add command password-store-generate-no-symbols 23 | 24 | # 2.1.5 25 | 26 | * (bugfix) Fix an infloop on Windows enviroments. 27 | 28 | # 2.1.4 29 | 30 | * Drop dependency on f library. 31 | 32 | # 2.1.3 33 | 34 | * Update password-store-clear docstring; clarify that the 35 | optional argument is only used in the print out message. 36 | 37 | # 2.1.2 38 | 39 | * Make argument optional in password-store-clear to preserve 40 | backward compatibility. 41 | 42 | # 2.1.1 43 | 44 | * (bugfix) Check that auth-source-pass-filename is bound before use it. 45 | 46 | # 2.1.0 47 | 48 | * (feature) Support extraction of any secret fields stored in the files. 49 | 50 | * (feature) The library is now integrated with auth-source-pass; thus, the 51 | filename of the password-store folder is set with the option 52 | auth-source-pass-filename. 53 | 54 | # 2.0.5 55 | 56 | Improve password-store-insert message on success/failure 57 | 58 | # 2.0.4 59 | 60 | * Re add password-store-timeout function to preserve backward 61 | compatibility with other libraries relying on it. 62 | 63 | # 2.0.3 64 | 65 | * (feature) Update password-store-password-length default value to 25 66 | 67 | * (feature) Add option password-store-time-before-clipboard-restore; delete 68 | password-store-timeout and use the new option instead. 69 | 70 | # 1.0.2 71 | 72 | * (bugfix) Fix typo in password-store-url function doc string 73 | 74 | # 1.0.1 75 | 76 | * (bugfix) Quote shell arguments in async call 77 | 78 | # 1.0.0 79 | 80 | * (feature) Call `pass edit` so that changes get committed to git 81 | 82 | # 0.1 83 | 84 | * Initial release 85 | -------------------------------------------------------------------------------- /contrib/emacs/Cask: -------------------------------------------------------------------------------- 1 | (source gnu) 2 | (source melpa) 3 | 4 | (package-file "password-store.el") 5 | 6 | (development 7 | (depends-on "with-editor") 8 | (depends-on "ecukes") 9 | (depends-on "ert-runner") 10 | (depends-on "el-mock")) 11 | -------------------------------------------------------------------------------- /contrib/emacs/README.md: -------------------------------------------------------------------------------- 1 | # Emacs password-store 2 | 3 | This package provides functions for working with pass ("the standard 4 | Unix password manager"). 5 | 6 | http://www.zx2c4.com/projects/password-store 7 | 8 | ## Setup 9 | 10 | The pass application must be installed and set up. See the pass 11 | website for instructions 12 | 13 | ## Example usage 14 | 15 | Interactive: 16 | 17 | M-x password-store-insert 18 | Password entry: foo-account 19 | Password: ........ 20 | Confirm password: ........ 21 | 22 | ;; Generate a random password. 23 | M-x password-store-generate 24 | Password entry: bar-account 25 | 26 | ;; Generate a random password without symbols. 27 | M-x password-store-generate-no-symbols 28 | Password entry: qux-account 29 | 30 | M-x password-store-copy 31 | Password entry: foo-account 32 | Copied password for foo-account to the kill ring. Will clear in 45 seconds. 33 | Field password cleared. 34 | 35 | M-x password-store-copy-field 36 | Password entry: foo-account 37 | Field: username 38 | Copied username for foo-account to the kill ring. Will clear in 45 seconds. 39 | Field url cleared. 40 | 41 | 42 | Lisp: 43 | 44 | (password-store-insert "foo-account" "password") 45 | (password-store-get "foo-account") ; Returns "password" 46 | (password-store-get-field "foo-account" "url") ; Returns "url" 47 | -------------------------------------------------------------------------------- /contrib/emacs/password-store.el: -------------------------------------------------------------------------------- 1 | ;;; password-store.el --- Password store (pass) support -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2014-2019 Svend Sorensen 4 | 5 | ;; Author: Svend Sorensen 6 | ;; Maintainer: Tino Calancha 7 | ;; Version: 2.3.3 8 | ;; URL: https://www.passwordstore.org/ 9 | ;; Package-Requires: ((emacs "26.1") (with-editor "2.5.11")) 10 | ;; SPDX-License-Identifier: GPL-3.0-or-later 11 | ;; Keywords: tools pass password password-store gpg 12 | 13 | ;; This file is not part of GNU Emacs. 14 | 15 | ;; This program is free software: you can redistribute it and/or 16 | ;; modify it under the terms of the GNU General Public License as 17 | ;; published by the Free Software Foundation, either version 3 of 18 | ;; the License, or (at your option) any later version. 19 | 20 | ;; This program is distributed in the hope that it will be 21 | ;; useful, but WITHOUT ANY WARRANTY; without even the implied 22 | ;; warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 23 | ;; PURPOSE. See the GNU General Public License for more details. 24 | 25 | ;; You should have received a copy of the GNU General Public 26 | ;; License along with this program. If not, see 27 | ;; . 28 | 29 | ;;; Commentary: 30 | 31 | ;; This package provides and Emacs interface for working with 32 | ;; pass ("the standard Unix password manager"). 33 | 34 | ;; https://www.passwordstore.org/ 35 | 36 | ;;; Code: 37 | 38 | (require 'with-editor) 39 | (require 'auth-source-pass) 40 | 41 | (defgroup password-store '() 42 | "Emacs mode for password-store. 43 | The standard Unix password manager" 44 | :prefix "password-store-" 45 | :group 'password-store 46 | :link '(url-link :tag "Description" "https://www.passwordstore.org/") 47 | :link '(url-link :tag "Download" "https://melpa.org/#/password-store") 48 | :link `(url-link :tag "Send Bug Report" 49 | ,(concat "mailto:" "password-store" "@" "lists.zx2c4" ".com?subject= 50 | password-store.el bug: \ 51 | &body=Describe bug here, starting with `emacs -q'. \ 52 | Don't forget to mention your Emacs and library versions."))) 53 | 54 | (defcustom password-store-password-length 25 55 | "Default password length." 56 | :group 'password-store 57 | :type 'number) 58 | 59 | (defcustom password-store-time-before-clipboard-restore 60 | (if (getenv "PASSWORD_STORE_CLIP_TIME") 61 | (string-to-number (getenv "PASSWORD_STORE_CLIP_TIME")) 62 | 45) 63 | "Number of seconds to wait before restoring the clipboard." 64 | :group 'password-store 65 | :type 'number) 66 | 67 | (defcustom password-store-url-field "url" 68 | "Field name used in the files to indicate a URL." 69 | :group 'password-store 70 | :type 'string) 71 | 72 | (defvar password-store-executable 73 | (executable-find "pass") 74 | "Pass executable.") 75 | 76 | (defvar password-store-timeout-timer nil 77 | "Timer for clearing clipboard.") 78 | 79 | (defun password-store-timeout () 80 | "Number of seconds to wait before restoring the clipboard. 81 | 82 | This function just returns 83 | `password-store-time-before-clipboard-restore'. Kept for 84 | backward compatibility with other libraries." 85 | password-store-time-before-clipboard-restore) 86 | 87 | (make-obsolete 'password-store-timeout 'password-store-time-before-clipboard-restore "2.0.4") 88 | 89 | (defun password-store--run-1 (callback &rest args) 90 | "Run pass with ARGS. 91 | 92 | Nil arguments are ignored. Calls CALLBACK with the output on 93 | success, or outputs error message on failure." 94 | (let ((output "")) 95 | (make-process 96 | :name "password-store-gpg" 97 | :command (cons password-store-executable (delq nil args)) 98 | :connection-type 'pipe 99 | :noquery t 100 | :filter (lambda (process text) 101 | (setq output (concat output text))) 102 | :sentinel (lambda (process state) 103 | (cond 104 | ((and (eq (process-status process) 'exit) 105 | (zerop (process-exit-status process))) 106 | (funcall callback output)) 107 | ((eq (process-status process) 'run) (accept-process-output process)) 108 | (t (error (concat "password-store: " state)))))))) 109 | 110 | (defun password-store--run (&rest args) 111 | "Run pass with ARGS. 112 | 113 | Nil arguments are ignored. Returns the output on success, or 114 | outputs error message on failure." 115 | (let ((output nil) 116 | (slept-for 0)) 117 | (apply #'password-store--run-1 (lambda (password) 118 | (setq output password)) 119 | (delq nil args)) 120 | (while (not output) 121 | (sleep-for .1)) 122 | output)) 123 | 124 | (defun password-store--run-async (&rest args) 125 | "Run pass asynchronously with ARGS. 126 | 127 | Nil arguments are ignored. Output is discarded." 128 | (let ((args (mapcar #'shell-quote-argument args))) 129 | (with-editor-async-shell-command 130 | (mapconcat 'identity 131 | (cons password-store-executable 132 | (delq nil args)) " ")))) 133 | 134 | (defun password-store--run-init (gpg-ids &optional subdir) 135 | (apply 'password-store--run "init" 136 | (if subdir (format "--path=%s" subdir)) 137 | gpg-ids)) 138 | 139 | (defun password-store--run-list (&optional subdir) 140 | (error "Not implemented")) 141 | 142 | (defun password-store--run-grep (&optional string) 143 | (error "Not implemented")) 144 | 145 | (defun password-store--run-find (&optional string) 146 | (error "Not implemented")) 147 | 148 | (defun password-store--run-show (entry &optional callback) 149 | (if callback 150 | (password-store--run-1 callback "show" entry) 151 | (password-store--run "show" entry))) 152 | 153 | (defun password-store--run-insert (entry password &optional force) 154 | (error "Not implemented")) 155 | 156 | (defun password-store--run-edit (entry) 157 | (password-store--run-async "edit" 158 | entry)) 159 | 160 | (defun password-store--run-generate (entry password-length &optional force no-symbols) 161 | (password-store--run "generate" 162 | (if force "--force") 163 | (if no-symbols "--no-symbols") 164 | entry 165 | (number-to-string password-length))) 166 | 167 | (defun password-store--run-remove (entry &optional recursive) 168 | (password-store--run "remove" 169 | "--force" 170 | (if recursive "--recursive") 171 | entry)) 172 | 173 | (defun password-store--run-rename (entry new-entry &optional force) 174 | (password-store--run "rename" 175 | (if force "--force") 176 | entry 177 | new-entry)) 178 | 179 | (defun password-store--run-copy (entry new-entry &optional force) 180 | (password-store--run "copy" 181 | (if force "--force") 182 | entry 183 | new-entry)) 184 | 185 | (defun password-store--run-git (&rest args) 186 | (apply 'password-store--run "git" 187 | args)) 188 | 189 | (defun password-store--run-version () 190 | (password-store--run "version")) 191 | 192 | (defvar password-store-kill-ring-pointer nil 193 | "The tail of of the kill ring ring whose car is the password.") 194 | 195 | (defun password-store-dir () 196 | "Return password store directory." 197 | (or (bound-and-true-p auth-source-pass-filename) 198 | (getenv "PASSWORD_STORE_DIR") 199 | "~/.password-store")) 200 | 201 | (defun password-store--entry-to-file (entry) 202 | "Return file name corresponding to ENTRY." 203 | (concat (expand-file-name entry (password-store-dir)) ".gpg")) 204 | 205 | (defun password-store--file-to-entry (file) 206 | "Return entry name corresponding to FILE." 207 | (file-name-sans-extension (file-relative-name file (password-store-dir)))) 208 | 209 | (defun password-store--completing-read (&optional require-match) 210 | "Read a password entry in the minibuffer, with completion. 211 | 212 | Require a matching password if `REQUIRE-MATCH' is 't'." 213 | (completing-read "Password entry: " (password-store-list) nil require-match)) 214 | 215 | (defun password-store-parse-entry (entry) 216 | "Return an alist of the data associated with ENTRY. 217 | 218 | ENTRY is the name of a password-store entry." 219 | (auth-source-pass-parse-entry entry)) 220 | 221 | (defun password-store-read-field (entry) 222 | "Read a field in the minibuffer, with completion for ENTRY." 223 | (let ((valid-fields 224 | (let ((inhibit-message t)) 225 | (mapcar #'car (password-store-parse-entry entry))))) 226 | (completing-read "Field: " valid-fields nil 'match))) 227 | 228 | (defun password-store-list (&optional subdir) 229 | "List password entries under SUBDIR." 230 | (unless subdir (setq subdir "")) 231 | (let ((dir (expand-file-name subdir (password-store-dir)))) 232 | (if (file-directory-p dir) 233 | (delete-dups 234 | (mapcar 'password-store--file-to-entry 235 | (directory-files-recursively dir ".+\\.gpg\\'")))))) 236 | 237 | ;;;###autoload 238 | (defun password-store-edit (entry) 239 | "Edit password for ENTRY." 240 | (interactive (list (password-store--completing-read t))) 241 | (password-store--run-edit entry)) 242 | 243 | ;;;###autoload 244 | (defun password-store-get (entry &optional callback) 245 | "Return password for ENTRY. 246 | 247 | Returns the first line of the password data. When CALLBACK is 248 | non-`NIL', call CALLBACK with the first line instead." 249 | (let ((secret 250 | (let ((inhibit-message t)) 251 | (auth-source-pass-get 'secret entry)))) 252 | (if callback 253 | (funcall callback secret) 254 | secret))) 255 | 256 | ;;;###autoload 257 | (defun password-store-get-field (entry field &optional callback) 258 | "Return FIELD for ENTRY. 259 | FIELD is a string, for instance \"url\". When CALLBACK is 260 | non-`NIL', call it with the line associated to FIELD instead. If 261 | FIELD equals to symbol secret, then this function reduces to 262 | `password-store-get'." 263 | (let ((secret 264 | (let ((inhibit-message t)) 265 | (auth-source-pass-get field entry)))) 266 | (if callback 267 | (funcall callback secret) 268 | secret))) 269 | 270 | 271 | ;;;###autoload 272 | (defun password-store-clear (&optional field) 273 | "Clear secret in the kill ring. 274 | 275 | Optional argument FIELD, a symbol or a string, describes the 276 | stored secret to clear; if nil, then set it to 'secret. Note, 277 | FIELD does not affect the function logic; it is only used to 278 | display the message: 279 | 280 | \(message \"Field %s cleared from kill ring and system clipboard.\" field)." 281 | (interactive "i") 282 | (unless field (setq field 'secret)) 283 | (when password-store-timeout-timer 284 | (cancel-timer password-store-timeout-timer) 285 | (setq password-store-timeout-timer nil)) 286 | (when password-store-kill-ring-pointer 287 | (setcar password-store-kill-ring-pointer "") 288 | (kill-new "") 289 | (setq password-store-kill-ring-pointer nil) 290 | (message "Field %s cleared from kill ring and system clipboard." field))) 291 | 292 | (defun password-store--save-field-in-kill-ring (entry secret field) 293 | (password-store-clear field) 294 | (kill-new secret) 295 | (setq password-store-kill-ring-pointer kill-ring-yank-pointer) 296 | (message "Copied %s for %s to the kill ring and system clipboard. Will clear in %s seconds." 297 | field entry password-store-time-before-clipboard-restore) 298 | (setq password-store-timeout-timer 299 | (run-at-time password-store-time-before-clipboard-restore nil 300 | (lambda () (funcall #'password-store-clear field))))) 301 | 302 | ;;;###autoload 303 | (defun password-store-copy (entry) 304 | "Add password for ENTRY into the kill ring. 305 | 306 | Clear previous password from the kill ring. Pointer to the kill 307 | ring is stored in `password-store-kill-ring-pointer'. Password 308 | is cleared after `password-store-time-before-clipboard-restore' 309 | seconds." 310 | (interactive (list (password-store--completing-read t))) 311 | (password-store-get 312 | entry 313 | (lambda (password) 314 | (password-store--save-field-in-kill-ring entry password 'secret)))) 315 | 316 | ;;;###autoload 317 | (defun password-store-copy-field (entry field) 318 | "Add FIELD for ENTRY into the kill ring. 319 | 320 | Clear previous secret from the kill ring. Pointer to the kill 321 | ring is stored in `password-store-kill-ring-pointer'. Secret 322 | field is cleared after 323 | `password-store-time-before-clipboard-restore' seconds. If FIELD 324 | equals to symbol secret, then this function reduces to 325 | `password-store-copy'." 326 | (interactive 327 | (let ((entry (password-store--completing-read))) 328 | (list entry (password-store-read-field entry)))) 329 | (password-store-get-field 330 | entry 331 | field 332 | (lambda (secret-value) 333 | (password-store--save-field-in-kill-ring entry secret-value field)))) 334 | 335 | ;;;###autoload 336 | (defun password-store-init (gpg-id) 337 | "Initialize new password store and use GPG-ID for encryption. 338 | 339 | Separate multiple IDs with spaces." 340 | (interactive (list (read-string "GPG ID: "))) 341 | (message "%s" (password-store--run-init (split-string gpg-id)))) 342 | 343 | ;;;###autoload 344 | (defun password-store-insert (entry password) 345 | "Insert a new ENTRY containing PASSWORD." 346 | (interactive (list (password-store--completing-read) 347 | (read-passwd "Password: " t))) 348 | (let* ((command (format "echo %s | %s insert -m -f %s" 349 | (shell-quote-argument password) 350 | password-store-executable 351 | (shell-quote-argument entry))) 352 | (ret (process-file-shell-command command))) 353 | (if (zerop ret) 354 | (message "Successfully inserted entry for %s" entry) 355 | (message "Cannot insert entry for %s" entry)) 356 | nil)) 357 | 358 | ;;;###autoload 359 | (defun password-store-generate (entry &optional password-length) 360 | "Generate a new password for ENTRY with PASSWORD-LENGTH. 361 | 362 | Default PASSWORD-LENGTH is `password-store-password-length'." 363 | (interactive (list (password-store--completing-read) 364 | (and current-prefix-arg 365 | (abs (prefix-numeric-value current-prefix-arg))))) 366 | ;; A message with the output of the command is not printed 367 | ;; because the output contains the password. 368 | (password-store--run-generate 369 | entry 370 | (or password-length password-store-password-length) 371 | 'force) 372 | nil) 373 | 374 | ;;;###autoload 375 | (defun password-store-generate-no-symbols (entry &optional password-length) 376 | "Generate a new password without symbols for ENTRY with PASSWORD-LENGTH. 377 | 378 | Default PASSWORD-LENGTH is `password-store-password-length'." 379 | (interactive (list (password-store--completing-read) 380 | (and current-prefix-arg 381 | (abs (prefix-numeric-value current-prefix-arg))))) 382 | 383 | ;; A message with the output of the command is not printed 384 | ;; because the output contains the password. 385 | (password-store--run-generate 386 | entry 387 | (or password-length password-store-password-length) 388 | 'force 'no-symbols) 389 | nil) 390 | 391 | ;;;###autoload 392 | (defun password-store-remove (entry) 393 | "Remove ENTRY." 394 | (interactive (list (password-store--completing-read t))) 395 | (message "%s" (password-store--run-remove entry t))) 396 | 397 | ;;;###autoload 398 | (defun password-store-rename (entry new-entry) 399 | "Rename ENTRY to NEW-ENTRY." 400 | (interactive (list (password-store--completing-read t) 401 | (read-string "Rename entry to: "))) 402 | (message "%s" (password-store--run-rename entry new-entry t))) 403 | 404 | ;;;###autoload 405 | (defun password-store-version () 406 | "Show version of `password-store-executable'." 407 | (interactive) 408 | (message "%s" (password-store--run-version))) 409 | 410 | ;;;###autoload 411 | (defun password-store-url (entry) 412 | "Load URL for ENTRY." 413 | (interactive (list (password-store--completing-read t))) 414 | (let ((url (password-store-get-field entry password-store-url-field))) 415 | (if url (browse-url url) 416 | (error "Field `%s' not found" password-store-url-field)))) 417 | 418 | 419 | (provide 'password-store) 420 | 421 | ;;; password-store.el ends here 422 | -------------------------------------------------------------------------------- /contrib/importers/1password2pass.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright (C) 2014 Tobias V. Langhoff . All Rights Reserved. 4 | # This file is licensed under GPLv2+. Please see COPYING for more information. 5 | # 6 | # 1Password Importer 7 | # 8 | # Reads files exported from 1Password and imports them into pass. Supports comma 9 | # and tab delimited text files, as well as logins (but not other items) stored 10 | # in the 1Password Interchange File (1PIF) format. 11 | # 12 | # Supports using the title (default) or URL as pass-name, depending on your 13 | # preferred organization. Also supports importing metadata, adding them with 14 | # `pass insert --multiline`; the username and URL are compatible with 15 | # https://github.com/jvenant/passff. 16 | 17 | require "optparse" 18 | require "ostruct" 19 | 20 | accepted_formats = [".txt", ".1pif"] 21 | 22 | # Default options 23 | options = OpenStruct.new 24 | options.force = false 25 | options.name = :title 26 | options.notes = true 27 | options.meta = true 28 | 29 | optparse = OptionParser.new do |opts| 30 | opts.banner = "Usage: #{opts.program_name}.rb [options] filename" 31 | opts.on_tail("-h", "--help", "Display this screen") { puts opts; exit } 32 | opts.on("-f", "--force", "Overwrite existing passwords") do 33 | options.force = true 34 | end 35 | opts.on("-d", "--default [FOLDER]", "Place passwords into FOLDER") do |group| 36 | options.group = group 37 | end 38 | opts.on("-n", "--name [PASS-NAME]", [:title, :url], 39 | "Select field to use as pass-name: title (default) or URL") do |name| 40 | options.name = name 41 | end 42 | opts.on("-m", "--[no-]meta", 43 | "Import metadata and insert it below the password") do |meta| 44 | options.meta = meta 45 | end 46 | 47 | begin 48 | opts.parse! 49 | rescue OptionParser::InvalidOption 50 | $stderr.puts optparse 51 | exit 52 | end 53 | end 54 | 55 | # Check for a valid filename 56 | filename = ARGV.pop 57 | unless filename 58 | abort optparse.to_s 59 | end 60 | unless accepted_formats.include?(File.extname(filename.downcase)) 61 | abort "Supported file types: comma/tab delimited .txt files and .1pif files." 62 | end 63 | 64 | passwords = [] 65 | 66 | # Parse comma or tab delimited text 67 | if File.extname(filename) =~ /.txt/i 68 | require "csv" 69 | 70 | # Very simple way to guess the delimiter 71 | delimiter = "" 72 | File.open(filename) do |file| 73 | first_line = file.readline 74 | if first_line =~ /,/ 75 | delimiter = "," 76 | elsif first_line =~ /\t/ 77 | delimiter = "\t" 78 | else 79 | abort "Supported file types: comma/tab delimited .txt files and .1pif files." 80 | end 81 | end 82 | 83 | # Import CSV/TSV 84 | CSV.foreach(filename, {col_sep: delimiter, headers: true, header_converters: :symbol}) do |entry| 85 | pass = {} 86 | pass[:name] = "#{(options.group + "/") if options.group}#{entry[options.name]}" 87 | pass[:title] = entry[:title] 88 | pass[:password] = entry[:password] 89 | pass[:login] = entry[:username] 90 | pass[:url] = entry[:url] 91 | pass[:notes] = entry[:notes] 92 | passwords << pass 93 | end 94 | # Parse 1PIF 95 | elsif File.extname(filename) =~ /.1pif/i 96 | require "json" 97 | 98 | options.name = :location if options.name == :url 99 | 100 | # 1PIF is almost JSON, but not quite. Remove the ***...*** lines 101 | # separating records, and then remove the trailing comma 102 | pif = File.open(filename).read.gsub(/^\*\*\*.*\*\*\*$/, ",").chomp.chomp(",") 103 | 104 | # Import 1PIF 105 | JSON.parse("[#{pif}]", symbolize_names: true).each do |entry| 106 | next unless entry[:typeName] == "webforms.WebForm" 107 | next if entry[:secureContents][:fields].nil? 108 | 109 | pass = {} 110 | 111 | pass[:name] = "#{(options.group + "/") if options.group}#{entry[options.name]}" 112 | 113 | pass[:title] = entry[:title] 114 | 115 | pass[:password] = entry[:secureContents][:fields].detect do |field| 116 | field[:designation] == "password" 117 | end[:value] 118 | 119 | username = entry[:secureContents][:fields].detect do |field| 120 | field[:designation] == "username" 121 | end 122 | # might be nil 123 | pass[:login] = username[:value] if username 124 | 125 | pass[:url] = entry[:location] 126 | pass[:notes] = entry[:secureContents][:notesPlain] 127 | passwords << pass 128 | end 129 | end 130 | 131 | puts "Read #{passwords.length} passwords." 132 | 133 | errors = [] 134 | # Save the passwords 135 | passwords.each do |pass| 136 | IO.popen("pass insert #{"-f " if options.force}-m \"#{pass[:name]}\" > /dev/null", "w") do |io| 137 | io.puts pass[:password] 138 | if options.meta 139 | io.puts "login: #{pass[:login]}" unless pass[:login].to_s.empty? 140 | io.puts "url: #{pass[:url]}" unless pass[:url].to_s.empty? 141 | io.puts pass[:notes] unless pass[:notes].to_s.empty? 142 | end 143 | end 144 | if $? == 0 145 | puts "Imported #{pass[:name]}" 146 | else 147 | $stderr.puts "ERROR: Failed to import #{pass[:name]}" 148 | errors << pass 149 | end 150 | end 151 | 152 | if errors.length > 0 153 | $stderr.puts "Failed to import #{errors.map {|e| e[:name]}.join ", "}" 154 | $stderr.puts "Check the errors. Make sure these passwords do not already "\ 155 | "exist. If you're sure you want to overwrite them with the "\ 156 | "new import, try again with --force." 157 | end 158 | -------------------------------------------------------------------------------- /contrib/importers/fpm2pass.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # Copyright (C) 2012 Jeffrey Ratcliffe . All Rights Reserved. 4 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 5 | 6 | use warnings; 7 | use strict; 8 | use XML::Simple; 9 | use Getopt::Long; 10 | use Pod::Usage; 11 | 12 | my ($help, $man); 13 | my @args = ('help' => \$help, 14 | 'man' => \$man,); 15 | GetOptions (@args) or pod2usage(2); 16 | pod2usage(1) if ($help); 17 | pod2usage(-exitstatus => 0, -verbose => 2) if $man; 18 | pod2usage( 19 | -msg => "Syntax error: must specify a file to read.", 20 | -exitval => 2, 21 | -verbose => 1 22 | ) 23 | if (@ARGV != 1); 24 | 25 | # Grab the XML to a perl structure 26 | my $xs = XML::Simple->new(); 27 | my $doc = $xs->XMLin(shift); 28 | 29 | for (@{$doc->{PasswordList}{PasswordItem}}) { 30 | my $name; 31 | if (ref($_->{category}) eq 'HASH') { 32 | $name = escape($_->{title}); 33 | } 34 | else { 35 | $name = escape($_->{category})."/".escape($_->{title}); 36 | } 37 | my $contents = ''; 38 | $contents .= "$_->{password}\n" unless (ref($_->{password}) eq 'HASH'); 39 | $contents .= "user $_->{user}\n" unless (ref($_->{user}) eq 'HASH'); 40 | $contents .= "url $_->{url}\n" unless (ref($_->{url}) eq 'HASH'); 41 | unless (ref($_->{notes}) eq 'HASH') { 42 | $_->{notes} =~ s/\n/\n /g; 43 | $contents .= "notes:\n $_->{notes}\n"; 44 | } 45 | my $cmd = "pass insert -f -m $name"; 46 | my $pid = open(my $fh, "| $cmd") or die "Couldn't fork: $!\n"; 47 | print $fh $contents; 48 | close $fh; 49 | } 50 | 51 | # escape inverted commas, spaces, ampersands and brackets 52 | sub escape { 53 | my ($s) = @_; 54 | $s =~ s/\//-/g; 55 | $s =~ s/(['\(\) &])/\\$1/g; 56 | return $s; 57 | } 58 | 59 | =head1 NAME 60 | 61 | fpm2pass.pl - imports an .xml exported by fpm2 into pass 62 | 63 | =head1 SYNOPSIS 64 | 65 | =head1 USAGE 66 | 67 | fpm2pass.pl [--help] [--man] 68 | 69 | The following options are available: 70 | 71 | =over 72 | 73 | =item --help 74 | 75 | =item --man 76 | 77 | =back 78 | 79 | =cut 80 | -------------------------------------------------------------------------------- /contrib/importers/gorilla2pass.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright (C) 2013 David Sklar . All Rights Reserved. 4 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 5 | 6 | entries = {} 7 | 8 | class HashCounter 9 | 10 | def initialize 11 | @h = Hash.new {|h,k| h[k] = 2 } 12 | end 13 | 14 | def get(k) 15 | v = @h[k] 16 | @h[k] = v + 1 17 | v 18 | end 19 | end 20 | 21 | hc = HashCounter.new 22 | 23 | $stdin.each do |line| 24 | uuid, group, title, url, user, password, notes = line.strip.split(',') 25 | next if uuid == "uuid" 26 | 27 | # check for missing group 28 | # check for missing title 29 | 30 | prefix = "#{group}/#{title}".gsub(/[\s\'\"()!]/,'') 31 | 32 | 33 | if user && user.length > 0 34 | entries["#{prefix}/user"] = user 35 | end 36 | if url && url.length > 0 37 | entries["#{prefix}/url"] = url 38 | end 39 | if password && password.length > 0 40 | entries["#{prefix}/password"] = password 41 | end 42 | if notes && notes.length > 0 43 | entries["#{prefix}/notes"] = notes.gsub('\n',"\n").strip 44 | end 45 | end 46 | 47 | entries.keys.each do |k| 48 | if k =~ /^(.+?)-merged\d{4}-\d\d-\d\d\d\d:\d\d:\d\d(\/.+)$/ 49 | other = $1 + $2 50 | if entries.has_key?(other) 51 | if entries[k] == entries[other] 52 | entries.delete(k) 53 | else 54 | i = hc.get(other) 55 | entries["#{other}#{i}"] = entries[k] 56 | entries.delete(k) 57 | end 58 | else 59 | entries[other] = entries[k] 60 | entries.delete(k) 61 | end 62 | end 63 | end 64 | 65 | pass_top_level = "Gorilla" 66 | entries.keys.each do |k| 67 | print "#{k}...(#{entries[k]})..." 68 | IO.popen("pass insert -e -f '#{pass_top_level}/#{k}' > /dev/null", 'w') do |io| 69 | io.puts entries[k] + "\n" 70 | end 71 | if $? == 0 72 | puts " done!" 73 | else 74 | puts " error!" 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /contrib/importers/kedpm2pass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2012 Antoine Beaupré . All Rights Reserved. 5 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 6 | # 7 | # To double-check your import worked: 8 | # grep Path passwords | sed 's#^Path: ##;s/$/.gpg/' | sort > listpaths 9 | # (cd ~/.password-store/ ; find -type f ) | sort | diff -u - listpaths 10 | 11 | import re 12 | import fileinput 13 | 14 | import sys # for exit 15 | 16 | import subprocess 17 | 18 | def insert(d): 19 | path = d['Path'] 20 | del d['Path'] 21 | print "inserting " + path 22 | content = d['Password'] + "\n" 23 | del d['Password'] 24 | for k, v in d.iteritems(): 25 | content += "%s: %s\n" % (k, v) 26 | del d 27 | cmd = ["pass", "insert", "--force", "--multiline", path] 28 | process = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 29 | stdout, stderr = process.communicate(content) 30 | retcode = process.wait() 31 | if retcode: 32 | print 'command "%s" failed with exit code %d: %s' % (" ".join(cmd), retcode, stdout + stderr) 33 | sys.exit(1); 34 | 35 | d = None 36 | for line in fileinput.input(): 37 | if line == "\n": 38 | continue 39 | match = re.match("(\w+): (.*)$", line) 40 | if match: 41 | if match.group(1) == 'Path': 42 | if d is not None: 43 | insert(d) 44 | else: 45 | d = {} 46 | d[match.group(1)] = match.group(2) 47 | #print "found field: %s => %s" % (match.group(1), match.group(2)) 48 | else: 49 | print "warning: no match found on line: *%s*" % line 50 | 51 | if d is not None: 52 | insert(d) 53 | -------------------------------------------------------------------------------- /contrib/importers/keepass2csv2pass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2015 David Francoeur 4 | # Copyright 2017 Nathan Sommer 5 | # 6 | # This file is licensed under the GPLv2+. Please see COPYING for more 7 | # information. 8 | # 9 | # KeePassX 2+ on Mac allows export to CSV. The CSV contains the following 10 | # headers: 11 | # "Group","Title","Username","Password","URL","Notes" 12 | # 13 | # By default the pass entry will have the path Group/Title/Username and will 14 | # have the following structure: 15 | # 16 | # 17 | # user: 18 | # url: 19 | # notes: 20 | # 21 | # Any missing fields will be omitted from the entry. If Username is not present 22 | # the path will be Group/Title. 23 | # 24 | # The username can be left out of the path by using the --name_is_original 25 | # switch. Group and Title can be converted to lowercase using the --to_lower 26 | # switch. Groups can be excluded using the --exclude_groups option. 27 | # 28 | # Default usage: ./keepass2csv2pass.py input.csv 29 | # 30 | # To see the full usage: ./keepass2csv2pass.py -h 31 | 32 | import sys 33 | import csv 34 | import argparse 35 | from subprocess import Popen, PIPE 36 | 37 | 38 | class KeepassCSVArgParser(argparse.ArgumentParser): 39 | """ 40 | Custom ArgumentParser class which prints the full usage message if the 41 | input file is not provided. 42 | """ 43 | def error(self, message): 44 | print(message, file=sys.stderr) 45 | self.print_help() 46 | sys.exit(2) 47 | 48 | 49 | def pass_import_entry(path, data): 50 | """Import new password entry to password-store using pass insert command""" 51 | proc = Popen(['pass', 'insert', '--multiline', path], stdin=PIPE, 52 | stdout=PIPE) 53 | proc.communicate(data.encode('utf8')) 54 | proc.wait() 55 | 56 | 57 | def confirmation(prompt): 58 | """ 59 | Ask the user for 'y' or 'n' confirmation and return a boolean indicating 60 | the user's choice. Returns True if the user simply presses enter. 61 | """ 62 | 63 | prompt = '{0} {1} '.format(prompt, '(Y/n)') 64 | 65 | while True: 66 | user_input = input(prompt) 67 | 68 | if len(user_input) > 0: 69 | first_char = user_input.lower()[0] 70 | else: 71 | first_char = 'y' 72 | 73 | if first_char == 'y': 74 | return True 75 | elif first_char == 'n': 76 | return False 77 | 78 | print('Please enter y or n') 79 | 80 | 81 | def insert_file_contents(filename, preparation_args): 82 | """ Read the file and insert each entry """ 83 | 84 | entries = [] 85 | 86 | with open(filename, 'rU') as csv_in: 87 | next(csv_in) 88 | csv_out = (line for line in csv.reader(csv_in, dialect='excel')) 89 | for row in csv_out: 90 | path, data = prepare_for_insertion(row, **preparation_args) 91 | if path and data: 92 | entries.append((path, data)) 93 | 94 | if len(entries) == 0: 95 | return 96 | 97 | print('Entries to import:') 98 | 99 | for (path, data) in entries: 100 | print(path) 101 | 102 | if confirmation('Proceed?'): 103 | for (path, data) in entries: 104 | pass_import_entry(path, data) 105 | print(path, 'imported!') 106 | 107 | 108 | def prepare_for_insertion(row, name_is_username=True, convert_to_lower=False, 109 | exclude_groups=None): 110 | """Prepare a CSV row as an insertable string""" 111 | 112 | group = escape(row[0]) 113 | name = escape(row[1]) 114 | 115 | # Bail if we are to exclude this group 116 | if exclude_groups is not None: 117 | for exclude_group in exclude_groups: 118 | if exclude_group.lower() in group.lower(): 119 | return None, None 120 | 121 | # The first component of the group is 'Root', which we do not need 122 | group_components = group.split('/')[1:] 123 | 124 | path = '/'.join(group_components + [name]) 125 | 126 | if convert_to_lower: 127 | path = path.lower() 128 | 129 | username = row[2] 130 | password = row[3] 131 | url = row[4] 132 | notes = row[5] 133 | 134 | if username and name_is_username: 135 | path += '/' + username 136 | 137 | data = '{}\n'.format(password) 138 | 139 | if username: 140 | data += 'user: {}\n'.format(username) 141 | 142 | if url: 143 | data += 'url: {}\n'.format(url) 144 | 145 | if notes: 146 | data += 'notes: {}\n'.format(notes) 147 | 148 | return path, data 149 | 150 | 151 | def escape(str_to_escape): 152 | """ escape the list """ 153 | return str_to_escape.replace(" ", "-")\ 154 | .replace("&", "and")\ 155 | .replace("[", "")\ 156 | .replace("]", "") 157 | 158 | 159 | def main(): 160 | description = 'Import pass entries from an exported KeePassX CSV file.' 161 | parser = KeepassCSVArgParser(description=description) 162 | 163 | parser.add_argument('--exclude_groups', nargs='+', 164 | help='Groups to exclude when importing') 165 | parser.add_argument('--to_lower', action='store_true', 166 | help='Convert group and name to lowercase') 167 | parser.add_argument('--name_is_original', action='store_true', 168 | help='Use the original entry name instead of the ' 169 | 'username for the pass entry') 170 | parser.add_argument('input_file', help='The CSV file to read from') 171 | 172 | args = parser.parse_args() 173 | 174 | preparation_args = { 175 | 'convert_to_lower': args.to_lower, 176 | 'name_is_username': not args.name_is_original, 177 | 'exclude_groups': args.exclude_groups 178 | } 179 | 180 | input_file = args.input_file 181 | print("File to read:", input_file) 182 | insert_file_contents(input_file, preparation_args) 183 | 184 | 185 | if __name__ == '__main__': 186 | main() 187 | -------------------------------------------------------------------------------- /contrib/importers/keepass2pass.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2013 Stefan Simroth . All Rights Reserved. 5 | # Based on the script for KeepassX by Juhamatti Niemelä . 6 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 7 | # 8 | # Usage: 9 | # ./keepass2pass.py -f export.xml 10 | # By default, takes the name of the root element and puts all passwords in there, but you can disable this: 11 | # ./keepass2pass.py -f export.xml -r "" 12 | # Or you can use another root folder: 13 | # ./keepass2pass.py -f export.xml -r foo 14 | # 15 | # Features: 16 | # * This script can handle duplicates and will merge them. 17 | # * Besides the password also the fields 'UserName', 'URL' and 'Notes' (comment) will be inserted. 18 | # * You get a warning if an entry has no password, but it will still insert it. 19 | 20 | import getopt, sys 21 | from subprocess import Popen, PIPE 22 | from xml.etree import ElementTree 23 | 24 | 25 | def pass_import_entry(path, data): 26 | """ Import new password entry to password-store using pass insert command """ 27 | proc = Popen(['pass', 'insert', '--multiline', path], stdin=PIPE, stdout=PIPE) 28 | proc.communicate(data.encode('utf8')) 29 | proc.wait() 30 | 31 | 32 | def get_value(elements, node_text): 33 | for element in elements: 34 | for child in element.findall('Key'): 35 | if child.text == node_text: 36 | return element.find('Value').text 37 | return '' 38 | 39 | def path_for(element, path=''): 40 | """ Generate path name from elements title and current path """ 41 | if element.tag == 'Entry': 42 | title = get_value(element.findall("String"), "Title") 43 | elif element.tag == 'Group': 44 | title = element.find('Name').text 45 | else: title = '' 46 | 47 | if path == '': return title 48 | else: return '/'.join([path, title]) 49 | 50 | def password_data(element, path=''): 51 | """ Return password data and additional info if available from password entry element. """ 52 | data = "" 53 | password = get_value(element.findall('String'), 'Password') 54 | if password is not None: data = password + "\n" 55 | else: 56 | print "[WARN] No password: %s" % path_for(element, path) 57 | 58 | for field in ['UserName', 'URL', 'Notes']: 59 | value = get_value(element, field) 60 | if value is not None and not len(value) == 0: 61 | data = "%s%s: %s\n" % (data, field, value) 62 | return data 63 | 64 | def import_entry(entries, element, path=''): 65 | element_path = path_for(element, path) 66 | if entries.has_key(element_path): 67 | print "[INFO] Duplicate needs merging: %s" % element_path 68 | existing_data = entries[element_path] 69 | data = "%s---------\nPassword: %s" % (existing_data, password_data(element)) 70 | else: 71 | data = password_data(element, path) 72 | 73 | entries[element_path] = data 74 | 75 | def import_group(entries, element, path='', npath=None): 76 | """ Import all entries and sub-groups from given group """ 77 | if npath is None: 78 | npath = path_for(element, path) 79 | for group in element.findall('Group'): 80 | import_group(entries, group, npath) 81 | for entry in element.findall('Entry'): 82 | import_entry(entries, entry, npath) 83 | 84 | def import_passwords(xml_file, root_path=None): 85 | """ Parse given Keepass2 XML file and import password groups from it """ 86 | print "[>>>>] Importing passwords from file %s" % xml_file 87 | print "[INFO] Root path: %s" % root_path 88 | entries = dict() 89 | with open(xml_file) as xml: 90 | text = xml.read() 91 | xml_tree = ElementTree.XML(text) 92 | root = xml_tree.find('Root') 93 | root_group = root.find('Group') 94 | import_group(entries, root_group, '', root_path) 95 | password_count = 0 96 | for path, data in sorted(entries.iteritems()): 97 | sys.stdout.write("[>>>>] Importing %s ... " % path.encode("utf-8")) 98 | pass_import_entry(path, data) 99 | sys.stdout.write("OK\n") 100 | password_count += 1 101 | 102 | print "[ OK ] Done. Imported %i passwords." % password_count 103 | 104 | 105 | def usage(): 106 | """ Print usage """ 107 | print "Usage: %s -f XML_FILE" % (sys.argv[0]) 108 | print "Optional:" 109 | print " -r ROOT_PATH Different root path to use than the one in xml file, use \"\" for none" 110 | 111 | 112 | def main(argv): 113 | try: 114 | opts, args = getopt.gnu_getopt(argv, "f:r:") 115 | except getopt.GetoptError as err: 116 | print str(err) 117 | usage() 118 | sys.exit(2) 119 | 120 | xml_file = None 121 | root_path = None 122 | 123 | for opt, arg in opts: 124 | if opt in "-f": 125 | xml_file = arg 126 | if opt in "-r": 127 | root_path = arg 128 | 129 | if xml_file is not None: 130 | import_passwords(xml_file, root_path) 131 | else: 132 | usage() 133 | sys.exit(2) 134 | 135 | if __name__ == '__main__': 136 | main(sys.argv[1:]) 137 | -------------------------------------------------------------------------------- /contrib/importers/keepassx2pass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (C) 2012 Juhamatti Niemelä . All Rights Reserved. 4 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 5 | # 6 | # Usage ./keepassx2pass.py export.xml 7 | 8 | import sys 9 | import re 10 | 11 | from subprocess import Popen, PIPE 12 | from xml.etree import ElementTree 13 | 14 | def space_to_camelcase(value): 15 | output = "" 16 | first_word_passed = False 17 | for word in value.split(" "): 18 | if not word: 19 | output += "_" 20 | continue 21 | if first_word_passed: 22 | output += word.capitalize() 23 | else: 24 | output += word.lower() 25 | first_word_passed = True 26 | return output 27 | 28 | def cleanTitle(title): 29 | # make the title more command line friendly 30 | title = re.sub("(\\|\||\(|\)|/)", "-", title) 31 | title = re.sub("-$", "", title) 32 | title = re.sub("\@", "At", title) 33 | title = re.sub("'", "", title) 34 | return title 35 | 36 | def path_for(element, path=''): 37 | """ Generate path name from elements title and current path """ 38 | title_text = element.find('title').text 39 | if title_text is None: 40 | title_text = '' 41 | title = cleanTitle(space_to_camelcase(title_text)) 42 | return '/'.join([path, title]) 43 | 44 | def password_data(element): 45 | """ Return password data and additional info if available from 46 | password entry element. """ 47 | passwd = element.find('password').text 48 | ret = (passwd + "\n") if passwd else "\n" 49 | for field in ['username', 'url', 'comment']: 50 | fel = element.find(field) 51 | children = [(e.text or '') + (e.tail or '') for e in list(fel)] 52 | if len(children) > 0: 53 | children.insert(0, '') 54 | text = (fel.text or '') + "\n".join(children) 55 | if len(text) > 0: 56 | ret = "%s%s: %s\n" % (ret, fel.tag, text) 57 | return ret 58 | 59 | def import_entry(element, path=''): 60 | """ Import new password entry to password-store using pass insert 61 | command """ 62 | print("Importing " + path_for(element, path)) 63 | proc = Popen(['pass', 'insert', '--multiline', '--force', 64 | path_for(element, path)], 65 | stdin=PIPE, stdout=PIPE) 66 | proc.communicate(password_data(element).encode()) 67 | proc.wait() 68 | 69 | def import_group(element, path=''): 70 | """ Import all entries and sub-groups from given group """ 71 | npath = path_for(element, path) 72 | for group in element.findall('group'): 73 | import_group(group, npath) 74 | for entry in element.findall('entry'): 75 | import_entry(entry, npath) 76 | 77 | 78 | def main(xml_file): 79 | """ Parse given KeepassX XML file and import password groups from it """ 80 | for group in ElementTree.parse(xml_file).findall('group'): 81 | import_group(group) 82 | 83 | if __name__ == '__main__': 84 | main(sys.argv[1]) 85 | -------------------------------------------------------------------------------- /contrib/importers/kwallet2pass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2012 Juhamatti Niemelä . All Rights Reserved. 5 | # Copyright (C) 2014 Diggory Hardy . All Rights Reserved. 6 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 7 | 8 | import sys 9 | import re 10 | 11 | from subprocess import Popen, PIPE 12 | from xml.etree import ElementTree 13 | 14 | HEAD = '/passwords/' 15 | 16 | def insert_data(path,text): 17 | """ Insert data into the password store. 18 | (1) removes HEAD from path 19 | (2) ensures text ends with a new line and encodes in UTF-8 20 | (3) inserts 21 | """ 22 | global HEAD 23 | if path.startswith(HEAD): 24 | path = path[len(HEAD):] 25 | 26 | if not text.endswith('\n'): 27 | text = text + '\n' 28 | text = text.encode('utf8') 29 | 30 | #print "Import: " + path + ": " + text 31 | proc = Popen(['pass', 'insert', '--multiline', '--force', path], 32 | stdin=PIPE, stdout=PIPE) 33 | proc.communicate(text) 34 | proc.wait() 35 | 36 | def space_to_camelcase(value): 37 | output = "" 38 | first_word_passed = False 39 | for word in value.split(" "): 40 | if not word: 41 | output += "_" 42 | continue 43 | if first_word_passed: 44 | output += word.capitalize() 45 | else: 46 | output += word.lower() 47 | first_word_passed = True 48 | return output 49 | 50 | def cleanTitle(title): 51 | # make the title more command line friendly 52 | title = re.sub("(\\|\||\(|\)|/)", "-", title) 53 | title = re.sub("-$", "", title) 54 | title = re.sub("\@", "At", title) 55 | title = re.sub("'", "", title) 56 | return title 57 | 58 | def path_for(element, path=''): 59 | """ Generate path name from elements title and current path """ 60 | title = cleanTitle(space_to_camelcase(element.attrib['name'])) 61 | return '/'.join([path, title]) 62 | 63 | def unexpected(element, path): 64 | print "Unexpected element: " + path + '/' + element.tag + "\tAttributes: " + str(element.attrib) 65 | 66 | def import_map(element, path): 67 | npath = path_for(element, path) 68 | nEntries = 0 69 | text = 'Map' 70 | for child in element: 71 | if child.tag == 'mapentry': 72 | name = child.attrib['name'] 73 | text = text + '\n\n' + name + '\n' + child.text 74 | nEntries += 1 75 | for child2 in child: 76 | unexpected(child, path_for(child, npath)) 77 | else: 78 | unexpected(child, npath) 79 | 80 | insert_data(npath, text) 81 | print "Map " + npath + " [" + str(nEntries) + " entries]" 82 | 83 | def import_password(element, path=''): 84 | """ Import new password entry to password-store using pass insert 85 | command """ 86 | npath = path_for(element, path) 87 | text = element.text 88 | if text == None: 89 | print "Password " + npath + ": no text" 90 | text = "" 91 | insert_data(npath, text) 92 | for child in element: 93 | unexpected(child, npath) 94 | 95 | def import_folder(element, path=''): 96 | """ Import all entries and folders from given folder """ 97 | npath = path_for(element, path) 98 | print "Importing folder " + npath 99 | nPasswords = 0 100 | for child in element: 101 | if child.tag == 'folder': 102 | import_folder(child, npath) 103 | elif child.tag == 'password': 104 | import_password(child, npath) 105 | nPasswords += 1 106 | elif child.tag == 'map': 107 | import_map(child, npath) 108 | else: 109 | unexpected(child, npath) 110 | 111 | if nPasswords > 0: 112 | print "[" + str(nPasswords) + " passwords]" 113 | 114 | def main(xml_file): 115 | """ Parse XML entries from a KWallet """ 116 | element = ElementTree.parse(xml_file).getroot() 117 | assert element.tag == 'wallet' 118 | import_folder(element) 119 | 120 | if __name__ == '__main__': 121 | main(sys.argv[1]) 122 | -------------------------------------------------------------------------------- /contrib/importers/lastpass2pass.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright (C) 2012 Alex Sayers . All Rights Reserved. 4 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 5 | 6 | # LastPass Importer 7 | # 8 | # Reads CSV files exported from LastPass and imports them into pass. 9 | 10 | # Usage: 11 | # 12 | # Go to lastpass.com and sign in. Next click on your username in the top-right 13 | # corner. In the drop-down meny that appears, click "Export". After filling in 14 | # your details again, copy the text and save it somewhere on your disk. Make sure 15 | # you copy the whole thing, and resist the temptation to "Save Page As" - the 16 | # script doesn't like HTML. 17 | # 18 | # Fire up a terminal and run the script, passing the file you saved as an argument. 19 | # It should look something like this: 20 | # 21 | #$ ./lastpass2pass.rb path/to/passwords_file.csv 22 | 23 | # Parse flags 24 | require 'optparse' 25 | optparse = OptionParser.new do |opts| 26 | opts.banner = "Usage: #{$0} [options] filename" 27 | 28 | FORCE = false 29 | opts.on("-f", "--force", "Overwrite existing records") { FORCE = true } 30 | DEFAULT_GROUP = "" 31 | opts.on("-d", "--default GROUP", "Place uncategorised records into GROUP") { |group| DEFAULT_GROUP = group } 32 | opts.on("-h", "--help", "Display this screen") { puts opts; exit } 33 | 34 | opts.parse! 35 | end 36 | 37 | # Check for a filename 38 | if ARGV.empty? 39 | puts optparse 40 | exit 0 41 | end 42 | 43 | # Get filename of csv file 44 | filename = ARGV.join(" ") 45 | puts "Reading '#{filename}'..." 46 | 47 | 48 | class Record 49 | def initialize name, url, username, password, extra, grouping, fav 50 | @name, @url, @username, @password, @extra, @grouping, @fav = name, url, username, password, extra, grouping, fav 51 | end 52 | 53 | def name 54 | s = "" 55 | s << @grouping + "/" unless @grouping.empty? 56 | s << @name unless @name == nil 57 | s.gsub(/ /, "_").gsub(/'/, "") 58 | end 59 | 60 | def to_s 61 | s = "" 62 | s << "#{@password}\n---\n" 63 | s << "#{@grouping} / " unless @grouping.empty? 64 | s << "#{@name}\n" 65 | s << "username: #{@username}\n" unless @username.empty? 66 | s << "password: #{@password}\n" unless @password.empty? 67 | s << "url: #{@url}\n" unless @url == "http://sn" 68 | s << "#{@extra}\n" unless @extra.nil? 69 | return s 70 | end 71 | end 72 | 73 | # Extract individual records 74 | entries = [] 75 | entry = "" 76 | begin 77 | file = File.open(filename) 78 | file.each do |line| 79 | if line =~ /^(http|ftp|ssh)/ 80 | entries.push(entry) 81 | entry = "" 82 | end 83 | entry += line 84 | end 85 | entries.push(entry) 86 | entries.shift 87 | puts "#{entries.length} records found!" 88 | rescue 89 | puts "Couldn't find #{filename}!" 90 | exit 1 91 | end 92 | 93 | # Parse records and create Record objects 94 | records = [] 95 | entries.each do |e| 96 | args = e.split(",") 97 | url = args.shift 98 | username = args.shift 99 | password = args.shift 100 | fav = args.pop 101 | grouping = args.pop 102 | grouping = DEFAULT_GROUP if grouping == nil 103 | name = args.pop 104 | extra = args.join(",")[1...-1] 105 | 106 | records << Record.new(name, url, username, password, extra, grouping, fav) 107 | end 108 | puts "Records parsed: #{records.length}" 109 | 110 | successful = 0 111 | errors = [] 112 | records.each do |r| 113 | if File.exist?("#{r.name}.gpg") and FORCE == false 114 | puts "skipped #{r.name}: already exists" 115 | next 116 | end 117 | print "Creating record #{r.name}..." 118 | IO.popen("pass insert -m '#{r.name}' > /dev/null", 'w') do |io| 119 | io.puts r 120 | end 121 | if $? == 0 122 | puts " done!" 123 | successful += 1 124 | else 125 | puts " error!" 126 | errors << r 127 | end 128 | end 129 | puts "#{successful} records successfully imported!" 130 | 131 | if errors.length > 0 132 | puts "There were #{errors.length} errors:" 133 | errors.each { |e| print e.name + (e == errors.last ? ".\n" : ", ")} 134 | puts "These probably occurred because an identically-named record already existed, or because there were multiple entries with the same name in the csv file." 135 | end 136 | -------------------------------------------------------------------------------- /contrib/importers/password-exporter2pass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (C) 2016 Daniele Pizzolli 5 | # 6 | # This file is licensed under GPLv2+. Please see COPYING for more 7 | # information. 8 | 9 | """Import password(s) exported by Password Exporter for Firefox in 10 | csv format to pass format. Supports Password Exporter format 1.1. 11 | """ 12 | 13 | import argparse 14 | import base64 15 | import csv 16 | import sys 17 | import subprocess 18 | 19 | 20 | PASS_PROG = 'pass' 21 | DEFAULT_USERNAME = 'login' 22 | 23 | 24 | def main(): 25 | "Parse the arguments and run the passimport with appropriate arguments." 26 | description = """\ 27 | Import password(s) exported by Password Exporter for Firefox in csv 28 | format to pass format. Supports Password Exporter format 1.1. 29 | 30 | Check the first line of your exported file. 31 | 32 | Must start with: 33 | 34 | # Generated by Password Exporter; Export format 1.1; 35 | 36 | Support obfuscated export (wrongly called encrypted by Password Exporter). 37 | 38 | It should help you to migrate from the default Firefox password 39 | store to passff. 40 | 41 | Please note that Password Exporter or passff may have problem with 42 | fields containing characters like " or :. 43 | 44 | More info at: 45 | 46 | 47 | """ 48 | parser = argparse.ArgumentParser(description=description) 49 | parser.add_argument( 50 | "filepath", type=str, 51 | help="The password Exporter generated file") 52 | parser.add_argument( 53 | "-p", "--prefix", type=str, 54 | help="Prefix for pass store path, you may want to use: sites") 55 | parser.add_argument( 56 | "-d", "--force", action="store_true", 57 | help="Call pass with --force option") 58 | parser.add_argument( 59 | "-v", "--verbose", action="store_true", 60 | help="Show pass output") 61 | parser.add_argument( 62 | "-q", "--quiet", action="store_true", 63 | help="No output") 64 | 65 | args = parser.parse_args() 66 | 67 | passimport(args.filepath, prefix=args.prefix, force=args.force, 68 | verbose=args.verbose, quiet=args.quiet) 69 | 70 | 71 | def passimport(filepath, prefix=None, force=False, verbose=False, quiet=False): 72 | "Import the password from filepath to pass" 73 | with open(filepath, 'rb') as csvfile: 74 | # Skip the first line if starts with a comment, as usually are 75 | # file exported with Password Exporter 76 | first_line = csvfile.readline() 77 | 78 | if not first_line.startswith( 79 | '# Generated by Password Exporter; Export format 1.1;'): 80 | sys.exit('Input format not supported') 81 | 82 | # Auto detect if the file is obfuscated 83 | obfuscation = False 84 | if first_line.startswith( 85 | ('# Generated by Password Exporter; ' 86 | 'Export format 1.1; Encrypted: true')): 87 | obfuscation = True 88 | 89 | if not first_line.startswith('#'): 90 | csvfile.seek(0) 91 | 92 | reader = csv.DictReader(csvfile, delimiter=',', quotechar='"') 93 | for row in reader: 94 | try: 95 | username = row['username'] 96 | password = row['password'] 97 | 98 | if obfuscation: 99 | username = base64.b64decode(row['username']) 100 | password = base64.b64decode(row['password']) 101 | 102 | # Not sure if some fiel can be empty, anyway tries to be 103 | # reasonably safe 104 | text = '{}\n'.format(password) 105 | if row['passwordField']: 106 | text += '{}: {}\n'.format(row['passwordField'], password) 107 | if username: 108 | text += '{}: {}\n'.format( 109 | row.get('usernameField', DEFAULT_USERNAME), username) 110 | if row['hostname']: 111 | text += 'Hostname: {}\n'.format(row['hostname']) 112 | if row['httpRealm']: 113 | text += 'httpRealm: {}\n'.format(row['httpRealm']) 114 | if row['formSubmitURL']: 115 | text += 'formSubmitURL: {}\n'.format(row['formSubmitURL']) 116 | 117 | # Remove the protocol prefix for http(s) 118 | simplename = row['hostname'].replace( 119 | 'https://', '').replace('http://', '') 120 | 121 | # Rough protection for fancy username like “; rm -Rf /\n” 122 | userpath = "".join(x for x in username if x.isalnum()) 123 | # TODO add some escape/protection also to the hostname 124 | storename = '{}@{}'.format(userpath, simplename) 125 | storepath = storename 126 | 127 | if prefix: 128 | storepath = '{}/{}'.format(prefix, storename) 129 | 130 | cmd = [PASS_PROG, 'insert', '--multiline'] 131 | 132 | if force: 133 | cmd.append('--force') 134 | 135 | cmd.append(storepath) 136 | 137 | proc = subprocess.Popen( 138 | cmd, 139 | stdin=subprocess.PIPE, 140 | stdout=subprocess.PIPE, 141 | stderr=subprocess.PIPE) 142 | stdout, stderr = proc.communicate(text) 143 | retcode = proc.wait() 144 | 145 | # TODO: please note that sometimes pass does not return an 146 | # error 147 | # 148 | # After this command: 149 | # 150 | # pass git config --bool --add pass.signcommits true 151 | # 152 | # pass import will fail with: 153 | # 154 | # gpg: skipped "First Last ": 155 | # secret key not available 156 | # gpg: signing failed: secret key not available 157 | # error: gpg failed to sign the data 158 | # fatal: failed to write commit object 159 | # 160 | # But the retcode is still 0. 161 | # 162 | # Workaround: add the first signing key id explicitly with: 163 | # 164 | # SIGKEY=$(gpg2 --list-keys --with-colons user@example.com | \ 165 | # awk -F : '/:s:$/ {printf "0x%s\n", $5; exit}') 166 | # pass git config --add user.signingkey "${SIGKEY}" 167 | 168 | if retcode: 169 | print 'command {}" failed with exit code {}: {}'.format( 170 | " ".join(cmd), retcode, stdout + stderr) 171 | 172 | if not quiet: 173 | print 'Imported {}'.format(storepath) 174 | 175 | if verbose: 176 | print stdout + stderr 177 | except: 178 | print 'Error: corrupted line: {}'.format(row) 179 | 180 | if __name__ == '__main__': 181 | main() 182 | -------------------------------------------------------------------------------- /contrib/importers/pwsafe2pass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2017 Sam Mason . All Rights Reserved. 4 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 5 | 6 | import sys 7 | import subprocess 8 | 9 | import pandas as pd 10 | 11 | # assumes STDIN is generated via File=>Export from the Mac version of 12 | # pwsafe, available from https://pwsafe.info/ 13 | df = pd.read_table(sys.stdin) 14 | df.sort_values(['Group/Title','Username'], inplace=True) 15 | 16 | tr = { 17 | ord('.'): '/', 18 | ord('»'): '.' 19 | } 20 | 21 | for i,row in df.iterrows(): 22 | na = row.notnull() 23 | 24 | path = 'pwsafe/{}'.format(row['Group/Title'].strip().translate(tr)) 25 | value = '{}\n'.format(row['Password']) 26 | 27 | if na['Username']: 28 | path = '{}/{}'.format(path,row['Username'].strip()) 29 | 30 | if na['e-mail']: 31 | value = 'email: {}\n'.format(value,row['e-mail'].strip()) 32 | 33 | if na['Notes']: 34 | value = '\n{}\n'.format(value, row['Notes'].strip()) 35 | 36 | with subprocess.Popen(['pass','add','-m',path],stdin=subprocess.PIPE) as proc: 37 | proc.communicate(value.encode('utf8')) 38 | if proc.returncode: 39 | print('failure with {}, returned {}'.format( 40 | path, proc.returncode)) 41 | -------------------------------------------------------------------------------- /contrib/importers/pwsafe2pass.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright (C) 2013 Tom Hendrikx . All Rights Reserved. 3 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 4 | 5 | export=$1 6 | 7 | IFS=" " # tab character 8 | cat "$export" | while read uuid group name login passwd notes; do 9 | test "$uuid" = "# passwordsafe version 2.0 database" && continue 10 | test "$uuid" = "uuid" && continue 11 | test "$name" = '""' && continue; 12 | 13 | group="$(echo $group | cut -d'"' -f2)" 14 | login="$(echo $login | cut -d'"' -f2)" 15 | passwd="$(echo $passwd | cut -d'"' -f2)" 16 | name="$(echo $name | cut -d'"' -f2)" 17 | 18 | # cleanup 19 | test "${name:0:4}" = "http" && name="$(echo $name | cut -d'/' -f3)" 20 | test "${name:0:4}" = "www." && name="$(echo $name | cut -c 5-)" 21 | 22 | entry="" 23 | test -n "$login" && entry="${entry}login: $login\n" 24 | test -n "$passwd" && entry="${entry}pass: $passwd\n" 25 | test -n "$group" && entry="${entry}group: $group\n" 26 | 27 | echo Adding entry for $name: 28 | echo -e $entry | pass insert --multiline --force "$name" 29 | test $? && echo "Added!" 30 | done 31 | -------------------------------------------------------------------------------- /contrib/importers/revelation2pass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2013 Emanuele Aina . All Rights Reserved. 5 | # Copyright (C) 2011 Toni Corvera. All Rights Reserved. 6 | # This file is licensed under the BSD 2-clause license: 7 | # http://www.opensource.org/licenses/BSD-2-Clause 8 | # 9 | # Import script for the Revelation password manager: 10 | # http://revelation.olasagasti.info/ 11 | # Heavily based on the Relevation command line tool: 12 | # http://p.outlyer.net/relevation/ 13 | 14 | import os, sys, argparse, zlib, getpass, traceback 15 | from subprocess import Popen, PIPE, STDOUT, CalledProcessError 16 | from collections import OrderedDict 17 | try: 18 | from lxml import etree 19 | except ImportError: 20 | from xml.etree import ElementTree as etree 21 | 22 | USE_PYCRYPTO = True 23 | try: 24 | from Crypto.Cipher import AES 25 | except ImportError: 26 | USE_PYCRYPTO = False 27 | try: 28 | from crypto.cipher import rijndael, cbc 29 | from crypto.cipher.base import noPadding 30 | except ImportError: 31 | sys.stderr.write('Either PyCrypto or cryptopy are required\n') 32 | raise 33 | 34 | def path_for(element, path=None): 35 | """ Generate path name from elements name and current path """ 36 | name = element.find('name').text 37 | name = name.replace('/', '-').replace('\\', '-') 38 | path = path if path else '' 39 | return os.path.join(path, name) 40 | 41 | def format_password_data(data): 42 | """ Format the secret data that will be handed to Pass in multi-line mode: 43 | $password 44 | $fieldname: $fielddata 45 | ... 46 | $multi_line_notes_with_leading_spaces""" 47 | password = data.pop('password', None) or '' 48 | ret = password + '\n' 49 | notes = data.pop('notes', None) 50 | for label, text in data.iteritems(): 51 | ret += label + ': ' + text + '\n' 52 | if notes: 53 | ret += ' ' + notes.replace('\n', '\n ').strip() + '\n' 54 | return ret 55 | 56 | def password_data(element): 57 | """ Return password data and additional info if available from 58 | password entry element. """ 59 | data = OrderedDict() 60 | try: 61 | data['password'] = element.find('field[@id="generic-password"]').text 62 | except AttributeError: 63 | data['password'] = None 64 | data['type'] = element.attrib['type'] 65 | for field in element.findall('field'): 66 | field_id = field.attrib['id'] 67 | if field_id == 'generic-password': 68 | continue 69 | if field.text is not None: 70 | data[field_id] = field.text 71 | for tag in ('description', 'notes'): 72 | field = element.find(tag) 73 | if field is not None and field.text: 74 | data[tag] = field.text 75 | return format_password_data(data) 76 | 77 | 78 | def import_entry(element, path=None, verbose=0): 79 | """ Import new password entry to password-store using pass insert 80 | command """ 81 | cmd = ['pass', 'insert', '--multiline', '--force', path_for(element, path)] 82 | if verbose: 83 | print 'cmd:\n ' + ' '.join(cmd) 84 | proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 85 | stdin = password_data(element).encode('utf8') 86 | if verbose: 87 | print 'input:\n ' + stdin.replace('\n', '\n ').strip() 88 | stdout, _ = proc.communicate(stdin) 89 | retcode = proc.poll() 90 | if retcode: 91 | raise CalledProcessError(retcode, cmd, output=stdout) 92 | 93 | def import_folder(element, path=None, verbose=0): 94 | path = path_for(element, path) 95 | import_subentries(element, path, verbose) 96 | 97 | def import_subentries(element, path=None, verbose=0): 98 | """ Import all sub entries of the current folder element """ 99 | for entry in element.findall('entry'): 100 | if entry.attrib['type'] == 'folder': 101 | import_folder(entry, path, verbose) 102 | else: 103 | import_entry(entry, path, verbose) 104 | 105 | def decrypt_gz(key, cipher_text): 106 | ''' Decrypt cipher_text using key. 107 | decrypt(str, str) -> cleartext (gzipped xml) 108 | 109 | This function will use the underlying, available, cipher module. 110 | ''' 111 | if USE_PYCRYPTO: 112 | # Extract IV 113 | c = AES.new(key) 114 | iv = c.decrypt(cipher_text[12:28]) 115 | # Decrypt data, CBC mode 116 | c = AES.new(key, AES.MODE_CBC, iv) 117 | ct = c.decrypt(cipher_text[28:]) 118 | else: 119 | # Extract IV 120 | c = rijndael.Rijndael(key, keySize=len(key), padding=noPadding()) 121 | iv = c.decrypt(cipher_text[12:28]) 122 | # Decrypt data, CBC mode 123 | bc = rijndael.Rijndael(key, keySize=len(key), padding=noPadding()) 124 | c = cbc.CBC(bc, padding=noPadding()) 125 | ct = c.decrypt(cipher_text[28:], iv=iv) 126 | return ct 127 | 128 | def main(datafile, verbose=False, xml=False): 129 | f = None 130 | with open(datafile, "rb") as f: 131 | # Encrypted data 132 | data = f.read() 133 | if xml: 134 | xmldata = data 135 | else: 136 | password = getpass.getpass() 137 | # Pad password 138 | password += (chr(0) * (32 - len(password))) 139 | # Decrypt. Decrypted data is compressed 140 | cleardata_gz = decrypt_gz(password, data) 141 | # Length of data padding 142 | padlen = ord(cleardata_gz[-1]) 143 | # Decompress actual data (15 is wbits [ref3] DON'T CHANGE, 2**15 is the (initial) buf size) 144 | xmldata = zlib.decompress(cleardata_gz[:-padlen], 15, 2**15) 145 | root = etree.fromstring(xmldata) 146 | import_subentries(root, verbose=verbose) 147 | 148 | if __name__ == '__main__': 149 | parser = argparse.ArgumentParser() 150 | parser.add_argument('-x', '--xml', help='read plain XML file', action='store_true') 151 | parser.add_argument('--verbose', '-v', action='count') 152 | parser.add_argument('FILE', help="the file storing the Revelation passwords") 153 | args = parser.parse_args() 154 | 155 | def err(s): 156 | sys.stderr.write(s+'\n') 157 | 158 | try: 159 | main(args.FILE, verbose=args.verbose, xml=args.xml) 160 | except KeyboardInterrupt: 161 | if args.verbose: 162 | traceback.print_exc() 163 | err(str(e)) 164 | except zlib.error: 165 | err('Failed to decompress decrypted data. Wrong password?') 166 | sys.exit(os.EX_DATAERR) 167 | except CalledProcessError as e: 168 | if args.verbose: 169 | traceback.print_exc() 170 | print 'output:\n ' + e.output.replace('\n', '\n ').strip() 171 | else: 172 | err('CalledProcessError: ' + str(e)) 173 | sys.exit(os.EX_IOERR) 174 | except IOError as e: 175 | if args.verbose: 176 | traceback.print_exc() 177 | else: 178 | err('IOError: ' + str(e)) 179 | sys.exit(os.EX_IOERR) 180 | -------------------------------------------------------------------------------- /contrib/importers/roboform2pass.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright 2015 Sergey Makridenkov . 4 | # Released under MIT License. 5 | # 6 | # Usage: 7 | # 1. Save roboform print lists (like File > Print lists > Logins) to ~/roboform_print_lists. 8 | # 2. Run import: ./roboform2pass.rb ~/roboform_print_lists 9 | 10 | 11 | require 'nokogiri' 12 | 13 | class Login 14 | def initialize 15 | self.fields = {} 16 | self.raw = [] 17 | end 18 | 19 | attr_accessor :key 20 | attr_accessor :password 21 | attr_accessor :url 22 | attr_accessor :fields 23 | attr_accessor :raw 24 | 25 | def ask_required_info 26 | ask!(:key) if blank?(key) 27 | find_password! 28 | if blank?(password) 29 | ask!(:password) 30 | puts '' 31 | end 32 | end 33 | 34 | def save 35 | if valid? 36 | IO.popen("pass insert -m -f '#{path}' > /dev/null", "w") do |pass_io| 37 | pass_io.puts password 38 | pass_io.puts "Url: #{url}" if present?(url) 39 | pass_io.puts "Fields: #{fields}" if fields.any? 40 | pass_io.puts "Notes: #{raw}" if raw.any? 41 | end 42 | 43 | $? == 0 44 | end 45 | end 46 | 47 | private 48 | 49 | def path 50 | key = self.key.downcase.gsub(/[^\w\.\/]/, '_').gsub(/_{2,}/, '_') 51 | "roboform/#{key}" 52 | end 53 | 54 | def valid? 55 | present?(key) && present?(password) 56 | end 57 | 58 | def find_password! 59 | fields.each do |key, val| 60 | key = key.downcase 61 | if key.include?('password') || key.include?('pwd') || key.include?('pass') 62 | self.password = val 63 | end 64 | end 65 | end 66 | 67 | def ask!(field) 68 | puts Colorize.red("#{field.capitalize} is empty for login:") 69 | print_self 70 | print Colorize.green "Please type #{field}: " 71 | self.send("#{field}=", gets.chomp) 72 | end 73 | 74 | def print_self 75 | puts Colorize.yellow "\tKey:\t#{key}" if present?(key) 76 | puts Colorize.yellow "\tPassword:\t#{password}" if present?(password) 77 | puts Colorize.yellow "\tUrl:\t#{url}" if present?(url) 78 | puts Colorize.yellow "\tFields:\t#{fields}" if fields.any? 79 | puts Colorize.yellow "\tRaw:\t#{raw}" if raw.any? 80 | end 81 | 82 | def blank?(str) 83 | !str || str.strip.empty? 84 | end 85 | 86 | def present?(str) 87 | !blank?(str) 88 | end 89 | end 90 | 91 | class Colorize 92 | class << self 93 | def red(mes) 94 | colorize(31, mes) 95 | end 96 | 97 | def green(mes) 98 | colorize(32, mes) 99 | end 100 | 101 | def yellow(mes) 102 | colorize(33, mes) 103 | end 104 | 105 | def pink(mes) 106 | colorize(35, mes) 107 | end 108 | 109 | private 110 | 111 | def colorize(color_code, mes) 112 | "\e[#{color_code}m#{mes}\e[0m" 113 | end 114 | end 115 | end 116 | 117 | 118 | print_list_dir = ARGV.pop 119 | unless print_list_dir 120 | raise "No dir/to/roboform/print_lists" 121 | end 122 | print_list_dir = File.expand_path(print_list_dir) 123 | 124 | # parse logins 125 | logins_path = Dir.glob("#{print_list_dir}/RoboForm Logins*.html").first 126 | unless logins_path 127 | raise 'Login HTML (RoboForm Logins*.html) not found' 128 | end 129 | 130 | html_logins = Nokogiri::HTML(File.open(logins_path)) 131 | 132 | saved_logins = 0 133 | 134 | html_logins.css('table').each do |table| 135 | login = Login.new 136 | 137 | table.css('tr').each do |tr| 138 | caption = tr.at_css('.caption') 139 | subcaption = tr.at_css('.subcaption') 140 | key = tr.at_css('.field') 141 | 142 | if caption 143 | login.key = caption.text() 144 | elsif subcaption 145 | login.url = subcaption.text() 146 | elsif key 147 | login.fields[key.text()] = tr.at_css('.wordbreakfield').text() 148 | else 149 | login.raw << tr.at_css('.wordbreakfield').text() 150 | end 151 | end 152 | 153 | if login.fields.any? 154 | login.ask_required_info 155 | if login.save 156 | saved_logins += 1 157 | end 158 | end 159 | end 160 | 161 | puts "Imported passwords: #{saved_logins}" 162 | 163 | -------------------------------------------------------------------------------- /contrib/pass.applescript: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zx2c4/password-store/3ca13cd8882cae4083c1c478858adbf2e82dd037/contrib/pass.applescript -------------------------------------------------------------------------------- /contrib/vim/redact_pass.txt: -------------------------------------------------------------------------------- 1 | *redact_pass.txt* For Vim version 6.0 Last change: 2018 June 24 2 | 3 | DESCRIPTION *redact_pass* 4 | 5 | This plugin switches off the 'viminfo', 'backup', 'writebackup', 'swapfile', 6 | and 'undofile' options globally when editing a password in `pass(1)`. 7 | 8 | This is to prevent anyone being able to extract passwords from your Vim cache 9 | files in the event of a compromise. 10 | 11 | You should test this after installation to ensure you see this message is 12 | printed whenever you `pass edit`: 13 | 14 | > Editing password file--disabled leaky options! 15 | 16 | REQUIREMENTS *redact_pass-requirements* 17 | 18 | This plugin is only available if 'compatible' is not set. It also requires the 19 | |+autocmd| feature. 20 | 21 | IMPLEMENTATION *redact_pass-implementation* 22 | 23 | The options are disabled globally rather than attempting to set them local to 24 | the buffer only, which was the flawed approach of previous versions. This is 25 | mostly because of the 'viminfo' option; it's global, and there's no meaningful 26 | way to exclude information from the sensitive buffer from appearing in it. 27 | 28 | Because the typical use case for editing a password file in Vim is that you 29 | load and change a single short document, and then quit, it's more sensible to 30 | just turn the relevant options off completely, and makes what the plugin is 31 | doing more reliable and straightforward to understand. 32 | 33 | AUTHOR *redact_pass-author* 34 | 35 | Written and maintained by Tom Ryder . 36 | 37 | LICENSE *redact_pass-license* 38 | 39 | Licensed for distribution under the same terms as the pass(1) project. 40 | 41 | vim:tw=78:ts=8:ft=help:norl: 42 | -------------------------------------------------------------------------------- /contrib/vim/redact_pass.vim: -------------------------------------------------------------------------------- 1 | " 2 | " redact_pass.vim: Switch off the 'viminfo', 'backup', 'writebackup', 3 | " 'swapfile', and 'undofile' globally when editing a password in pass(1). 4 | " 5 | " This is to prevent anyone being able to extract passwords from your Vim 6 | " cache files in the event of a compromise. 7 | " 8 | " Author: Tom Ryder 9 | " License: Same as Vim itself 10 | " 11 | if exists('g:loaded_redact_pass') || &compatible 12 | finish 13 | endif 14 | if !has('autocmd') || v:version < 600 15 | finish 16 | endif 17 | let g:loaded_redact_pass = 1 18 | 19 | " Check whether we should set redacting options or not 20 | function! s:CheckArgsRedact() 21 | 22 | " Ensure there's one argument and it's the matched file 23 | if argc() != 1 || fnamemodify(argv(0), ':p') !=# expand(':p') 24 | return 25 | endif 26 | 27 | " Disable all the leaky options globally 28 | set nobackup 29 | set nowritebackup 30 | set noswapfile 31 | set viminfo= 32 | if has('persistent_undo') 33 | set noundofile 34 | endif 35 | 36 | " Tell the user what we're doing so they know this worked, via a message and 37 | " a global variable they can check 38 | redraw 39 | echomsg 'Editing password file--disabled leaky options!' 40 | let g:redact_pass_redacted = 1 41 | 42 | endfunction 43 | 44 | " Auto function loads only when Vim starts up 45 | augroup redact_pass 46 | autocmd! 47 | autocmd VimEnter 48 | \ /dev/shm/pass.?*/?*.txt 49 | \,$TMPDIR/pass.?*/?*.txt 50 | \,/tmp/pass.?*/?*.txt 51 | \ call s:CheckArgsRedact() 52 | " Work around macOS' dynamic symlink structure for temporary directories 53 | if has('mac') 54 | autocmd VimEnter 55 | \ /private/var/?*/pass.?*/?*.txt 56 | \ call s:CheckArgsRedact() 57 | endif 58 | augroup END 59 | -------------------------------------------------------------------------------- /man/example-filter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This is a super bootleg script for converting plaintext examples into groff. 4 | 5 | while read line; do 6 | echo "$line" | while read -n 1 char; do 7 | if [[ $char == "%" ]]; then 8 | echo -n '%' 9 | continue 10 | fi 11 | ord=$(printf '%d' "'$char") 12 | if [[ $ord -eq 0 ]]; then 13 | printf ' ' 14 | elif [[ $ord -gt 127 ]]; then 15 | printf '\[u%X]' "'$char" 16 | else 17 | printf "$char" 18 | fi 19 | done 20 | echo 21 | echo ".br" 22 | done 23 | -------------------------------------------------------------------------------- /man/pass.1: -------------------------------------------------------------------------------- 1 | .TH PASS 1 "2014 March 18" ZX2C4 "Password Store" 2 | 3 | .SH NAME 4 | pass - stores, retrieves, generates, and synchronizes passwords securely 5 | 6 | .SH SYNOPSIS 7 | .B pass 8 | [ 9 | .I COMMAND 10 | ] [ 11 | .I OPTIONS 12 | ]... [ 13 | .I ARGS 14 | ]... 15 | 16 | .SH DESCRIPTION 17 | 18 | .B pass 19 | is a very simple password store that keeps passwords inside 20 | .BR gpg2 (1) 21 | encrypted files inside a simple directory tree residing at 22 | .IR ~/.password-store . 23 | The 24 | .B pass 25 | utility provides a series of commands for manipulating the password store, 26 | allowing the user to add, remove, edit, synchronize, generate, and manipulate 27 | passwords. 28 | 29 | If no COMMAND is specified, COMMAND defaults to either 30 | .B show 31 | or 32 | .BR ls , 33 | depending on the type of specifier in ARGS. Alternatively, if \fIPASSWORD_STORE_ENABLE_EXTENSIONS\fP 34 | is set to "true", and the file \fI.extensions/COMMAND.bash\fP exists inside the 35 | password store and is executable, then it is sourced into the environment, 36 | passing any arguments and environment variables. Extensions existing in a 37 | system-wide directory, only installable by the administrator, are always enabled. 38 | 39 | Otherwise COMMAND must be one of the valid commands listed below. 40 | 41 | Several of the commands below rely on or provide additional functionality if 42 | the password store directory is also a git repository. If the password store 43 | directory is a git repository, all password store modification commands will 44 | cause a corresponding git commit. Sub-directories may be separate nested git 45 | repositories, and pass will use the inner-most directory relative to the 46 | current password. See the \fIEXTENDED GIT EXAMPLE\fP section for a detailed 47 | description using \fBinit\fP and 48 | .BR git (1). 49 | 50 | The \fBinit\fP command must be run before other commands in order to initialize 51 | the password store with the correct gpg key id. Passwords are encrypted using 52 | the gpg key set with \fBinit\fP. 53 | 54 | There is a corresponding bash completion script for use with tab completing 55 | password names in 56 | .BR bash (1). 57 | 58 | .SH COMMANDS 59 | 60 | .TP 61 | \fBinit\fP [ \fI--path=sub-folder\fP, \fI-p sub-folder\fP ] \fIgpg-id...\fP 62 | Initialize new password storage and use 63 | .I gpg-id 64 | for encryption. Multiple gpg-ids may be specified, in order to encrypt each 65 | password with multiple ids. This command must be run first before a password 66 | store can be used. If the specified \fIgpg-id\fP is different from the key 67 | used in any existing files, these files will be reencrypted to use the new id. 68 | Note that use of 69 | .BR gpg-agent (1) 70 | is recommended so that the batch decryption does not require as much user 71 | intervention. If \fI--path\fP or \fI-p\fP is specified, along with an argument, 72 | a specific gpg-id or set of gpg-ids is assigned for that specific sub folder of 73 | the password store. If only one \fIgpg-id\fP is given, and it is an empty string, 74 | then the current \fI.gpg-id\fP file for the specified \fIsub-folder\fP (or root if 75 | unspecified) is removed. 76 | .TP 77 | \fBls\fP \fIsubfolder\fP 78 | List names of passwords inside the tree at 79 | .I subfolder 80 | by using the 81 | .BR tree (1) 82 | program. This command is alternatively named \fBlist\fP. 83 | .TP 84 | \fBgrep\fP [\fIGREPOPTIONS\fP] \fIsearch-string\fP 85 | Searches inside each decrypted password file for \fIsearch-string\fP, and displays line 86 | containing matched string along with filename. Uses 87 | .BR grep (1) 88 | for matching. \fIGREPOPTIONS\fP are passed to 89 | .BR grep (1) 90 | as-is. (Note: the \fIGREP_OPTIONS\fP environment variable functions as well.) 91 | .TP 92 | \fBfind\fP \fIpass-names\fP... 93 | List names of passwords inside the tree that match \fIpass-names\fP by using the 94 | .BR tree (1) 95 | program. This command is alternatively named \fBsearch\fP. 96 | .TP 97 | \fBshow\fP [ \fI--clip\fP[=\fIline-number\fP], \fI-c\fP[\fIline-number\fP] ] [ \fI--qrcode\fP[=\fIline-number\fP], \fI-q\fP[\fIline-number\fP] ] \fIpass-name\fP 98 | Decrypt and print a password named \fIpass-name\fP. If \fI--clip\fP or \fI-c\fP 99 | is specified, do not print the password but instead copy the first (or otherwise specified) 100 | line to the clipboard using 101 | .BR xclip (1) 102 | or 103 | .BR wl-clipboard(1) 104 | and then restore the clipboard after 45 (or \fIPASSWORD_STORE_CLIP_TIME\fP) seconds. If \fI--qrcode\fP 105 | or \fI-q\fP is specified, do not print the password but instead display a QR code using 106 | .BR qrencode (1) 107 | either to the terminal or graphically if supported. 108 | .TP 109 | \fBinsert\fP [ \fI--echo\fP, \fI-e\fP | \fI--multiline\fP, \fI-m\fP ] [ \fI--force\fP, \fI-f\fP ] \fIpass-name\fP 110 | Insert a new password into the password store called \fIpass-name\fP. This will 111 | read the new password from standard in. If \fI--echo\fP or \fI-e\fP is \fInot\fP specified, 112 | disable keyboard echo when the password is entered and confirm the password by asking 113 | for it twice. If \fI--multiline\fP or \fI-m\fP is specified, lines will be read until 114 | EOF or Ctrl+D is reached. Otherwise, only a single line from standard in is read. Prompt 115 | before overwriting an existing password, unless \fI--force\fP or \fI-f\fP is specified. This 116 | command is alternatively named \fBadd\fP. 117 | .TP 118 | \fBedit\fP \fIpass-name\fP 119 | Insert a new password or edit an existing password using the default text editor specified 120 | by the environment variable \fIEDITOR\fP or using 121 | .BR vi (1) 122 | as a fallback. This mode makes use of temporary files for editing, but care is taken to 123 | ensure that temporary files are created in \fI/dev/shm\fP in order to avoid writing to 124 | difficult-to-erase disk sectors. If \fI/dev/shm\fP is not accessible, fallback to 125 | the ordinary \fITMPDIR\fP location, and print a warning. 126 | .TP 127 | \fBgenerate\fP [ \fI--no-symbols\fP, \fI-n\fP ] [ \fI--clip\fP, \fI-c\fP ] [ \fI--in-place\fP, \fI-i\fP | \fI--force\fP, \fI-f\fP ] \fIpass-name [pass-length]\fP 128 | Generate a new password using \fB/dev/urandom\fP of length \fIpass-length\fP 129 | (or \fIPASSWORD_STORE_GENERATED_LENGTH\fP if unspecified) and insert into 130 | \fIpass-name\fP. If \fI--no-symbols\fP or \fI-n\fP is specified, do not use 131 | any non-alphanumeric characters in the generated password. The character sets used 132 | in generating passwords can be changed with the \fIPASSWORD_STORE_CHARACTER_SET\fP and 133 | \fIPASSWORD_STORE_CHARACTER_SET_NO_SYMBOLS\fP environment variables, described below. 134 | If \fI--clip\fP or \fI-c\fP is specified, do not print the password but instead copy 135 | it to the clipboard using 136 | .BR xclip (1) 137 | or 138 | .BR wl-clipboard(1) 139 | and then restore the clipboard after 45 (or \fIPASSWORD_STORE_CLIP_TIME\fP) seconds. If \fI--qrcode\fP 140 | or \fI-q\fP is specified, do not print the password but instead display a QR code using 141 | .BR qrencode (1) 142 | either to the terminal or graphically if supported. Prompt before overwriting an existing password, 143 | unless \fI--force\fP or \fI-f\fP is specified. If \fI--in-place\fP or \fI-i\fP is 144 | specified, do not interactively prompt, and only replace the first line of the password 145 | file with the new generated password, keeping the remainder of the file intact. 146 | .TP 147 | \fBrm\fP [ \fI--recursive\fP, \fI-r\fP ] [ \fI--force\fP, \fI-f\fP ] \fIpass-name\fP 148 | Remove the password named \fIpass-name\fP from the password store. This command is 149 | alternatively named \fBremove\fP or \fBdelete\fP. If \fI--recursive\fP or \fI-r\fP 150 | is specified, delete pass-name recursively if it is a directory. If \fI--force\fP 151 | or \fI-f\fP is specified, do not interactively prompt before removal. 152 | .TP 153 | \fBmv\fP [ \fI--force\fP, \fI-f\fP ] \fIold-path\fP \fInew-path\fP 154 | Renames the password or directory named \fIold-path\fP to \fInew-path\fP. This 155 | command is alternatively named \fBrename\fP. If \fI--force\fP is specified, 156 | silently overwrite \fInew-path\fP if it exists. If \fInew-path\fP ends in a 157 | trailing \fI/\fP, it is always treated as a directory. Passwords are selectively 158 | reencrypted to the corresponding keys of their new destination. 159 | .TP 160 | \fBcp\fP [ \fI--force\fP, \fI-f\fP ] \fIold-path\fP \fInew-path\fP 161 | Copies the password or directory named \fIold-path\fP to \fInew-path\fP. This 162 | command is alternatively named \fBcopy\fP. If \fI--force\fP is specified, 163 | silently overwrite \fInew-path\fP if it exists. If \fInew-path\fP ends in a 164 | trailing \fI/\fP, it is always treated as a directory. Passwords are selectively 165 | reencrypted to the corresponding keys of their new destination. 166 | .TP 167 | \fBgit\fP \fIgit-command-args\fP... 168 | If the password store is a git repository, pass \fIgit-command-args\fP as arguments to 169 | .BR git (1) 170 | using the password store as the git repository. If \fIgit-command-args\fP is \fBinit\fP, 171 | in addition to initializing the git repository, add the current contents of the password 172 | store to the repository in an initial commit. If the git config key \fIpass.signcommits\fP 173 | is set to \fItrue\fP, then all commits will be signed using \fIuser.signingkey\fP or the 174 | default git signing key. This config key may be turned on using: 175 | .B `pass git config --bool --add pass.signcommits true` 176 | .TP 177 | \fBhelp\fP 178 | Show usage message. 179 | .TP 180 | \fBversion\fP 181 | Show version information. 182 | 183 | .SH SIMPLE EXAMPLES 184 | 185 | .TP 186 | Initialize password store 187 | .B zx2c4@laptop ~ $ pass init Jason@zx2c4.com 188 | .br 189 | mkdir: created directory \[u2018]/home/zx2c4/.password-store\[u2019] 190 | .br 191 | Password store initialized for Jason@zx2c4.com. 192 | .TP 193 | List existing passwords in store 194 | .B zx2c4@laptop ~ $ pass 195 | .br 196 | Password Store 197 | .br 198 | \[u251C]\[u2500]\[u2500] Business 199 | .br 200 | \[u2502] \[u251C]\[u2500]\[u2500] some-silly-business-site.com 201 | .br 202 | \[u2502] \[u2514]\[u2500]\[u2500] another-business-site.net 203 | .br 204 | \[u251C]\[u2500]\[u2500] Email 205 | .br 206 | \[u2502] \[u251C]\[u2500]\[u2500] donenfeld.com 207 | .br 208 | \[u2502] \[u2514]\[u2500]\[u2500] zx2c4.com 209 | .br 210 | \[u2514]\[u2500]\[u2500] France 211 | .br 212 | \[u251C]\[u2500]\[u2500] bank 213 | .br 214 | \[u251C]\[u2500]\[u2500] freebox 215 | .br 216 | \[u2514]\[u2500]\[u2500] mobilephone 217 | .br 218 | 219 | .br 220 | Alternatively, "\fBpass ls\fP". 221 | .TP 222 | Find existing passwords in store that match .com 223 | .B zx2c4@laptop ~ $ pass find .com 224 | .br 225 | Search Terms: .com 226 | .br 227 | \[u251C]\[u2500]\[u2500] Business 228 | .br 229 | \[u2502] \[u251C]\[u2500]\[u2500] some-silly-business-site.com 230 | .br 231 | \[u2514]\[u2500]\[u2500] Email 232 | .br 233 | \[u251C]\[u2500]\[u2500] donenfeld.com 234 | .br 235 | \[u2514]\[u2500]\[u2500] zx2c4.com 236 | .br 237 | 238 | .br 239 | Alternatively, "\fBpass search .com\fP". 240 | .TP 241 | Show existing password 242 | .B zx2c4@laptop ~ $ pass Email/zx2c4.com 243 | .br 244 | sup3rh4x3rizmynam3 245 | .TP 246 | Copy existing password to clipboard 247 | .B zx2c4@laptop ~ $ pass -c Email/zx2c4.com 248 | .br 249 | Copied Email/jason@zx2c4.com to clipboard. Will clear in 45 seconds. 250 | .TP 251 | Add password to store 252 | .B zx2c4@laptop ~ $ pass insert Business/cheese-whiz-factory 253 | .br 254 | Enter password for Business/cheese-whiz-factory: omg so much cheese what am i gonna do 255 | .TP 256 | Add multiline password to store 257 | .B zx2c4@laptop ~ $ pass insert -m Business/cheese-whiz-factory 258 | .br 259 | Enter contents of Business/cheese-whiz-factory and press Ctrl+D when finished: 260 | .br 261 | 262 | .br 263 | Hey this is my 264 | .br 265 | awesome 266 | .br 267 | multi 268 | .br 269 | line 270 | .br 271 | passworrrrrrrrd. 272 | .br 273 | ^D 274 | .TP 275 | Generate new password 276 | .B zx2c4@laptop ~ $ pass generate Email/jasondonenfeld.com 15 277 | .br 278 | The generated password to Email/jasondonenfeld.com is: 279 | .br 280 | $(-QF&Q=IN2nFBx 281 | .TP 282 | Generate new alphanumeric password 283 | .B zx2c4@laptop ~ $ pass generate -n Email/jasondonenfeld.com 12 284 | .br 285 | The generated password to Email/jasondonenfeld.com is: 286 | .br 287 | YqFsMkBeO6di 288 | .TP 289 | Generate new password and copy it to the clipboard 290 | .B zx2c4@laptop ~ $ pass generate -c Email/jasondonenfeld.com 19 291 | .br 292 | Copied Email/jasondonenfeld.com to clipboard. Will clear in 45 seconds. 293 | .TP 294 | Remove password from store 295 | .B zx2c4@laptop ~ $ pass remove Business/cheese-whiz-factory 296 | .br 297 | rm: remove regular file \[u2018]/home/zx2c4/.password-store/Business/cheese-whiz-factory.gpg\[u2019]? y 298 | .br 299 | removed \[u2018]/home/zx2c4/.password-store/Business/cheese-whiz-factory.gpg\[u2019] 300 | 301 | .SH EXTENDED GIT EXAMPLE 302 | Here, we initialize new password store, create a git repository, and then manipulate and sync passwords. Make note of the arguments to the first call of \fBpass git push\fP; consult 303 | .BR git-push (1) 304 | for more information. 305 | 306 | .B zx2c4@laptop ~ $ pass init Jason@zx2c4.com 307 | .br 308 | mkdir: created directory \[u2018]/home/zx2c4/.password-store\[u2019] 309 | .br 310 | Password store initialized for Jason@zx2c4.com. 311 | 312 | .B zx2c4@laptop ~ $ pass git init 313 | .br 314 | Initialized empty Git repository in /home/zx2c4/.password-store/.git/ 315 | .br 316 | [master (root-commit) 998c8fd] Added current contents of password store. 317 | .br 318 | 1 file changed, 1 insertion(+) 319 | .br 320 | create mode 100644 .gpg-id 321 | 322 | .B zx2c4@laptop ~ $ pass git remote add origin kexec.com:pass-store 323 | 324 | .B zx2c4@laptop ~ $ pass generate Amazon/amazonemail@email.com 21 325 | .br 326 | mkdir: created directory \[u2018]/home/zx2c4/.password-store/Amazon\[u2019] 327 | .br 328 | [master 30fdc1e] Added generated password for Amazon/amazonemail@email.com to store. 329 | .br 330 | 1 file changed, 0 insertions(+), 0 deletions(-) 331 | .br 332 | create mode 100644 Amazon/amazonemail@email.com.gpg 333 | .br 334 | The generated password to Amazon/amazonemail@email.com is: 335 | .br 336 | <5m,_BrZY`antNDxKN<0A 337 | 338 | .B zx2c4@laptop ~ $ pass git push -u --all 339 | .br 340 | Counting objects: 4, done. 341 | .br 342 | Delta compression using up to 2 threads. 343 | .br 344 | Compressing objects: 100% (3/3), done. 345 | .br 346 | Writing objects: 100% (4/4), 921 bytes, done. 347 | .br 348 | Total 4 (delta 0), reused 0 (delta 0) 349 | .br 350 | To kexec.com:pass-store 351 | .br 352 | * [new branch] master -> master 353 | .br 354 | Branch master set up to track remote branch master from origin. 355 | 356 | .B zx2c4@laptop ~ $ pass insert Amazon/otheraccount@email.com 357 | .br 358 | Enter password for Amazon/otheraccount@email.com: som3r3a11yb1gp4ssw0rd!!88** 359 | .br 360 | [master b9b6746] Added given password for Amazon/otheraccount@email.com to store. 361 | .br 362 | 1 file changed, 0 insertions(+), 0 deletions(-) 363 | .br 364 | create mode 100644 Amazon/otheraccount@email.com.gpg 365 | 366 | .B zx2c4@laptop ~ $ pass rm Amazon/amazonemail@email.com 367 | .br 368 | rm: remove regular file \[u2018]/home/zx2c4/.password-store/Amazon/amazonemail@email.com.gpg\[u2019]? y 369 | .br 370 | removed \[u2018]/home/zx2c4/.password-store/Amazon/amazonemail@email.com.gpg\[u2019] 371 | .br 372 | rm 'Amazon/amazonemail@email.com.gpg' 373 | .br 374 | [master 288b379] Removed Amazon/amazonemail@email.com from store. 375 | .br 376 | 1 file changed, 0 insertions(+), 0 deletions(-) 377 | .br 378 | delete mode 100644 Amazon/amazonemail@email.com.gpg 379 | 380 | .B zx2c4@laptop ~ $ pass git push 381 | .br 382 | Counting objects: 9, done. 383 | .br 384 | Delta compression using up to 2 threads. 385 | .br 386 | Compressing objects: 100% (5/5), done. 387 | .br 388 | Writing objects: 100% (7/7), 1.25 KiB, done. 389 | .br 390 | Total 7 (delta 0), reused 0 (delta 0) 391 | .br 392 | To kexec.com:pass-store 393 | 394 | .SH FILES 395 | 396 | .TP 397 | .B ~/.password-store 398 | The default password storage directory. 399 | .TP 400 | .B ~/.password-store/.gpg-id 401 | Contains the default gpg key identification used for encryption and decryption. 402 | Multiple gpg keys may be specified in this file, one per line. If this file 403 | exists in any sub directories, passwords inside those sub directories are 404 | encrypted using those keys. This should be set using the \fBinit\fP command. 405 | .TP 406 | .B ~/.password-store/.extensions 407 | The directory containing extension files. 408 | 409 | .SH ENVIRONMENT VARIABLES 410 | 411 | .TP 412 | .I PASSWORD_STORE_DIR 413 | Overrides the default password storage directory. 414 | .TP 415 | .I PASSWORD_STORE_KEY 416 | Overrides the default gpg key identification set by \fBinit\fP. Keys must not 417 | contain spaces and thus use of the hexadecimal key signature is recommended. 418 | Multiple keys may be specified separated by spaces. 419 | .TP 420 | .I PASSWORD_STORE_GPG_OPTS 421 | Additional options to be passed to all invocations of GPG. 422 | .TP 423 | .I PASSWORD_STORE_X_SELECTION 424 | Overrides the selection passed to \fBxclip\fP, by default \fIclipboard\fP. See 425 | .BR xclip (1) 426 | for more info. 427 | .TP 428 | .I PASSWORD_STORE_CLIP_TIME 429 | Specifies the number of seconds to wait before restoring the clipboard, by default 430 | \fI45\fP seconds. 431 | .TP 432 | .I PASSWORD_STORE_UMASK 433 | Sets the umask of all files modified by pass, by default \fI077\fP. 434 | .TP 435 | .I PASSWORD_STORE_GENERATED_LENGTH 436 | The default password length if the \fIpass-length\fP parameter to \fBgenerate\fP 437 | is unspecified. 438 | .TP 439 | .I PASSWORD_STORE_CHARACTER_SET 440 | The character set to be used in password generation for \fBgenerate\fP. This value 441 | is to be interpreted by \fBtr\fP. See 442 | .BR tr (1) 443 | for more info. 444 | .TP 445 | .I PASSWORD_STORE_CHARACTER_SET_NO_SYMBOLS 446 | The character set to be used in no-symbol password generation for \fBgenerate\fP, 447 | when \fI--no-symbols\fP, \fI-n\fP is specified. This value is to be interpreted 448 | by \fBtr\fP. See 449 | .BR tr (1) 450 | for more info. 451 | .TP 452 | .I PASSWORD_STORE_ENABLE_EXTENSIONS 453 | This environment variable must be set to "true" for extensions to be enabled. 454 | .TP 455 | .I PASSWORD_STORE_EXTENSIONS_DIR 456 | The location to look for executable extension files, by default 457 | \fIPASSWORD_STORE_DIR/.extensions\fP. 458 | .TP 459 | .I PASSWORD_STORE_SIGNING_KEY 460 | If this environment variable is set, then all \fB.gpg-id\fP files and non-system extension files 461 | must be signed using a detached signature using the GPG key specified by the full 40 character 462 | upper-case fingerprint in this variable. If multiple fingerprints are specified, each 463 | separated by a whitespace character, then signatures must match at least one. 464 | The \fBinit\fP command will keep signatures of \fB.gpg-id\fP files up to date. 465 | .TP 466 | .I EDITOR 467 | The location of the text editor used by \fBedit\fP. 468 | .SH SEE ALSO 469 | .BR gpg2 (1), 470 | .BR tr (1), 471 | .BR git (1), 472 | .BR xclip (1), 473 | .BR wl-clipboard (1), 474 | .BR qrencode (1). 475 | 476 | .SH AUTHOR 477 | .B pass 478 | was written by 479 | .MT Jason@zx2c4.com 480 | Jason A. Donenfeld 481 | .ME . 482 | For updates and more information, a project page is available on the 483 | .UR http://\:www.passwordstore.org/ 484 | World Wide Web 485 | .UE . 486 | 487 | .SH COPYING 488 | This program is free software; you can redistribute it and/or 489 | modify it under the terms of the GNU General Public License 490 | as published by the Free Software Foundation; either version 2 491 | of the License, or (at your option) any later version. 492 | 493 | This program is distributed in the hope that it will be useful, 494 | but WITHOUT ANY WARRANTY; without even the implied warranty of 495 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 496 | GNU General Public License for more details. 497 | 498 | You should have received a copy of the GNU General Public License 499 | along with this program; if not, write to the Free Software 500 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 501 | -------------------------------------------------------------------------------- /src/completion/pass.bash-completion: -------------------------------------------------------------------------------- 1 | # completion file for bash 2 | 3 | # Copyright (C) 2012 - 2014 Jason A. Donenfeld and 4 | # Brian Mattern . All Rights Reserved. 5 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 6 | 7 | _pass_complete_entries () { 8 | local prefix="${PASSWORD_STORE_DIR:-$HOME/.password-store/}" 9 | prefix="${prefix%/}/" 10 | local suffix=".gpg" 11 | local autoexpand=${1:-0} 12 | 13 | local IFS=$'\n' 14 | local items=($(compgen -f $prefix$cur)) 15 | 16 | # Remember the value of the first item, to see if it is a directory. If 17 | # it is a directory, then don't add a space to the completion 18 | local firstitem="" 19 | # Use counter, can't use ${#items[@]} as we skip hidden directories 20 | local i=0 item 21 | 22 | for item in ${items[@]}; do 23 | [[ $item =~ /\.[^/]*$ ]] && continue 24 | 25 | # if there is a unique match, and it is a directory with one entry 26 | # autocomplete the subentry as well (recursively) 27 | if [[ ${#items[@]} -eq 1 && $autoexpand -eq 1 ]]; then 28 | while [[ -d $item ]]; do 29 | local subitems=($(compgen -f "$item/")) 30 | local filtereditems=( ) item2 31 | for item2 in "${subitems[@]}"; do 32 | [[ $item2 =~ /\.[^/]*$ ]] && continue 33 | filtereditems+=( "$item2" ) 34 | done 35 | if [[ ${#filtereditems[@]} -eq 1 ]]; then 36 | item="${filtereditems[0]}" 37 | else 38 | break 39 | fi 40 | done 41 | fi 42 | 43 | # append / to directories 44 | [[ -d $item ]] && item="$item/" 45 | 46 | item="${item%$suffix}" 47 | COMPREPLY+=("${item#$prefix}") 48 | if [[ $i -eq 0 ]]; then 49 | firstitem=$item 50 | fi 51 | let i+=1 52 | done 53 | 54 | # The only time we want to add a space to the end is if there is only 55 | # one match, and it is not a directory 56 | if [[ $i -gt 1 || ( $i -eq 1 && -d $firstitem ) ]]; then 57 | compopt -o nospace 58 | fi 59 | } 60 | 61 | _pass_complete_folders () { 62 | local prefix="${PASSWORD_STORE_DIR:-$HOME/.password-store/}" 63 | prefix="${prefix%/}/" 64 | 65 | local IFS=$'\n' 66 | local items=($(compgen -d $prefix$cur)) 67 | for item in ${items[@]}; do 68 | [[ $item == $prefix.* ]] && continue 69 | COMPREPLY+=("${item#$prefix}/") 70 | done 71 | } 72 | 73 | _pass_complete_keys () { 74 | local GPG="gpg" 75 | command -v gpg2 &>/dev/null && GPG="gpg2" 76 | 77 | local IFS=$'\n' 78 | # Extract names and email addresses from gpg --list-keys 79 | local keys="$($GPG --list-secret-keys --with-colons | cut -d : -f 10 | sort -u | sed '/^$/d')" 80 | COMPREPLY+=($(compgen -W "${keys}" -- ${cur})) 81 | } 82 | 83 | _pass() 84 | { 85 | COMPREPLY=() 86 | local cur="${COMP_WORDS[COMP_CWORD]}" 87 | local commands="init ls find grep show insert generate edit rm mv cp git help version ${PASSWORD_STORE_EXTENSION_COMMANDS[*]}" 88 | if [[ $COMP_CWORD -gt 1 ]]; then 89 | local lastarg="${COMP_WORDS[$COMP_CWORD-1]}" 90 | case "${COMP_WORDS[1]}" in 91 | init) 92 | if [[ $lastarg == "-p" || $lastarg == "--path" ]]; then 93 | _pass_complete_folders 94 | compopt -o nospace 95 | else 96 | COMPREPLY+=($(compgen -W "-p --path" -- ${cur})) 97 | _pass_complete_keys 98 | fi 99 | ;; 100 | ls|list|edit) 101 | _pass_complete_entries 102 | ;; 103 | show|-*) 104 | COMPREPLY+=($(compgen -W "-c --clip" -- ${cur})) 105 | _pass_complete_entries 1 106 | ;; 107 | insert) 108 | COMPREPLY+=($(compgen -W "-e --echo -m --multiline -f --force" -- ${cur})) 109 | _pass_complete_entries 110 | ;; 111 | generate) 112 | COMPREPLY+=($(compgen -W "-n --no-symbols -c --clip -f --force -i --in-place" -- ${cur})) 113 | _pass_complete_entries 114 | ;; 115 | cp|copy|mv|rename) 116 | COMPREPLY+=($(compgen -W "-f --force" -- ${cur})) 117 | _pass_complete_entries 118 | ;; 119 | rm|remove|delete) 120 | COMPREPLY+=($(compgen -W "-r --recursive -f --force" -- ${cur})) 121 | _pass_complete_entries 122 | ;; 123 | git) 124 | COMPREPLY+=($(compgen -W "init push pull config log reflog rebase" -- ${cur})) 125 | ;; 126 | esac 127 | 128 | # To add completion for an extension command define a function like this: 129 | # __password_store_extension_complete_() { 130 | # COMPREPLY+=($(compgen -W "-o --option" -- ${cur})) 131 | # _pass_complete_entries 1 132 | # } 133 | # 134 | # and add the command to the $PASSWORD_STORE_EXTENSION_COMMANDS array 135 | if [[ " ${PASSWORD_STORE_EXTENSION_COMMANDS[*]} " == *" ${COMP_WORDS[1]} "* ]] && type "__password_store_extension_complete_${COMP_WORDS[1]}" &> /dev/null; then 136 | "__password_store_extension_complete_${COMP_WORDS[1]}" 137 | fi 138 | else 139 | COMPREPLY+=($(compgen -W "${commands}" -- ${cur})) 140 | _pass_complete_entries 1 141 | fi 142 | } 143 | 144 | complete -o filenames -F _pass pass 145 | -------------------------------------------------------------------------------- /src/completion/pass.fish-completion: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012-2014 Dmitry Medvinsky . All Rights Reserved. 2 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 3 | 4 | set -l PROG 'pass' 5 | 6 | function __fish_pass_get_prefix 7 | if set -q PASSWORD_STORE_DIR 8 | realpath -- "$PASSWORD_STORE_DIR" 9 | else 10 | echo "$HOME/.password-store" 11 | end 12 | end 13 | 14 | function __fish_pass_needs_command 15 | [ (count (commandline -opc)) -eq 1 ] 16 | end 17 | 18 | function __fish_pass_uses_command 19 | set -l cmd (commandline -opc) 20 | if [ (count $cmd) -gt 1 ] 21 | if [ $argv[1] = $cmd[2] ] 22 | return 0 23 | end 24 | end 25 | return 1 26 | end 27 | 28 | function __fish_pass_print_gpg_keys 29 | gpg2 --list-keys | grep uid | sed 's/.*<\(.*\)>/\1/' 30 | end 31 | 32 | function __fish_pass_print 33 | set -l ext $argv[1] 34 | set -l strip $argv[2] 35 | set -l prefix (__fish_pass_get_prefix) 36 | set -l matches $prefix/**$ext 37 | printf '%s\n' $matches | sed "s#$prefix/\(.*\)$strip#\1#" 38 | end 39 | 40 | function __fish_pass_print_entry_dirs 41 | __fish_pass_print "/" 42 | end 43 | 44 | function __fish_pass_print_entries 45 | __fish_pass_print ".gpg" ".gpg" 46 | end 47 | 48 | function __fish_pass_print_entries_and_dirs 49 | __fish_pass_print_entry_dirs 50 | __fish_pass_print_entries 51 | end 52 | 53 | function __fish_pass_git_complete 54 | set -l prefix (__fish_pass_get_prefix) 55 | set -l git_cmd (commandline -opc) (commandline -ct) 56 | set -e git_cmd[1 2] # Drop "pass git". 57 | complete -C"git -C $prefix $git_cmd" 58 | end 59 | 60 | complete -c $PROG -f -n '__fish_pass_needs_command' -a help -d 'Command: show usage help' 61 | complete -c $PROG -f -n '__fish_pass_needs_command' -a version -d 'Command: show program version' 62 | 63 | complete -c $PROG -f -n '__fish_pass_needs_command' -a init -d 'Command: initialize new password storage' 64 | complete -c $PROG -f -n '__fish_pass_uses_command init' -s p -l path -d 'Assign gpg-id for specified sub folder of password store' 65 | 66 | complete -c $PROG -f -n '__fish_pass_needs_command' -a ls -d 'Command: list passwords' 67 | complete -c $PROG -f -n '__fish_pass_uses_command ls' -a "(__fish_pass_print_entry_dirs)" 68 | 69 | complete -c $PROG -f -n '__fish_pass_needs_command' -a insert -d 'Command: insert new password' 70 | complete -c $PROG -f -n '__fish_pass_uses_command insert' -s e -l echo -d 'Echo the password on console' 71 | complete -c $PROG -f -n '__fish_pass_uses_command insert' -s m -l multiline -d 'Provide multiline password entry' 72 | complete -c $PROG -f -n '__fish_pass_uses_command insert' -s f -l force -d 'Do not prompt before overwritting' 73 | complete -c $PROG -f -n '__fish_pass_uses_command insert' -a "(__fish_pass_print_entry_dirs)" 74 | 75 | complete -c $PROG -f -n '__fish_pass_needs_command' -a generate -d 'Command: generate new password' 76 | complete -c $PROG -f -n '__fish_pass_uses_command generate' -s n -l no-symbols -d 'Do not use special symbols' 77 | complete -c $PROG -f -n '__fish_pass_uses_command generate' -s c -l clip -d 'Put the password in clipboard' 78 | complete -c $PROG -f -n '__fish_pass_uses_command generate' -s f -l force -d 'Do not prompt before overwritting' 79 | complete -c $PROG -f -n '__fish_pass_uses_command generate' -s i -l in-place -d 'Replace only the first line with the generated password' 80 | complete -c $PROG -f -n '__fish_pass_uses_command generate' -a "(__fish_pass_print_entry_dirs)" 81 | 82 | complete -c $PROG -f -n '__fish_pass_needs_command' -a mv -d 'Command: rename existing password' 83 | complete -c $PROG -f -n '__fish_pass_uses_command mv' -s f -l force -d 'Force rename' 84 | complete -c $PROG -f -n '__fish_pass_uses_command mv' -a "(__fish_pass_print_entries_and_dirs)" 85 | 86 | complete -c $PROG -f -n '__fish_pass_needs_command' -a cp -d 'Command: copy existing password' 87 | complete -c $PROG -f -n '__fish_pass_uses_command cp' -s f -l force -d 'Force copy' 88 | complete -c $PROG -f -n '__fish_pass_uses_command cp' -a "(__fish_pass_print_entries_and_dirs)" 89 | 90 | complete -c $PROG -f -n '__fish_pass_needs_command' -a rm -d 'Command: remove existing password' 91 | complete -c $PROG -f -n '__fish_pass_uses_command rm' -s r -l recursive -d 'Remove password groups recursively' 92 | complete -c $PROG -f -n '__fish_pass_uses_command rm' -s f -l force -d 'Force removal' 93 | complete -c $PROG -f -n '__fish_pass_uses_command rm' -a "(__fish_pass_print_entries_and_dirs)" 94 | 95 | complete -c $PROG -f -n '__fish_pass_needs_command' -a edit -d 'Command: edit password using text editor' 96 | complete -c $PROG -f -n '__fish_pass_uses_command edit' -a "(__fish_pass_print_entries)" 97 | 98 | complete -c $PROG -f -n '__fish_pass_needs_command' -a show -d 'Command: show existing password' 99 | complete -c $PROG -f -n '__fish_pass_uses_command show' -s c -l clip -d 'Put password in clipboard' 100 | complete -c $PROG -f -n '__fish_pass_uses_command show' -a "(__fish_pass_print_entries)" 101 | # When no command is given, `show` is defaulted. 102 | complete -c $PROG -f -n '__fish_pass_needs_command' -s c -l clip -d 'Put password in clipboard' 103 | complete -c $PROG -f -n '__fish_pass_needs_command' -a "(__fish_pass_print_entries)" 104 | complete -c $PROG -f -n '__fish_pass_uses_command -c' -a "(__fish_pass_print_entries)" 105 | complete -c $PROG -f -n '__fish_pass_uses_command --clip' -a "(__fish_pass_print_entries)" 106 | 107 | complete -c $PROG -f -n '__fish_pass_needs_command' -a git -d 'Command: execute a git command' 108 | complete -c $PROG -f -n '__fish_pass_uses_command git' -a '(__fish_pass_git_complete)' 109 | complete -c $PROG -f -n '__fish_pass_needs_command' -a find -d 'Command: find a password file or directory matching pattern' 110 | complete -c $PROG -f -n '__fish_pass_needs_command' -a grep -d 'Command: search inside of decrypted password files for matching pattern' 111 | complete -c $PROG -f -n '__fish_pass_uses_command grep' -a '(begin 112 | set -l cmd (commandline -opc) (commandline -ct) 113 | set -e cmd[1 2] # Drop "pass grep". 114 | complete -C"grep $cmd" 115 | end)' 116 | -------------------------------------------------------------------------------- /src/completion/pass.zsh-completion: -------------------------------------------------------------------------------- 1 | #compdef pass 2 | #autoload 3 | 4 | # Copyright (C) 2012 - 2014: 5 | # Johan Venant 6 | # Brian Mattern 7 | # Jason A. Donenfeld . 8 | # All Rights Reserved. 9 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 10 | 11 | 12 | # If you use multiple repositories, you can configure completion like this: 13 | # 14 | # compdef _pass workpass 15 | # zstyle ':completion::complete:workpass::' prefix "$HOME/work/pass" 16 | # workpass() { 17 | # PASSWORD_STORE_DIR=$HOME/work/pass pass $@ 18 | # } 19 | 20 | 21 | _pass () { 22 | local cmd 23 | if (( CURRENT > 2)); then 24 | cmd=${words[2]} 25 | # Set the context for the subcommand. 26 | curcontext="${curcontext%:*:*}:pass-$cmd" 27 | # Narrow the range of words we are looking at to exclude `pass' 28 | (( CURRENT-- )) 29 | shift words 30 | # Run the completion for the subcommand 31 | case "${cmd}" in 32 | init) 33 | _arguments : \ 34 | "-p[gpg-id will only be applied to this subfolder]" \ 35 | "--path[gpg-id will only be applied to this subfolder]" 36 | _pass_complete_keys 37 | ;; 38 | ls|list|edit) 39 | _pass_complete_entries_with_subdirs 40 | ;; 41 | insert) 42 | _arguments : \ 43 | "-e[echo password to console]" \ 44 | "--echo[echo password to console]" \ 45 | "-m[multiline]" \ 46 | "--multiline[multiline]" 47 | _pass_complete_entries_with_subdirs 48 | ;; 49 | generate) 50 | _arguments : \ 51 | "-n[don't include symbols in password]" \ 52 | "--no-symbols[don't include symbols in password]" \ 53 | "-c[copy password to the clipboard]" \ 54 | "--clip[copy password to the clipboard]" \ 55 | "-f[force overwrite]" \ 56 | "--force[force overwrite]" \ 57 | "-i[replace first line]" \ 58 | "--in-place[replace first line]" 59 | _pass_complete_entries_with_subdirs 60 | ;; 61 | cp|copy|mv|rename) 62 | _arguments : \ 63 | "-f[force rename]" \ 64 | "--force[force rename]" 65 | _pass_complete_entries_with_subdirs 66 | ;; 67 | rm) 68 | _arguments : \ 69 | "-f[force deletion]" \ 70 | "--force[force deletion]" \ 71 | "-r[recursively delete]" \ 72 | "--recursive[recursively delete]" 73 | _pass_complete_entries_with_subdirs 74 | ;; 75 | git) 76 | local -a subcommands 77 | subcommands=( 78 | "init:Initialize git repository" 79 | "push:Push to remote repository" 80 | "pull:Pull from remote repository" 81 | "config:Show git config" 82 | "log:Show git log" 83 | "reflog:Show git reflog" 84 | ) 85 | _describe -t commands 'pass git' subcommands 86 | ;; 87 | show|*) 88 | _pass_cmd_show 89 | ;; 90 | esac 91 | else 92 | local -a subcommands 93 | subcommands=( 94 | "init:Initialize new password storage" 95 | "ls:List passwords" 96 | "find:Find password files or directories based on pattern" 97 | "grep:Search inside decrypted password files for matching pattern" 98 | "show:Decrypt and print a password" 99 | "insert:Insert a new password" 100 | "generate:Generate a new password using pwgen" 101 | "edit:Edit a password with \$EDITOR" 102 | "mv:Rename the password" 103 | "cp:Copy the password" 104 | "rm:Remove the password" 105 | "git:Call git on the password store" 106 | "version:Output version information" 107 | "help:Output help message" 108 | ) 109 | _describe -t commands 'pass' subcommands 110 | _arguments : \ 111 | "--version[Output version information]" \ 112 | "--help[Output help message]" 113 | _pass_cmd_show 114 | fi 115 | } 116 | 117 | _pass_cmd_show () { 118 | _arguments : \ 119 | "-c[put it on the clipboard]" \ 120 | "--clip[put it on the clipboard]" 121 | _pass_complete_entries 122 | } 123 | _pass_complete_entries_helper () { 124 | local IFS=$'\n' 125 | local prefix 126 | zstyle -s ":completion:${curcontext}:" prefix prefix || prefix="${PASSWORD_STORE_DIR:-$HOME/.password-store}" 127 | _values -C 'passwords' ${$(find -L "$prefix" \( -name .git -o -name .gpg-id \) -prune -o $@ -print 2>/dev/null | sed -e "s#${prefix}/\{0,1\}##" -e 's#\.gpg##' -e 's#\\#\\\\#g' -e 's#:#\\:#g' | sort):-""} 128 | } 129 | 130 | _pass_complete_entries_with_subdirs () { 131 | _pass_complete_entries_helper 132 | } 133 | 134 | _pass_complete_entries () { 135 | _pass_complete_entries_helper -type f 136 | } 137 | 138 | _pass_complete_keys () { 139 | local IFS=$'\n' 140 | # Extract names and email addresses from gpg --list-keys 141 | _values 'gpg keys' $(gpg2 --list-secret-keys --with-colons | cut -d : -f 10 | sort -u | sed '/^$/d') 142 | } 143 | 144 | _pass 145 | -------------------------------------------------------------------------------- /src/password-store.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright (C) 2012 - 2018 Jason A. Donenfeld . All Rights Reserved. 4 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 5 | 6 | umask "${PASSWORD_STORE_UMASK:-077}" 7 | set -o pipefail 8 | 9 | GPG_OPTS=( $PASSWORD_STORE_GPG_OPTS "--quiet" "--yes" "--compress-algo=none" "--no-encrypt-to" ) 10 | GPG="gpg" 11 | export GPG_TTY="${GPG_TTY:-$(tty 2>/dev/null)}" 12 | command -v gpg2 &>/dev/null && GPG="gpg2" 13 | [[ -n $GPG_AGENT_INFO || $GPG == "gpg2" ]] && GPG_OPTS+=( "--batch" "--use-agent" ) 14 | 15 | PREFIX="${PASSWORD_STORE_DIR:-$HOME/.password-store}" 16 | EXTENSIONS="${PASSWORD_STORE_EXTENSIONS_DIR:-$PREFIX/.extensions}" 17 | X_SELECTION="${PASSWORD_STORE_X_SELECTION:-clipboard}" 18 | CLIP_TIME="${PASSWORD_STORE_CLIP_TIME:-45}" 19 | GENERATED_LENGTH="${PASSWORD_STORE_GENERATED_LENGTH:-25}" 20 | CHARACTER_SET="${PASSWORD_STORE_CHARACTER_SET:-[:punct:][:alnum:]}" 21 | CHARACTER_SET_NO_SYMBOLS="${PASSWORD_STORE_CHARACTER_SET_NO_SYMBOLS:-[:alnum:]}" 22 | 23 | unset GIT_DIR GIT_WORK_TREE GIT_NAMESPACE GIT_INDEX_FILE GIT_INDEX_VERSION GIT_OBJECT_DIRECTORY GIT_COMMON_DIR 24 | export GIT_CEILING_DIRECTORIES="$PREFIX/.." 25 | 26 | # 27 | # BEGIN helper functions 28 | # 29 | 30 | set_git() { 31 | INNER_GIT_DIR="${1%/*}" 32 | while [[ ! -d $INNER_GIT_DIR && ${INNER_GIT_DIR%/*}/ == "${PREFIX%/}/"* ]]; do 33 | INNER_GIT_DIR="${INNER_GIT_DIR%/*}" 34 | done 35 | [[ $(git -C "$INNER_GIT_DIR" rev-parse --is-inside-work-tree 2>/dev/null) == true ]] || INNER_GIT_DIR="" 36 | } 37 | git_add_file() { 38 | [[ -n $INNER_GIT_DIR ]] || return 39 | git -C "$INNER_GIT_DIR" add "$1" || return 40 | [[ -n $(git -C "$INNER_GIT_DIR" status --porcelain "$1") ]] || return 41 | git_commit "$2" 42 | } 43 | git_commit() { 44 | local sign="" 45 | [[ -n $INNER_GIT_DIR ]] || return 46 | [[ $(git -C "$INNER_GIT_DIR" config --bool --get pass.signcommits) == "true" ]] && sign="-S" 47 | git -C "$INNER_GIT_DIR" commit $sign -m "$1" 48 | } 49 | yesno() { 50 | [[ -t 0 ]] || return 0 51 | local response 52 | read -r -p "$1 [y/N] " response 53 | [[ $response == [yY] ]] || exit 1 54 | } 55 | die() { 56 | echo "$@" >&2 57 | exit 1 58 | } 59 | verify_file() { 60 | [[ -n $PASSWORD_STORE_SIGNING_KEY ]] || return 0 61 | [[ -f $1.sig ]] || die "Signature for $1 does not exist." 62 | local fingerprints="$($GPG $PASSWORD_STORE_GPG_OPTS --verify --status-fd=1 "$1.sig" "$1" 2>/dev/null | sed -n 's/^\[GNUPG:\] VALIDSIG \([A-F0-9]\{40\}\) .* \([A-F0-9]\{40\}\)$/\1\n\2/p')" 63 | local fingerprint found=0 64 | for fingerprint in $PASSWORD_STORE_SIGNING_KEY; do 65 | [[ $fingerprint =~ ^[A-F0-9]{40}$ ]] || continue 66 | [[ $fingerprints == *$fingerprint* ]] && { found=1; break; } 67 | done 68 | [[ $found -eq 1 ]] || die "Signature for $1 is invalid." 69 | } 70 | set_gpg_recipients() { 71 | GPG_RECIPIENT_ARGS=( ) 72 | GPG_RECIPIENTS=( ) 73 | local gpg_id 74 | 75 | if [[ -n $PASSWORD_STORE_KEY ]]; then 76 | for gpg_id in $PASSWORD_STORE_KEY; do 77 | GPG_RECIPIENT_ARGS+=( "-r" "$gpg_id" ) 78 | GPG_RECIPIENTS+=( "$gpg_id" ) 79 | done 80 | return 81 | fi 82 | 83 | local current="$PREFIX/$1" 84 | while [[ $current != "$PREFIX" && ! -f $current/.gpg-id ]]; do 85 | current="${current%/*}" 86 | done 87 | current="$current/.gpg-id" 88 | 89 | if [[ ! -f $current ]]; then 90 | cat >&2 <<-_EOF 91 | Error: You must run: 92 | $PROGRAM init your-gpg-id 93 | before you may use the password store. 94 | 95 | _EOF 96 | cmd_usage 97 | exit 1 98 | fi 99 | 100 | verify_file "$current" 101 | 102 | while read -r gpg_id; do 103 | gpg_id="${gpg_id%%#*}" # strip comment 104 | [[ -n $gpg_id ]] || continue 105 | GPG_RECIPIENT_ARGS+=( "-r" "$gpg_id" ) 106 | GPG_RECIPIENTS+=( "$gpg_id" ) 107 | done < "$current" 108 | } 109 | 110 | reencrypt_path() { 111 | local prev_gpg_recipients="" gpg_keys="" current_keys="" index passfile 112 | local groups="$($GPG $PASSWORD_STORE_GPG_OPTS --list-config --with-colons | grep "^cfg:group:.*")" 113 | while read -r -d "" passfile; do 114 | [[ -L $passfile ]] && continue 115 | local passfile_dir="${passfile%/*}" 116 | passfile_dir="${passfile_dir#$PREFIX}" 117 | passfile_dir="${passfile_dir#/}" 118 | local passfile_display="${passfile#$PREFIX/}" 119 | passfile_display="${passfile_display%.gpg}" 120 | local passfile_temp="${passfile}.tmp.${RANDOM}.${RANDOM}.${RANDOM}.${RANDOM}.--" 121 | 122 | set_gpg_recipients "$passfile_dir" 123 | if [[ $prev_gpg_recipients != "${GPG_RECIPIENTS[*]}" ]]; then 124 | for index in "${!GPG_RECIPIENTS[@]}"; do 125 | local group="$(sed -n "s/^cfg:group:$(sed 's/[\/&]/\\&/g' <<<"${GPG_RECIPIENTS[$index]}"):\\(.*\\)\$/\\1/p" <<<"$groups" | head -n 1)" 126 | [[ -z $group ]] && continue 127 | IFS=";" eval 'GPG_RECIPIENTS+=( $group )' # http://unix.stackexchange.com/a/92190 128 | unset "GPG_RECIPIENTS[$index]" 129 | done 130 | gpg_keys="$($GPG $PASSWORD_STORE_GPG_OPTS --list-keys --with-colons "${GPG_RECIPIENTS[@]}" | sed -n 's/^sub:[^idr:]*:[^:]*:[^:]*:\([^:]*\):[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[a-zA-Z]*e[a-zA-Z]*:.*/\1/p' | LC_ALL=C sort -u)" 131 | fi 132 | current_keys="$(LC_ALL=C $GPG $PASSWORD_STORE_GPG_OPTS -v --no-secmem-warning --no-permission-warning --decrypt --list-only --keyid-format long "$passfile" 2>&1 | sed -nE 's/^gpg: public key is ([A-F0-9]+)$/\1/p' | LC_ALL=C sort -u)" 133 | 134 | if [[ $gpg_keys != "$current_keys" ]]; then 135 | echo "$passfile_display: reencrypting to ${gpg_keys//$'\n'/ }" 136 | $GPG -d "${GPG_OPTS[@]}" "$passfile" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile_temp" "${GPG_OPTS[@]}" && 137 | mv "$passfile_temp" "$passfile" || rm -f "$passfile_temp" 138 | fi 139 | prev_gpg_recipients="${GPG_RECIPIENTS[*]}" 140 | done < <(find "$1" -path '*/.git' -prune -o -path '*/.extensions' -prune -o -iname '*.gpg' -print0) 141 | } 142 | check_sneaky_paths() { 143 | local path 144 | for path in "$@"; do 145 | [[ $path =~ /\.\.$ || $path =~ ^\.\./ || $path =~ /\.\./ || $path =~ ^\.\.$ ]] && die "Error: You've attempted to pass a sneaky path to pass. Go home." 146 | done 147 | } 148 | 149 | # 150 | # END helper functions 151 | # 152 | 153 | # 154 | # BEGIN platform definable 155 | # 156 | 157 | clip() { 158 | if [[ -n $WAYLAND_DISPLAY ]] && command -v wl-copy &> /dev/null; then 159 | local copy_cmd=( wl-copy ) 160 | local paste_cmd=( wl-paste -n ) 161 | if [[ $X_SELECTION == primary ]]; then 162 | copy_cmd+=( --primary ) 163 | paste_cmd+=( --primary ) 164 | fi 165 | local display_name="$WAYLAND_DISPLAY" 166 | elif [[ -n $DISPLAY ]] && command -v xclip &> /dev/null; then 167 | local copy_cmd=( xclip -selection "$X_SELECTION" ) 168 | local paste_cmd=( xclip -o -selection "$X_SELECTION" ) 169 | local display_name="$DISPLAY" 170 | else 171 | die "Error: No X11 or Wayland display and clipper detected" 172 | fi 173 | local sleep_argv0="password store sleep on display $display_name" 174 | 175 | # This base64 business is because bash cannot store binary data in a shell 176 | # variable. Specifically, it cannot store nulls nor (non-trivally) store 177 | # trailing new lines. 178 | pkill -f "^$sleep_argv0" 2>/dev/null && sleep 0.5 179 | local before="$("${paste_cmd[@]}" 2>/dev/null | $BASE64)" 180 | echo -n "$1" | "${copy_cmd[@]}" || die "Error: Could not copy data to the clipboard" 181 | ( 182 | ( exec -a "$sleep_argv0" bash <<<"trap 'kill %1' TERM; sleep '$CLIP_TIME' & wait" ) 183 | local now="$("${paste_cmd[@]}" | $BASE64)" 184 | [[ $now != $(echo -n "$1" | $BASE64) ]] && before="$now" 185 | 186 | # It might be nice to programatically check to see if klipper exists, 187 | # as well as checking for other common clipboard managers. But for now, 188 | # this works fine -- if qdbus isn't there or if klipper isn't running, 189 | # this essentially becomes a no-op. 190 | # 191 | # Clipboard managers frequently write their history out in plaintext, 192 | # so we axe it here: 193 | qdbus org.kde.klipper /klipper org.kde.klipper.klipper.clearClipboardHistory &>/dev/null 194 | 195 | echo "$before" | $BASE64 -d | "${copy_cmd[@]}" 196 | ) >/dev/null 2>&1 & disown 197 | echo "Copied $2 to clipboard. Will clear in $CLIP_TIME seconds." 198 | } 199 | 200 | qrcode() { 201 | if [[ -n $DISPLAY || -n $WAYLAND_DISPLAY ]]; then 202 | if type feh >/dev/null 2>&1; then 203 | echo -n "$1" | qrencode --size 10 -o - | feh -x --title "pass: $2" -g +200+200 - 204 | return 205 | elif type gm >/dev/null 2>&1; then 206 | echo -n "$1" | qrencode --size 10 -o - | gm display -title "pass: $2" -geometry +200+200 - 207 | return 208 | elif type display >/dev/null 2>&1; then 209 | echo -n "$1" | qrencode --size 10 -o - | display -title "pass: $2" -geometry +200+200 - 210 | return 211 | fi 212 | fi 213 | echo -n "$1" | qrencode -t utf8 214 | } 215 | 216 | tmpdir() { 217 | [[ -n $SECURE_TMPDIR ]] && return 218 | local warn=1 219 | [[ $1 == "nowarn" ]] && warn=0 220 | local template="$PROGRAM.XXXXXXXXXXXXX" 221 | if [[ -d /dev/shm && -w /dev/shm && -x /dev/shm ]]; then 222 | SECURE_TMPDIR="$(mktemp -d "/dev/shm/$template")" 223 | remove_tmpfile() { 224 | rm -rf "$SECURE_TMPDIR" 225 | } 226 | trap remove_tmpfile EXIT 227 | else 228 | [[ $warn -eq 1 ]] && yesno "$(cat <<-_EOF 229 | Your system does not have /dev/shm, which means that it may 230 | be difficult to entirely erase the temporary non-encrypted 231 | password file after editing. 232 | 233 | Are you sure you would like to continue? 234 | _EOF 235 | )" 236 | SECURE_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/$template")" 237 | shred_tmpfile() { 238 | find "$SECURE_TMPDIR" -type f -exec $SHRED {} + 239 | rm -rf "$SECURE_TMPDIR" 240 | } 241 | trap shred_tmpfile EXIT 242 | fi 243 | 244 | } 245 | GETOPT="getopt" 246 | SHRED="shred -f -z" 247 | BASE64="base64" 248 | 249 | source "$(dirname "$0")/platform/$(uname | cut -d _ -f 1 | tr '[:upper:]' '[:lower:]').sh" 2>/dev/null # PLATFORM_FUNCTION_FILE 250 | 251 | # 252 | # END platform definable 253 | # 254 | 255 | 256 | # 257 | # BEGIN subcommand functions 258 | # 259 | 260 | cmd_version() { 261 | cat <<-_EOF 262 | ============================================ 263 | = pass: the standard unix password manager = 264 | = = 265 | = v1.7.4 = 266 | = = 267 | = Jason A. Donenfeld = 268 | = Jason@zx2c4.com = 269 | = = 270 | = http://www.passwordstore.org/ = 271 | ============================================ 272 | _EOF 273 | } 274 | 275 | cmd_usage() { 276 | cmd_version 277 | echo 278 | cat <<-_EOF 279 | Usage: 280 | $PROGRAM init [--path=subfolder,-p subfolder] gpg-id... 281 | Initialize new password storage and use gpg-id for encryption. 282 | Selectively reencrypt existing passwords using new gpg-id. 283 | $PROGRAM [ls] [subfolder] 284 | List passwords. 285 | $PROGRAM find pass-names... 286 | List passwords that match pass-names. 287 | $PROGRAM [show] [--clip[=line-number],-c[line-number]] pass-name 288 | Show existing password and optionally put it on the clipboard. 289 | If put on the clipboard, it will be cleared in $CLIP_TIME seconds. 290 | $PROGRAM grep [GREPOPTIONS] search-string 291 | Search for password files containing search-string when decrypted. 292 | $PROGRAM insert [--echo,-e | --multiline,-m] [--force,-f] pass-name 293 | Insert new password. Optionally, echo the password back to the console 294 | during entry. Or, optionally, the entry may be multiline. Prompt before 295 | overwriting existing password unless forced. 296 | $PROGRAM edit pass-name 297 | Insert a new password or edit an existing password using ${EDITOR:-vi}. 298 | $PROGRAM generate [--no-symbols,-n] [--clip,-c] [--in-place,-i | --force,-f] pass-name [pass-length] 299 | Generate a new password of pass-length (or $GENERATED_LENGTH if unspecified) with optionally no symbols. 300 | Optionally put it on the clipboard and clear board after $CLIP_TIME seconds. 301 | Prompt before overwriting existing password unless forced. 302 | Optionally replace only the first line of an existing file with a new password. 303 | $PROGRAM rm [--recursive,-r] [--force,-f] pass-name 304 | Remove existing password or directory, optionally forcefully. 305 | $PROGRAM mv [--force,-f] old-path new-path 306 | Renames or moves old-path to new-path, optionally forcefully, selectively reencrypting. 307 | $PROGRAM cp [--force,-f] old-path new-path 308 | Copies old-path to new-path, optionally forcefully, selectively reencrypting. 309 | $PROGRAM git git-command-args... 310 | If the password store is a git repository, execute a git command 311 | specified by git-command-args. 312 | $PROGRAM help 313 | Show this text. 314 | $PROGRAM version 315 | Show version information. 316 | 317 | More information may be found in the pass(1) man page. 318 | _EOF 319 | } 320 | 321 | cmd_init() { 322 | local opts id_path="" 323 | opts="$($GETOPT -o p: -l path: -n "$PROGRAM" -- "$@")" 324 | local err=$? 325 | eval set -- "$opts" 326 | while true; do case $1 in 327 | -p|--path) id_path="$2"; shift 2 ;; 328 | --) shift; break ;; 329 | esac done 330 | 331 | [[ $err -ne 0 || $# -lt 1 ]] && die "Usage: $PROGRAM $COMMAND [--path=subfolder,-p subfolder] gpg-id..." 332 | [[ -n $id_path ]] && check_sneaky_paths "$id_path" 333 | [[ -n $id_path && ! -d $PREFIX/$id_path && -e $PREFIX/$id_path ]] && die "Error: $PREFIX/$id_path exists but is not a directory." 334 | 335 | local gpg_id="$PREFIX/$id_path/.gpg-id" 336 | set_git "$gpg_id" 337 | 338 | if [[ $# -eq 1 && -z $1 ]]; then 339 | [[ ! -f "$gpg_id" ]] && die "Error: $gpg_id does not exist and so cannot be removed." 340 | rm -v -f "$gpg_id" || exit 1 341 | if [[ -n $INNER_GIT_DIR ]]; then 342 | git -C "$INNER_GIT_DIR" rm -qr "$gpg_id" 343 | git_commit "Deinitialize ${gpg_id}${id_path:+ ($id_path)}." 344 | fi 345 | rmdir -p "${gpg_id%/*}" 2>/dev/null 346 | else 347 | mkdir -v -p "$PREFIX/$id_path" 348 | printf "%s\n" "$@" > "$gpg_id" 349 | local id_print="$(printf "%s, " "$@")" 350 | echo "Password store initialized for ${id_print%, }${id_path:+ ($id_path)}" 351 | git_add_file "$gpg_id" "Set GPG id to ${id_print%, }${id_path:+ ($id_path)}." 352 | if [[ -n $PASSWORD_STORE_SIGNING_KEY ]]; then 353 | local signing_keys=( ) key 354 | for key in $PASSWORD_STORE_SIGNING_KEY; do 355 | signing_keys+=( --default-key $key ) 356 | done 357 | $GPG "${GPG_OPTS[@]}" "${signing_keys[@]}" --detach-sign "$gpg_id" || die "Could not sign .gpg_id." 358 | key="$($GPG "${GPG_OPTS[@]}" --verify --status-fd=1 "$gpg_id.sig" "$gpg_id" 2>/dev/null | sed -n 's/^\[GNUPG:\] VALIDSIG [A-F0-9]\{40\} .* \([A-F0-9]\{40\}\)$/\1/p')" 359 | [[ -n $key ]] || die "Signing of .gpg_id unsuccessful." 360 | git_add_file "$gpg_id.sig" "Signing new GPG id with ${key//[$IFS]/,}." 361 | fi 362 | fi 363 | 364 | reencrypt_path "$PREFIX/$id_path" 365 | git_add_file "$PREFIX/$id_path" "Reencrypt password store using new GPG id ${id_print%, }${id_path:+ ($id_path)}." 366 | } 367 | 368 | cmd_show() { 369 | local opts selected_line clip=0 qrcode=0 370 | opts="$($GETOPT -o q::c:: -l qrcode::,clip:: -n "$PROGRAM" -- "$@")" 371 | local err=$? 372 | eval set -- "$opts" 373 | while true; do case $1 in 374 | -q|--qrcode) qrcode=1; selected_line="${2:-1}"; shift 2 ;; 375 | -c|--clip) clip=1; selected_line="${2:-1}"; shift 2 ;; 376 | --) shift; break ;; 377 | esac done 378 | 379 | [[ $err -ne 0 || ( $qrcode -eq 1 && $clip -eq 1 ) ]] && die "Usage: $PROGRAM $COMMAND [--clip[=line-number],-c[line-number]] [--qrcode[=line-number],-q[line-number]] [pass-name]" 380 | 381 | local pass 382 | local path="$1" 383 | local passfile="$PREFIX/$path.gpg" 384 | check_sneaky_paths "$path" 385 | if [[ -f $passfile ]]; then 386 | if [[ $clip -eq 0 && $qrcode -eq 0 ]]; then 387 | pass="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | $BASE64)" || exit $? 388 | echo "$pass" | $BASE64 -d 389 | else 390 | [[ $selected_line =~ ^[0-9]+$ ]] || die "Clip location '$selected_line' is not a number." 391 | pass="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | tail -n +${selected_line} | head -n 1)" || exit $? 392 | [[ -n $pass ]] || die "There is no password to put on the clipboard at line ${selected_line}." 393 | if [[ $clip -eq 1 ]]; then 394 | clip "$pass" "$path" 395 | elif [[ $qrcode -eq 1 ]]; then 396 | qrcode "$pass" "$path" 397 | fi 398 | fi 399 | elif [[ -d $PREFIX/$path ]]; then 400 | if [[ -z $path ]]; then 401 | echo "Password Store" 402 | else 403 | echo "${path%\/}" 404 | fi 405 | tree -N -C -l --noreport "$PREFIX/$path" 3>&- | tail -n +2 | sed -E 's/\.gpg(\x1B\[[0-9]+m)?( ->|$)/\1\2/g' # remove .gpg at end of line, but keep colors 406 | elif [[ -z $path ]]; then 407 | die "Error: password store is empty. Try \"pass init\"." 408 | else 409 | die "Error: $path is not in the password store." 410 | fi 411 | } 412 | 413 | cmd_find() { 414 | [[ $# -eq 0 ]] && die "Usage: $PROGRAM $COMMAND pass-names..." 415 | IFS="," eval 'echo "Search Terms: $*"' 416 | local terms="*$(printf '%s*|*' "$@")" 417 | tree -N -C -l --noreport -P "${terms%|*}" --prune --matchdirs --ignore-case "$PREFIX" 3>&- | tail -n +2 | sed -E 's/\.gpg(\x1B\[[0-9]+m)?( ->|$)/\1\2/g' 418 | } 419 | 420 | cmd_grep() { 421 | [[ $# -lt 1 ]] && die "Usage: $PROGRAM $COMMAND [GREPOPTIONS] search-string" 422 | local passfile grepresults 423 | while read -r -d "" passfile; do 424 | grepresults="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | grep --color=always "$@")" 425 | [[ $? -ne 0 ]] && continue 426 | passfile="${passfile%.gpg}" 427 | passfile="${passfile#$PREFIX/}" 428 | local passfile_dir="${passfile%/*}/" 429 | [[ $passfile_dir == "${passfile}/" ]] && passfile_dir="" 430 | passfile="${passfile##*/}" 431 | printf "\e[94m%s\e[1m%s\e[0m:\n" "$passfile_dir" "$passfile" 432 | echo "$grepresults" 433 | done < <(find -L "$PREFIX" -path '*/.git' -prune -o -path '*/.extensions' -prune -o -iname '*.gpg' -print0) 434 | } 435 | 436 | cmd_insert() { 437 | local opts multiline=0 noecho=1 force=0 438 | opts="$($GETOPT -o mef -l multiline,echo,force -n "$PROGRAM" -- "$@")" 439 | local err=$? 440 | eval set -- "$opts" 441 | while true; do case $1 in 442 | -m|--multiline) multiline=1; shift ;; 443 | -e|--echo) noecho=0; shift ;; 444 | -f|--force) force=1; shift ;; 445 | --) shift; break ;; 446 | esac done 447 | 448 | [[ $err -ne 0 || ( $multiline -eq 1 && $noecho -eq 0 ) || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--echo,-e | --multiline,-m] [--force,-f] pass-name" 449 | local path="${1%/}" 450 | local passfile="$PREFIX/$path.gpg" 451 | check_sneaky_paths "$path" 452 | set_git "$passfile" 453 | 454 | [[ $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?" 455 | 456 | mkdir -p -v "$PREFIX/$(dirname -- "$path")" 457 | set_gpg_recipients "$(dirname -- "$path")" 458 | 459 | if [[ $multiline -eq 1 ]]; then 460 | echo "Enter contents of $path and press Ctrl+D when finished:" 461 | echo 462 | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" || die "Password encryption aborted." 463 | elif [[ $noecho -eq 1 ]]; then 464 | local password password_again 465 | while true; do 466 | read -r -p "Enter password for $path: " -s password || exit 1 467 | echo 468 | read -r -p "Retype password for $path: " -s password_again || exit 1 469 | echo 470 | if [[ $password == "$password_again" ]]; then 471 | echo "$password" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" || die "Password encryption aborted." 472 | break 473 | else 474 | die "Error: the entered passwords do not match." 475 | fi 476 | done 477 | else 478 | local password 479 | read -r -p "Enter password for $path: " -e password 480 | echo "$password" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" || die "Password encryption aborted." 481 | fi 482 | git_add_file "$passfile" "Add given password for $path to store." 483 | } 484 | 485 | cmd_edit() { 486 | [[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND pass-name" 487 | 488 | local path="${1%/}" 489 | check_sneaky_paths "$path" 490 | mkdir -p -v "$PREFIX/$(dirname -- "$path")" 491 | set_gpg_recipients "$(dirname -- "$path")" 492 | local passfile="$PREFIX/$path.gpg" 493 | set_git "$passfile" 494 | 495 | tmpdir #Defines $SECURE_TMPDIR 496 | local tmp_file="$(mktemp -u "$SECURE_TMPDIR/XXXXXX")-${path//\//-}.txt" 497 | 498 | local action="Add" 499 | if [[ -f $passfile ]]; then 500 | $GPG -d -o "$tmp_file" "${GPG_OPTS[@]}" "$passfile" || exit 1 501 | action="Edit" 502 | fi 503 | ${EDITOR:-vi} "$tmp_file" 504 | [[ -f $tmp_file ]] || die "New password not saved." 505 | $GPG -d -o - "${GPG_OPTS[@]}" "$passfile" 2>/dev/null | diff - "$tmp_file" &>/dev/null && die "Password unchanged." 506 | while ! $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" "$tmp_file"; do 507 | yesno "GPG encryption failed. Would you like to try again?" 508 | done 509 | git_add_file "$passfile" "$action password for $path using ${EDITOR:-vi}." 510 | } 511 | 512 | cmd_generate() { 513 | local opts qrcode=0 clip=0 force=0 characters="$CHARACTER_SET" inplace=0 pass 514 | opts="$($GETOPT -o nqcif -l no-symbols,qrcode,clip,in-place,force -n "$PROGRAM" -- "$@")" 515 | local err=$? 516 | eval set -- "$opts" 517 | while true; do case $1 in 518 | -n|--no-symbols) characters="$CHARACTER_SET_NO_SYMBOLS"; shift ;; 519 | -q|--qrcode) qrcode=1; shift ;; 520 | -c|--clip) clip=1; shift ;; 521 | -f|--force) force=1; shift ;; 522 | -i|--in-place) inplace=1; shift ;; 523 | --) shift; break ;; 524 | esac done 525 | 526 | [[ $err -ne 0 || ( $# -ne 2 && $# -ne 1 ) || ( $force -eq 1 && $inplace -eq 1 ) || ( $qrcode -eq 1 && $clip -eq 1 ) ]] && die "Usage: $PROGRAM $COMMAND [--no-symbols,-n] [--clip,-c] [--qrcode,-q] [--in-place,-i | --force,-f] pass-name [pass-length]" 527 | local path="$1" 528 | local length="${2:-$GENERATED_LENGTH}" 529 | check_sneaky_paths "$path" 530 | [[ $length =~ ^[0-9]+$ ]] || die "Error: pass-length \"$length\" must be a number." 531 | [[ $length -gt 0 ]] || die "Error: pass-length must be greater than zero." 532 | mkdir -p -v "$PREFIX/$(dirname -- "$path")" 533 | set_gpg_recipients "$(dirname -- "$path")" 534 | local passfile="$PREFIX/$path.gpg" 535 | set_git "$passfile" 536 | 537 | [[ $inplace -eq 0 && $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?" 538 | 539 | read -r -n $length pass < <(LC_ALL=C tr -dc "$characters" < /dev/urandom) 540 | [[ ${#pass} -eq $length ]] || die "Could not generate password from /dev/urandom." 541 | if [[ $inplace -eq 0 ]]; then 542 | echo "$pass" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" || die "Password encryption aborted." 543 | else 544 | local passfile_temp="${passfile}.tmp.${RANDOM}.${RANDOM}.${RANDOM}.${RANDOM}.--" 545 | if { echo "$pass"; $GPG -d "${GPG_OPTS[@]}" "$passfile" | tail -n +2; } | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile_temp" "${GPG_OPTS[@]}"; then 546 | mv "$passfile_temp" "$passfile" 547 | else 548 | rm -f "$passfile_temp" 549 | die "Could not reencrypt new password." 550 | fi 551 | fi 552 | local verb="Add" 553 | [[ $inplace -eq 1 ]] && verb="Replace" 554 | git_add_file "$passfile" "$verb generated password for ${path}." 555 | 556 | if [[ $clip -eq 1 ]]; then 557 | clip "$pass" "$path" 558 | elif [[ $qrcode -eq 1 ]]; then 559 | qrcode "$pass" "$path" 560 | else 561 | printf "\e[1mThe generated password for \e[4m%s\e[24m is:\e[0m\n\e[1m\e[93m%s\e[0m\n" "$path" "$pass" 562 | fi 563 | } 564 | 565 | cmd_delete() { 566 | local opts recursive="" force=0 567 | opts="$($GETOPT -o rf -l recursive,force -n "$PROGRAM" -- "$@")" 568 | local err=$? 569 | eval set -- "$opts" 570 | while true; do case $1 in 571 | -r|--recursive) recursive="-r"; shift ;; 572 | -f|--force) force=1; shift ;; 573 | --) shift; break ;; 574 | esac done 575 | [[ $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--recursive,-r] [--force,-f] pass-name" 576 | local path="$1" 577 | check_sneaky_paths "$path" 578 | 579 | local passdir="$PREFIX/${path%/}" 580 | local passfile="$PREFIX/$path.gpg" 581 | [[ -f $passfile && -d $passdir && $path == */ || ! -f $passfile ]] && passfile="${passdir%/}/" 582 | [[ -e $passfile ]] || die "Error: $path is not in the password store." 583 | set_git "$passfile" 584 | 585 | [[ $force -eq 1 ]] || yesno "Are you sure you would like to delete $path?" 586 | 587 | rm $recursive -f -v "$passfile" 588 | set_git "$passfile" 589 | if [[ -n $INNER_GIT_DIR && ! -e $passfile ]]; then 590 | git -C "$INNER_GIT_DIR" rm -qr "$passfile" 591 | set_git "$passfile" 592 | git_commit "Remove $path from store." 593 | fi 594 | rmdir -p "${passfile%/*}" 2>/dev/null 595 | } 596 | 597 | cmd_copy_move() { 598 | local opts move=1 force=0 599 | [[ $1 == "copy" ]] && move=0 600 | shift 601 | opts="$($GETOPT -o f -l force -n "$PROGRAM" -- "$@")" 602 | local err=$? 603 | eval set -- "$opts" 604 | while true; do case $1 in 605 | -f|--force) force=1; shift ;; 606 | --) shift; break ;; 607 | esac done 608 | [[ $# -ne 2 ]] && die "Usage: $PROGRAM $COMMAND [--force,-f] old-path new-path" 609 | check_sneaky_paths "$@" 610 | local old_path="$PREFIX/${1%/}" 611 | local old_dir="$old_path" 612 | local new_path="$PREFIX/$2" 613 | 614 | if ! [[ -f $old_path.gpg && -d $old_path && $1 == */ || ! -f $old_path.gpg ]]; then 615 | old_dir="${old_path%/*}" 616 | old_path="${old_path}.gpg" 617 | fi 618 | echo "$old_path" 619 | [[ -e $old_path ]] || die "Error: $1 is not in the password store." 620 | 621 | mkdir -p -v "${new_path%/*}" 622 | [[ -d $old_path || -d $new_path || $new_path == */ ]] || new_path="${new_path}.gpg" 623 | 624 | local interactive="-i" 625 | [[ ! -t 0 || $force -eq 1 ]] && interactive="-f" 626 | 627 | set_git "$new_path" 628 | if [[ $move -eq 1 ]]; then 629 | mv $interactive -v "$old_path" "$new_path" || exit 1 630 | [[ -e "$new_path" ]] && reencrypt_path "$new_path" 631 | 632 | set_git "$new_path" 633 | if [[ -n $INNER_GIT_DIR && ! -e $old_path ]]; then 634 | git -C "$INNER_GIT_DIR" rm -qr "$old_path" 2>/dev/null 635 | set_git "$new_path" 636 | git_add_file "$new_path" "Rename ${1} to ${2}." 637 | fi 638 | set_git "$old_path" 639 | if [[ -n $INNER_GIT_DIR && ! -e $old_path ]]; then 640 | git -C "$INNER_GIT_DIR" rm -qr "$old_path" 2>/dev/null 641 | set_git "$old_path" 642 | [[ -n $(git -C "$INNER_GIT_DIR" status --porcelain "$old_path") ]] && git_commit "Remove ${1}." 643 | fi 644 | rmdir -p "$old_dir" 2>/dev/null 645 | else 646 | cp $interactive -r -v "$old_path" "$new_path" || exit 1 647 | [[ -e "$new_path" ]] && reencrypt_path "$new_path" 648 | git_add_file "$new_path" "Copy ${1} to ${2}." 649 | fi 650 | } 651 | 652 | cmd_git() { 653 | set_git "$PREFIX/" 654 | if [[ $1 == "init" ]]; then 655 | INNER_GIT_DIR="$PREFIX" 656 | git -C "$INNER_GIT_DIR" "$@" || exit 1 657 | git_add_file "$PREFIX" "Add current contents of password store." 658 | 659 | echo '*.gpg diff=gpg' > "$PREFIX/.gitattributes" 660 | git_add_file .gitattributes "Configure git repository for gpg file diff." 661 | git -C "$INNER_GIT_DIR" config --local diff.gpg.binary true 662 | git -C "$INNER_GIT_DIR" config --local diff.gpg.textconv "$GPG -d ${GPG_OPTS[*]}" 663 | elif [[ -n $INNER_GIT_DIR ]]; then 664 | tmpdir nowarn #Defines $SECURE_TMPDIR. We don't warn, because at most, this only copies encrypted files. 665 | export TMPDIR="$SECURE_TMPDIR" 666 | git -C "$INNER_GIT_DIR" "$@" 667 | else 668 | die "Error: the password store is not a git repository. Try \"$PROGRAM git init\"." 669 | fi 670 | } 671 | 672 | cmd_extension_or_show() { 673 | if ! cmd_extension "$@"; then 674 | COMMAND="show" 675 | cmd_show "$@" 676 | fi 677 | } 678 | 679 | SYSTEM_EXTENSION_DIR="" 680 | cmd_extension() { 681 | check_sneaky_paths "$1" 682 | local user_extension system_extension extension 683 | [[ -n $SYSTEM_EXTENSION_DIR ]] && system_extension="$SYSTEM_EXTENSION_DIR/$1.bash" 684 | [[ $PASSWORD_STORE_ENABLE_EXTENSIONS == true ]] && user_extension="$EXTENSIONS/$1.bash" 685 | if [[ -n $user_extension && -f $user_extension && -x $user_extension ]]; then 686 | verify_file "$user_extension" 687 | extension="$user_extension" 688 | elif [[ -n $system_extension && -f $system_extension && -x $system_extension ]]; then 689 | extension="$system_extension" 690 | else 691 | return 1 692 | fi 693 | shift 694 | source "$extension" "$@" 695 | return 0 696 | } 697 | 698 | # 699 | # END subcommand functions 700 | # 701 | 702 | PROGRAM="${0##*/}" 703 | COMMAND="$1" 704 | 705 | case "$1" in 706 | init) shift; cmd_init "$@" ;; 707 | help|--help) shift; cmd_usage "$@" ;; 708 | version|--version) shift; cmd_version "$@" ;; 709 | show|ls|list) shift; cmd_show "$@" ;; 710 | find|search) shift; cmd_find "$@" ;; 711 | grep) shift; cmd_grep "$@" ;; 712 | insert|add) shift; cmd_insert "$@" ;; 713 | edit) shift; cmd_edit "$@" ;; 714 | generate) shift; cmd_generate "$@" ;; 715 | delete|rm|remove) shift; cmd_delete "$@" ;; 716 | rename|mv) shift; cmd_copy_move "move" "$@" ;; 717 | copy|cp) shift; cmd_copy_move "copy" "$@" ;; 718 | git) shift; cmd_git "$@" ;; 719 | *) cmd_extension_or_show "$@" ;; 720 | esac 721 | exit 0 722 | -------------------------------------------------------------------------------- /src/platform/cygwin.sh: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Jason A. Donenfeld . All Rights Reserved. 2 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 3 | 4 | clip() { 5 | local sleep_argv0="password store sleep on display $DISPLAY" 6 | pkill -f "^$sleep_argv0" 2>/dev/null && sleep 0.5 7 | local before="$($BASE64 < /dev/clipboard)" 8 | echo -n "$1" > /dev/clipboard 9 | ( 10 | ( exec -a "$sleep_argv0" sleep "$CLIP_TIME" ) 11 | local now="$($BASE64 < /dev/clipboard)" 12 | [[ $now != $(echo -n "$1" | $BASE64) ]] && before="$now" 13 | echo "$before" | $BASE64 -d > /dev/clipboard 14 | ) >/dev/null 2>&1 & disown 15 | echo "Copied $2 to clipboard. Will clear in $CLIP_TIME seconds." 16 | } 17 | 18 | # replaces Cygwin-style filenames with their Windows counterparts 19 | gpg_winpath() { 20 | local args=("$@") 21 | # as soon as an argument (from back to front) is no file, it can only be a filename argument if it is preceeded by '-o' 22 | local could_be_filenames="true" 23 | local i 24 | for ((i=${#args[@]}-1; i>=0; i--)); do 25 | if ( [ $i -gt 0 ] && [ "${args[$i-1]}" = "-o" ] && [ "${args[$i]}" != "-" ] ); then 26 | args[$i]="$(cygpath -am "${args[$i]}")" 27 | elif [ $could_be_filenames = "true" ]; then 28 | if [ -e "${args[$i]}" ]; then 29 | args[$i]="$(cygpath -am "${args[$i]}")" 30 | else 31 | could_be_filenames="false" 32 | fi 33 | fi 34 | done 35 | $GPG_ORIG "${args[@]}" 36 | } 37 | 38 | if $GPG --help | grep -q 'Home: [A-Z]:[/\\]'; then 39 | GPG_ORIG="$GPG" 40 | GPG=gpg_winpath 41 | fi 42 | -------------------------------------------------------------------------------- /src/platform/darwin.sh: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 - 2014 Jason A. Donenfeld . All Rights Reserved. 2 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 3 | 4 | clip() { 5 | local sleep_argv0="password store sleep for user $(id -u)" 6 | pkill -f "^$sleep_argv0" 2>/dev/null && sleep 0.5 7 | local before="$(pbpaste | $BASE64)" 8 | echo -n "$1" | pbcopy 9 | ( 10 | ( exec -a "$sleep_argv0" sleep "$CLIP_TIME" ) 11 | local now="$(pbpaste | $BASE64)" 12 | [[ $now != $(echo -n "$1" | $BASE64) ]] && before="$now" 13 | echo "$before" | $BASE64 -d | pbcopy 14 | ) >/dev/null 2>&1 & disown 15 | echo "Copied $2 to clipboard. Will clear in $CLIP_TIME seconds." 16 | } 17 | 18 | tmpdir() { 19 | [[ -n $SECURE_TMPDIR ]] && return 20 | unmount_tmpdir() { 21 | [[ -n $SECURE_TMPDIR && -d $SECURE_TMPDIR && -n $DARWIN_RAMDISK_DEV ]] || return 22 | umount "$SECURE_TMPDIR" 23 | diskutil quiet eject "$DARWIN_RAMDISK_DEV" 24 | rm -rf "$SECURE_TMPDIR" 25 | } 26 | trap unmount_tmpdir INT TERM EXIT 27 | SECURE_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/$PROGRAM.XXXXXXXXXXXXX")" 28 | DARWIN_RAMDISK_DEV="$(hdid -drivekey system-image=yes -nomount 'ram://32768' | cut -d ' ' -f 1)" # 32768 sectors = 16 mb 29 | [[ -z $DARWIN_RAMDISK_DEV ]] && die "Error: could not create ramdisk." 30 | newfs_hfs -M 700 "$DARWIN_RAMDISK_DEV" &>/dev/null || die "Error: could not create filesystem on ramdisk." 31 | mount -t hfs -o noatime -o nobrowse "$DARWIN_RAMDISK_DEV" "$SECURE_TMPDIR" || die "Error: could not mount filesystem on ramdisk." 32 | } 33 | 34 | qrcode() { 35 | if type imgcat >/dev/null 2>&1; then 36 | echo -n "$1" | qrencode --size 10 -o - | imgcat 37 | else 38 | echo -n "$1" | qrencode -t utf8 39 | fi 40 | } 41 | 42 | GETOPT="$({ test -x /usr/local/opt/gnu-getopt/bin/getopt && echo /usr/local/opt/gnu-getopt; } || brew --prefix gnu-getopt 2>/dev/null || { command -v port &>/dev/null && echo /opt/local; } || echo /usr/local)/bin/getopt" 43 | SHRED="srm -f -z" 44 | BASE64="openssl base64" 45 | -------------------------------------------------------------------------------- /src/platform/freebsd.sh: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Jonathan Chu . All Rights Reserved. 2 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 3 | 4 | GETOPT="/usr/local/bin/getopt" 5 | SHRED="rm -P -f" 6 | BASE64="openssl base64" 7 | -------------------------------------------------------------------------------- /src/platform/openbsd.sh: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Jonathan Chu . All Rights Reserved. 2 | # Copyright (C) 2015 David Dahlberg . All Rights Reserved. 3 | # This file is licensed under the GPLv2+. Please see COPYING for more information. 4 | 5 | tmpdir() { 6 | [[ -n $SECURE_TMPDIR ]] && return 7 | local warn=1 8 | [[ $1 == "nowarn" ]] && warn=0 9 | local template="$PROGRAM.XXXXXXXXXXXXX" 10 | if [[ $(sysctl -n kern.usermount) == 1 ]]; then 11 | SECURE_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/$template")" 12 | mount -t tmpfs -o -s16M tmpfs "$SECURE_TMPDIR" || die "Error: could not create tmpfs." 13 | unmount_tmpdir() { 14 | [[ -n $SECURE_TMPDIR && -d $SECURE_TMPDIR ]] || return 15 | umount "$SECURE_TMPDIR" 16 | rm -rf "$SECURE_TMPDIR" 17 | } 18 | trap unmount_tmpdir INT TERM EXIT 19 | else 20 | [[ $warn -eq 1 ]] && yesno "$(cat <<-_EOF 21 | The sysctl kern.usermount is disabled, therefore it is not 22 | possible to create a tmpfs for temporary storage of files 23 | in memory. 24 | This means that it may be difficult to entirely erase 25 | the temporary non-encrypted password file after editing. 26 | 27 | Are you sure you would like to continue? 28 | _EOF 29 | )" 30 | SECURE_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/$template")" 31 | shred_tmpfile() { 32 | find "$SECURE_TMPDIR" -type f -exec $SHRED {} + 33 | rm -rf "$SECURE_TMPDIR" 34 | } 35 | trap shred_tmpfile INT TERM EXIT 36 | fi 37 | } 38 | 39 | GETOPT="gnugetopt" 40 | SHRED="rm -P -f" 41 | BASE64="openssl base64" 42 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | gnupg/random_seed 2 | test-results/ 3 | trash directory.* 4 | -------------------------------------------------------------------------------- /tests/TODO.txt: -------------------------------------------------------------------------------- 1 | * pass cp [all the above] 2 | * pass insert, pass ls 3 | * pass insert [with -e, with -m, without either] 4 | * pass insert, cp, mv, rm, generate [with -f, without -f, on existing] 5 | * git operations on all commands 6 | * Are empty folders pruned for rm, cp, mv, init (when "" is the argument) [with git, and without git] 7 | * Are git commits happening as planned? Are they being signed when signing is enabled? 8 | -------------------------------------------------------------------------------- /tests/fake-editor-change-password.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Fake editor program for testing 'pass edit'. 3 | # Changes password to 'Hello World', leaving rest of file intact. 4 | # 5 | # Intended use: 6 | # export FAKE_EDITOR_PASSWORD="blah blah blah" 7 | # export EDITOR=fake-editor-change-password.sh 8 | # $EDITOR 9 | # 10 | # Arguments: 11 | # Returns: 0 on success, 1 on error 12 | 13 | if [[ $# -ne 1 ]]; then 14 | echo "Usage: $0 " 15 | exit 1 16 | fi 17 | 18 | filename=$1 ; shift ; 19 | new_password="${FAKE_EDITOR_PASSWORD:-Hello World}" 20 | 21 | # And change only first line of file 22 | # -i.tmp allows editing file in place. Extension needed on Mac OSX 23 | sed -i.tmp "1 s/^.*\$/$new_password/g" "$filename" 24 | 25 | exit 0 26 | -------------------------------------------------------------------------------- /tests/gnupg/.gpg-v21-migrated: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zx2c4/password-store/3ca13cd8882cae4083c1c478858adbf2e82dd037/tests/gnupg/.gpg-v21-migrated -------------------------------------------------------------------------------- /tests/gnupg/gpg.conf: -------------------------------------------------------------------------------- 1 | group group1 = 9378267629F989A0E96677B7976DD3D6E4691410 70BD448330ACF0653645B8F2B4DDBFF0D774A374 2 | group group2 = 9378267629F989A0E96677B7976DD3D6E4691410 3 | group big group = D4C78DB7920E1E27F5416B81CC9DB947CF90C77B 70BD448330ACF0653645B8F2B4DDBFF0D774A374 62EBE74BE834C2EC71E6414595C4B715EB7D54A8 9378267629F989A0E96677B7976DD3D6E4691410 4D2AFBDE67C60F5999D143AFA6E073D439E5020C 4 | -------------------------------------------------------------------------------- /tests/gnupg/private-keys-v1.d/0606FE40527B8F47BFD30238709F895642EEF303.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zx2c4/password-store/3ca13cd8882cae4083c1c478858adbf2e82dd037/tests/gnupg/private-keys-v1.d/0606FE40527B8F47BFD30238709F895642EEF303.key -------------------------------------------------------------------------------- /tests/gnupg/private-keys-v1.d/06278846A35FE4416E8701DDCF6B60E93F8BCB63.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zx2c4/password-store/3ca13cd8882cae4083c1c478858adbf2e82dd037/tests/gnupg/private-keys-v1.d/06278846A35FE4416E8701DDCF6B60E93F8BCB63.key -------------------------------------------------------------------------------- /tests/gnupg/private-keys-v1.d/615FC2A5B2CBFD58B7FFA0A140D43B74AB9748B0.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zx2c4/password-store/3ca13cd8882cae4083c1c478858adbf2e82dd037/tests/gnupg/private-keys-v1.d/615FC2A5B2CBFD58B7FFA0A140D43B74AB9748B0.key -------------------------------------------------------------------------------- /tests/gnupg/private-keys-v1.d/63D607EC5C89163B473708E7B3E5115301CF06E4.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zx2c4/password-store/3ca13cd8882cae4083c1c478858adbf2e82dd037/tests/gnupg/private-keys-v1.d/63D607EC5C89163B473708E7B3E5115301CF06E4.key -------------------------------------------------------------------------------- /tests/gnupg/private-keys-v1.d/A5CEE9554AA7090ADD97D97E0DA902764E6C2111.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zx2c4/password-store/3ca13cd8882cae4083c1c478858adbf2e82dd037/tests/gnupg/private-keys-v1.d/A5CEE9554AA7090ADD97D97E0DA902764E6C2111.key -------------------------------------------------------------------------------- /tests/gnupg/private-keys-v1.d/AD20D0B45D263DD5AE866FDB98E04A0D20070F68.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zx2c4/password-store/3ca13cd8882cae4083c1c478858adbf2e82dd037/tests/gnupg/private-keys-v1.d/AD20D0B45D263DD5AE866FDB98E04A0D20070F68.key -------------------------------------------------------------------------------- /tests/gnupg/private-keys-v1.d/C93858C40FA9E117DA4E7F336580B8B12354EB83.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zx2c4/password-store/3ca13cd8882cae4083c1c478858adbf2e82dd037/tests/gnupg/private-keys-v1.d/C93858C40FA9E117DA4E7F336580B8B12354EB83.key -------------------------------------------------------------------------------- /tests/gnupg/private-keys-v1.d/C93F70CA322D4F42E7FC7D54F6367E65C23E5CA3.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zx2c4/password-store/3ca13cd8882cae4083c1c478858adbf2e82dd037/tests/gnupg/private-keys-v1.d/C93F70CA322D4F42E7FC7D54F6367E65C23E5CA3.key -------------------------------------------------------------------------------- /tests/gnupg/private-keys-v1.d/CDA6EE91E62A15AB9F6A3041FE01CC123B7E9D23.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zx2c4/password-store/3ca13cd8882cae4083c1c478858adbf2e82dd037/tests/gnupg/private-keys-v1.d/CDA6EE91E62A15AB9F6A3041FE01CC123B7E9D23.key -------------------------------------------------------------------------------- /tests/gnupg/private-keys-v1.d/FFED3C5A6A52B200BCCE3F41593EA51D6054649F.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zx2c4/password-store/3ca13cd8882cae4083c1c478858adbf2e82dd037/tests/gnupg/private-keys-v1.d/FFED3C5A6A52B200BCCE3F41593EA51D6054649F.key -------------------------------------------------------------------------------- /tests/gnupg/pubring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zx2c4/password-store/3ca13cd8882cae4083c1c478858adbf2e82dd037/tests/gnupg/pubring.gpg -------------------------------------------------------------------------------- /tests/gnupg/secring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zx2c4/password-store/3ca13cd8882cae4083c1c478858adbf2e82dd037/tests/gnupg/secring.gpg -------------------------------------------------------------------------------- /tests/gnupg/trustdb.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zx2c4/password-store/3ca13cd8882cae4083c1c478858adbf2e82dd037/tests/gnupg/trustdb.gpg -------------------------------------------------------------------------------- /tests/setup.sh: -------------------------------------------------------------------------------- 1 | # This file should be sourced by all test-scripts 2 | # 3 | # This scripts sets the following: 4 | # $PASS Full path to password-store script to test 5 | # $GPG Name of gpg executable 6 | # $KEY{1..5} GPG key ids of testing keys 7 | # $TEST_HOME This folder 8 | 9 | 10 | # Unset config vars 11 | unset PASSWORD_STORE_DIR 12 | unset PASSWORD_STORE_KEY 13 | unset PASSWORD_STORE_GIT 14 | unset PASSWORD_STORE_GPG_OPTS 15 | unset PASSWORD_STORE_X_SELECTION 16 | unset PASSWORD_STORE_CLIP_TIME 17 | unset PASSWORD_STORE_UMASK 18 | unset PASSWORD_STORE_GENERATED_LENGTH 19 | unset PASSWORD_STORE_CHARACTER_SET 20 | unset PASSWORD_STORE_CHARACTER_SET_NO_SYMBOLS 21 | unset PASSWORD_STORE_ENABLE_EXTENSIONS 22 | unset PASSWORD_STORE_EXTENSIONS_DIR 23 | unset PASSWORD_STORE_SIGNING_KEY 24 | unset EDITOR 25 | 26 | # We must be called from tests/ 27 | TEST_HOME="$(pwd)" 28 | 29 | . ./sharness.sh 30 | 31 | export PASSWORD_STORE_DIR="$SHARNESS_TRASH_DIRECTORY/test-store/" 32 | rm -rf "$PASSWORD_STORE_DIR" 33 | mkdir -p "$PASSWORD_STORE_DIR" 34 | if [[ ! -d $PASSWORD_STORE_DIR ]]; then 35 | echo "Could not create $PASSWORD_STORE_DIR" 36 | exit 1 37 | fi 38 | 39 | export GIT_DIR="$PASSWORD_STORE_DIR/.git" 40 | export GIT_WORK_TREE="$PASSWORD_STORE_DIR" 41 | git config --global user.email "Pass-Automated-Testing-Suite@zx2c4.com" 42 | git config --global user.name "Pass Automated Testing Suite" 43 | 44 | 45 | PASS="$TEST_HOME/../src/password-store.sh" 46 | if [[ ! -e $PASS ]]; then 47 | echo "Could not find password-store.sh" 48 | exit 1 49 | fi 50 | 51 | # Note: the assumption is the test key is unencrypted. 52 | export GNUPGHOME="$TEST_HOME/gnupg/" 53 | chmod 700 "$GNUPGHOME" 54 | GPG="gpg" 55 | command -v gpg2 &>/dev/null && GPG="gpg2" 56 | 57 | # We don't want any currently running agent to conflict. 58 | unset GPG_AGENT_INFO 59 | 60 | KEY1="D4C78DB7920E1E27F5416B81CC9DB947CF90C77B" # pass test key 1 61 | KEY2="70BD448330ACF0653645B8F2B4DDBFF0D774A374" # pass test key 2 62 | KEY3="62EBE74BE834C2EC71E6414595C4B715EB7D54A8" # pass test key 3 63 | KEY4="9378267629F989A0E96677B7976DD3D6E4691410" # pass test key 4 64 | KEY5="4D2AFBDE67C60F5999D143AFA6E073D439E5020C" # pass test key 5 65 | -------------------------------------------------------------------------------- /tests/sharness.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Copyright (c) 2011-2012 Mathias Lafeldt 4 | # Copyright (c) 2005-2012 Git project 5 | # Copyright (c) 2005-2012 Junio C Hamano 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see http://www.gnu.org/licenses/ . 19 | 20 | # Public: Current version of Sharness. 21 | SHARNESS_VERSION="0.3.0" 22 | export SHARNESS_VERSION 23 | 24 | # Public: The file extension for tests. By default, it is set to "t". 25 | : ${SHARNESS_TEST_EXTENSION:=t} 26 | export SHARNESS_TEST_EXTENSION 27 | 28 | # Keep the original TERM for say_color 29 | ORIGINAL_TERM=$TERM 30 | 31 | # For repeatability, reset the environment to a known state. 32 | LANG=C 33 | LC_ALL=C 34 | PAGER=cat 35 | TZ=UTC 36 | TERM=dumb 37 | EDITOR=: 38 | export LANG LC_ALL PAGER TZ TERM EDITOR 39 | unset VISUAL CDPATH GREP_OPTIONS 40 | 41 | # Line feed 42 | LF=' 43 | ' 44 | 45 | [ "x$ORIGINAL_TERM" != "xdumb" ] && ( 46 | TERM=$ORIGINAL_TERM && 47 | export TERM && 48 | [ -t 1 ] && 49 | tput bold >/dev/null 2>&1 && 50 | tput setaf 1 >/dev/null 2>&1 && 51 | tput sgr0 >/dev/null 2>&1 52 | ) && 53 | color=t 54 | 55 | while test "$#" -ne 0; do 56 | case "$1" in 57 | -d|--d|--de|--deb|--debu|--debug) 58 | debug=t; shift ;; 59 | -i|--i|--im|--imm|--imme|--immed|--immedi|--immedia|--immediat|--immediate) 60 | immediate=t; shift ;; 61 | -l|--l|--lo|--lon|--long|--long-|--long-t|--long-te|--long-tes|--long-test|--long-tests) 62 | TEST_LONG=t; export TEST_LONG; shift ;; 63 | -h|--h|--he|--hel|--help) 64 | help=t; shift ;; 65 | -v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose) 66 | verbose=t; shift ;; 67 | -q|--q|--qu|--qui|--quie|--quiet) 68 | # Ignore --quiet under a TAP::Harness. Saying how many tests 69 | # passed without the ok/not ok details is always an error. 70 | test -z "$HARNESS_ACTIVE" && quiet=t; shift ;; 71 | --no-color) 72 | color=; shift ;; 73 | --root=*) 74 | root=$(expr "z$1" : 'z[^=]*=\(.*\)') 75 | shift ;; 76 | *) 77 | echo "error: unknown test option '$1'" >&2; exit 1 ;; 78 | esac 79 | done 80 | 81 | if test -n "$color"; then 82 | say_color() { 83 | ( 84 | TERM=$ORIGINAL_TERM 85 | export TERM 86 | case "$1" in 87 | error) 88 | tput bold; tput setaf 1;; # bold red 89 | skip) 90 | tput setaf 4;; # blue 91 | warn) 92 | tput setaf 3;; # brown/yellow 93 | pass) 94 | tput setaf 2;; # green 95 | info) 96 | tput setaf 6;; # cyan 97 | *) 98 | test -n "$quiet" && return;; 99 | esac 100 | shift 101 | printf "%s" "$*" 102 | tput sgr0 103 | echo 104 | ) 105 | } 106 | else 107 | say_color() { 108 | test -z "$1" && test -n "$quiet" && return 109 | shift 110 | printf "%s\n" "$*" 111 | } 112 | fi 113 | 114 | error() { 115 | say_color error "error: $*" 116 | EXIT_OK=t 117 | exit 1 118 | } 119 | 120 | say() { 121 | say_color info "$*" 122 | } 123 | 124 | test -n "$test_description" || error "Test script did not set test_description." 125 | 126 | if test "$help" = "t"; then 127 | echo "$test_description" 128 | exit 0 129 | fi 130 | 131 | exec 5>&1 132 | exec 6<&0 133 | if test "$verbose" = "t"; then 134 | exec 4>&2 3>&1 135 | else 136 | exec 4>/dev/null 3>/dev/null 137 | fi 138 | 139 | test_failure=0 140 | test_count=0 141 | test_fixed=0 142 | test_broken=0 143 | test_success=0 144 | 145 | die() { 146 | code=$? 147 | if test -n "$EXIT_OK"; then 148 | exit $code 149 | else 150 | echo >&5 "FATAL: Unexpected exit with code $code" 151 | exit 1 152 | fi 153 | } 154 | 155 | EXIT_OK= 156 | trap 'die' EXIT 157 | 158 | # Public: Define that a test prerequisite is available. 159 | # 160 | # The prerequisite can later be checked explicitly using test_have_prereq or 161 | # implicitly by specifying the prerequisite name in calls to test_expect_success 162 | # or test_expect_failure. 163 | # 164 | # $1 - Name of prerequiste (a simple word, in all capital letters by convention) 165 | # 166 | # Examples 167 | # 168 | # # Set PYTHON prerequisite if interpreter is available. 169 | # command -v python >/dev/null && test_set_prereq PYTHON 170 | # 171 | # # Set prerequisite depending on some variable. 172 | # test -z "$NO_GETTEXT" && test_set_prereq GETTEXT 173 | # 174 | # Returns nothing. 175 | test_set_prereq() { 176 | satisfied_prereq="$satisfied_prereq$1 " 177 | } 178 | satisfied_prereq=" " 179 | 180 | # Public: Check if one or more test prerequisites are defined. 181 | # 182 | # The prerequisites must have previously been set with test_set_prereq. 183 | # The most common use of this is to skip all the tests if some essential 184 | # prerequisite is missing. 185 | # 186 | # $1 - Comma-separated list of test prerequisites. 187 | # 188 | # Examples 189 | # 190 | # # Skip all remaining tests if prerequisite is not set. 191 | # if ! test_have_prereq PERL; then 192 | # skip_all='skipping perl interface tests, perl not available' 193 | # test_done 194 | # fi 195 | # 196 | # Returns 0 if all prerequisites are defined or 1 otherwise. 197 | test_have_prereq() { 198 | # prerequisites can be concatenated with ',' 199 | save_IFS=$IFS 200 | IFS=, 201 | set -- $* 202 | IFS=$save_IFS 203 | 204 | total_prereq=0 205 | ok_prereq=0 206 | missing_prereq= 207 | 208 | for prerequisite; do 209 | case "$prerequisite" in 210 | !*) 211 | negative_prereq=t 212 | prerequisite=${prerequisite#!} 213 | ;; 214 | *) 215 | negative_prereq= 216 | esac 217 | 218 | total_prereq=$(($total_prereq + 1)) 219 | case "$satisfied_prereq" in 220 | *" $prerequisite "*) 221 | satisfied_this_prereq=t 222 | ;; 223 | *) 224 | satisfied_this_prereq= 225 | esac 226 | 227 | case "$satisfied_this_prereq,$negative_prereq" in 228 | t,|,t) 229 | ok_prereq=$(($ok_prereq + 1)) 230 | ;; 231 | *) 232 | # Keep a list of missing prerequisites; restore 233 | # the negative marker if necessary. 234 | prerequisite=${negative_prereq:+!}$prerequisite 235 | if test -z "$missing_prereq"; then 236 | missing_prereq=$prerequisite 237 | else 238 | missing_prereq="$prerequisite,$missing_prereq" 239 | fi 240 | esac 241 | done 242 | 243 | test $total_prereq = $ok_prereq 244 | } 245 | 246 | # You are not expected to call test_ok_ and test_failure_ directly, use 247 | # the text_expect_* functions instead. 248 | 249 | test_ok_() { 250 | test_success=$(($test_success + 1)) 251 | say_color "" "ok $test_count - $@" 252 | } 253 | 254 | test_failure_() { 255 | test_failure=$(($test_failure + 1)) 256 | say_color error "not ok $test_count - $1" 257 | shift 258 | echo "$@" | sed -e 's/^/# /' 259 | test "$immediate" = "" || { EXIT_OK=t; exit 1; } 260 | } 261 | 262 | test_known_broken_ok_() { 263 | test_fixed=$(($test_fixed + 1)) 264 | say_color error "ok $test_count - $@ # TODO known breakage vanished" 265 | } 266 | 267 | test_known_broken_failure_() { 268 | test_broken=$(($test_broken + 1)) 269 | say_color warn "not ok $test_count - $@ # TODO known breakage" 270 | } 271 | 272 | # Public: Execute commands in debug mode. 273 | # 274 | # Takes a single argument and evaluates it only when the test script is started 275 | # with --debug. This is primarily meant for use during the development of test 276 | # scripts. 277 | # 278 | # $1 - Commands to be executed. 279 | # 280 | # Examples 281 | # 282 | # test_debug "cat some_log_file" 283 | # 284 | # Returns the exit code of the last command executed in debug mode or 0 285 | # otherwise. 286 | test_debug() { 287 | test "$debug" = "" || eval "$1" 288 | } 289 | 290 | test_eval_() { 291 | # This is a separate function because some tests use 292 | # "return" to end a test_expect_success block early. 293 | eval &3 2>&4 "$*" 294 | } 295 | 296 | test_run_() { 297 | test_cleanup=: 298 | expecting_failure=$2 299 | test_eval_ "$1" 300 | eval_ret=$? 301 | 302 | if test -z "$immediate" || test $eval_ret = 0 || test -n "$expecting_failure"; then 303 | test_eval_ "$test_cleanup" 304 | fi 305 | if test "$verbose" = "t" && test -n "$HARNESS_ACTIVE"; then 306 | echo "" 307 | fi 308 | return "$eval_ret" 309 | } 310 | 311 | test_skip_() { 312 | test_count=$(($test_count + 1)) 313 | to_skip= 314 | for skp in $SKIP_TESTS; do 315 | case $this_test.$test_count in 316 | $skp) 317 | to_skip=t 318 | break 319 | esac 320 | done 321 | if test -z "$to_skip" && test -n "$test_prereq" && ! test_have_prereq "$test_prereq"; then 322 | to_skip=t 323 | fi 324 | case "$to_skip" in 325 | t) 326 | of_prereq= 327 | if test "$missing_prereq" != "$test_prereq"; then 328 | of_prereq=" of $test_prereq" 329 | fi 330 | 331 | say_color skip >&3 "skipping test: $@" 332 | say_color skip "ok $test_count # skip $1 (missing $missing_prereq${of_prereq})" 333 | : true 334 | ;; 335 | *) 336 | false 337 | ;; 338 | esac 339 | } 340 | 341 | # Public: Run test commands and expect them to succeed. 342 | # 343 | # When the test passed, an "ok" message is printed and the number of successful 344 | # tests is incremented. When it failed, a "not ok" message is printed and the 345 | # number of failed tests is incremented. 346 | # 347 | # With --immediate, exit test immediately upon the first failed test. 348 | # 349 | # Usually takes two arguments: 350 | # $1 - Test description 351 | # $2 - Commands to be executed. 352 | # 353 | # With three arguments, the first will be taken to be a prerequisite: 354 | # $1 - Comma-separated list of test prerequisites. The test will be skipped if 355 | # not all of the given prerequisites are set. To negate a prerequisite, 356 | # put a "!" in front of it. 357 | # $2 - Test description 358 | # $3 - Commands to be executed. 359 | # 360 | # Examples 361 | # 362 | # test_expect_success \ 363 | # 'git-write-tree should be able to write an empty tree.' \ 364 | # 'tree=$(git-write-tree)' 365 | # 366 | # # Test depending on one prerequisite. 367 | # test_expect_success TTY 'git --paginate rev-list uses a pager' \ 368 | # ' ... ' 369 | # 370 | # # Multiple prerequisites are separated by a comma. 371 | # test_expect_success PERL,PYTHON 'yo dawg' \ 372 | # ' test $(perl -E 'print eval "1 +" . qx[python -c "print 2"]') == "4" ' 373 | # 374 | # Returns nothing. 375 | test_expect_success() { 376 | test "$#" = 3 && { test_prereq=$1; shift; } || test_prereq= 377 | test "$#" = 2 || error "bug in the test script: not 2 or 3 parameters to test_expect_success" 378 | export test_prereq 379 | if ! test_skip_ "$@"; then 380 | say >&3 "expecting success: $2" 381 | if test_run_ "$2"; then 382 | test_ok_ "$1" 383 | else 384 | test_failure_ "$@" 385 | fi 386 | fi 387 | echo >&3 "" 388 | } 389 | 390 | # Public: Run test commands and expect them to fail. Used to demonstrate a known 391 | # breakage. 392 | # 393 | # This is NOT the opposite of test_expect_success, but rather used to mark a 394 | # test that demonstrates a known breakage. 395 | # 396 | # When the test passed, an "ok" message is printed and the number of fixed tests 397 | # is incremented. When it failed, a "not ok" message is printed and the number 398 | # of tests still broken is incremented. 399 | # 400 | # Failures from these tests won't cause --immediate to stop. 401 | # 402 | # Usually takes two arguments: 403 | # $1 - Test description 404 | # $2 - Commands to be executed. 405 | # 406 | # With three arguments, the first will be taken to be a prerequisite: 407 | # $1 - Comma-separated list of test prerequisites. The test will be skipped if 408 | # not all of the given prerequisites are set. To negate a prerequisite, 409 | # put a "!" in front of it. 410 | # $2 - Test description 411 | # $3 - Commands to be executed. 412 | # 413 | # Returns nothing. 414 | test_expect_failure() { 415 | test "$#" = 3 && { test_prereq=$1; shift; } || test_prereq= 416 | test "$#" = 2 || error "bug in the test script: not 2 or 3 parameters to test_expect_failure" 417 | export test_prereq 418 | if ! test_skip_ "$@"; then 419 | say >&3 "checking known breakage: $2" 420 | if test_run_ "$2" expecting_failure; then 421 | test_known_broken_ok_ "$1" 422 | else 423 | test_known_broken_failure_ "$1" 424 | fi 425 | fi 426 | echo >&3 "" 427 | } 428 | 429 | # Public: Run command and ensure that it fails in a controlled way. 430 | # 431 | # Use it instead of "! ". For example, when dies due to a 432 | # segfault, test_must_fail diagnoses it as an error, while "! " would 433 | # mistakenly be treated as just another expected failure. 434 | # 435 | # This is one of the prefix functions to be used inside test_expect_success or 436 | # test_expect_failure. 437 | # 438 | # $1.. - Command to be executed. 439 | # 440 | # Examples 441 | # 442 | # test_expect_success 'complain and die' ' 443 | # do something && 444 | # do something else && 445 | # test_must_fail git checkout ../outerspace 446 | # ' 447 | # 448 | # Returns 1 if the command succeeded (exit code 0). 449 | # Returns 1 if the command died by signal (exit codes 130-192) 450 | # Returns 1 if the command could not be found (exit code 127). 451 | # Returns 0 otherwise. 452 | test_must_fail() { 453 | "$@" 454 | exit_code=$? 455 | if test $exit_code = 0; then 456 | echo >&2 "test_must_fail: command succeeded: $*" 457 | return 1 458 | elif test $exit_code -gt 129 -a $exit_code -le 192; then 459 | echo >&2 "test_must_fail: died by signal: $*" 460 | return 1 461 | elif test $exit_code = 127; then 462 | echo >&2 "test_must_fail: command not found: $*" 463 | return 1 464 | fi 465 | return 0 466 | } 467 | 468 | # Public: Run command and ensure that it succeeds or fails in a controlled way. 469 | # 470 | # Similar to test_must_fail, but tolerates success too. Use it instead of 471 | # " || :" to catch failures caused by a segfault, for instance. 472 | # 473 | # This is one of the prefix functions to be used inside test_expect_success or 474 | # test_expect_failure. 475 | # 476 | # $1.. - Command to be executed. 477 | # 478 | # Examples 479 | # 480 | # test_expect_success 'some command works without configuration' ' 481 | # test_might_fail git config --unset all.configuration && 482 | # do something 483 | # ' 484 | # 485 | # Returns 1 if the command died by signal (exit codes 130-192) 486 | # Returns 1 if the command could not be found (exit code 127). 487 | # Returns 0 otherwise. 488 | test_might_fail() { 489 | "$@" 490 | exit_code=$? 491 | if test $exit_code -gt 129 -a $exit_code -le 192; then 492 | echo >&2 "test_might_fail: died by signal: $*" 493 | return 1 494 | elif test $exit_code = 127; then 495 | echo >&2 "test_might_fail: command not found: $*" 496 | return 1 497 | fi 498 | return 0 499 | } 500 | 501 | # Public: Run command and ensure it exits with a given exit code. 502 | # 503 | # This is one of the prefix functions to be used inside test_expect_success or 504 | # test_expect_failure. 505 | # 506 | # $1 - Expected exit code. 507 | # $2.. - Command to be executed. 508 | # 509 | # Examples 510 | # 511 | # test_expect_success 'Merge with d/f conflicts' ' 512 | # test_expect_code 1 git merge "merge msg" B master 513 | # ' 514 | # 515 | # Returns 0 if the expected exit code is returned or 1 otherwise. 516 | test_expect_code() { 517 | want_code=$1 518 | shift 519 | "$@" 520 | exit_code=$? 521 | if test $exit_code = $want_code; then 522 | return 0 523 | fi 524 | 525 | echo >&2 "test_expect_code: command exited with $exit_code, we wanted $want_code $*" 526 | return 1 527 | } 528 | 529 | # Public: Compare two files to see if expected output matches actual output. 530 | # 531 | # The TEST_CMP variable defines the command used for the comparision; it 532 | # defaults to "diff -u". Only when the test script was started with --verbose, 533 | # will the command's output, the diff, be printed to the standard output. 534 | # 535 | # This is one of the prefix functions to be used inside test_expect_success or 536 | # test_expect_failure. 537 | # 538 | # $1 - Path to file with expected output. 539 | # $2 - Path to file with actual output. 540 | # 541 | # Examples 542 | # 543 | # test_expect_success 'foo works' ' 544 | # echo expected >expected && 545 | # foo >actual && 546 | # test_cmp expected actual 547 | # ' 548 | # 549 | # Returns the exit code of the command set by TEST_CMP. 550 | test_cmp() { 551 | ${TEST_CMP:-diff -u} "$@" 552 | } 553 | 554 | # Public: Schedule cleanup commands to be run unconditionally at the end of a 555 | # test. 556 | # 557 | # If some cleanup command fails, the test will not pass. With --immediate, no 558 | # cleanup is done to help diagnose what went wrong. 559 | # 560 | # This is one of the prefix functions to be used inside test_expect_success or 561 | # test_expect_failure. 562 | # 563 | # $1.. - Commands to prepend to the list of cleanup commands. 564 | # 565 | # Examples 566 | # 567 | # test_expect_success 'test core.capslock' ' 568 | # git config core.capslock true && 569 | # test_when_finished "git config --unset core.capslock" && 570 | # do_something 571 | # ' 572 | # 573 | # Returns the exit code of the last cleanup command executed. 574 | test_when_finished() { 575 | test_cleanup="{ $* 576 | } && (exit \"\$eval_ret\"); eval_ret=\$?; $test_cleanup" 577 | } 578 | 579 | # Public: Summarize test results and exit with an appropriate error code. 580 | # 581 | # Must be called at the end of each test script. 582 | # 583 | # Can also be used to stop tests early and skip all remaining tests. For this, 584 | # set skip_all to a string explaining why the tests were skipped before calling 585 | # test_done. 586 | # 587 | # Examples 588 | # 589 | # # Each test script must call test_done at the end. 590 | # test_done 591 | # 592 | # # Skip all remaining tests if prerequisite is not set. 593 | # if ! test_have_prereq PERL; then 594 | # skip_all='skipping perl interface tests, perl not available' 595 | # test_done 596 | # fi 597 | # 598 | # Returns 0 if all tests passed or 1 if there was a failure. 599 | test_done() { 600 | EXIT_OK=t 601 | 602 | if test -z "$HARNESS_ACTIVE"; then 603 | test_results_dir="$SHARNESS_TEST_DIRECTORY/test-results" 604 | mkdir -p "$test_results_dir" 605 | test_results_path="$test_results_dir/${SHARNESS_TEST_FILE%.$SHARNESS_TEST_EXTENSION}.$$.counts" 606 | 607 | cat >>"$test_results_path" <<-EOF 608 | total $test_count 609 | success $test_success 610 | fixed $test_fixed 611 | broken $test_broken 612 | failed $test_failure 613 | 614 | EOF 615 | fi 616 | 617 | if test "$test_fixed" != 0; then 618 | say_color error "# $test_fixed known breakage(s) vanished; please update test(s)" 619 | fi 620 | if test "$test_broken" != 0; then 621 | say_color warn "# still have $test_broken known breakage(s)" 622 | fi 623 | if test "$test_broken" != 0 || test "$test_fixed" != 0; then 624 | test_remaining=$(( $test_count - $test_broken - $test_fixed )) 625 | msg="remaining $test_remaining test(s)" 626 | else 627 | test_remaining=$test_count 628 | msg="$test_count test(s)" 629 | fi 630 | 631 | case "$test_failure" in 632 | 0) 633 | # Maybe print SKIP message 634 | if test -n "$skip_all" && test $test_count -gt 0; then 635 | error "Can't use skip_all after running some tests" 636 | fi 637 | [ -z "$skip_all" ] || skip_all=" # SKIP $skip_all" 638 | 639 | if test $test_remaining -gt 0; then 640 | say_color pass "# passed all $msg" 641 | fi 642 | say "1..$test_count$skip_all" 643 | 644 | test -d "$remove_trash" && 645 | cd "$(dirname "$remove_trash")" && 646 | rm -rf "$(basename "$remove_trash")" 647 | 648 | exit 0 ;; 649 | 650 | *) 651 | say_color error "# failed $test_failure among $msg" 652 | say "1..$test_count" 653 | 654 | exit 1 ;; 655 | 656 | esac 657 | } 658 | 659 | # Public: Root directory containing tests. Tests can override this variable, 660 | # e.g. for testing Sharness itself. 661 | : ${SHARNESS_TEST_DIRECTORY:=$(pwd)} 662 | export SHARNESS_TEST_DIRECTORY 663 | 664 | # Public: Build directory that will be added to PATH. By default, it is set to 665 | # the parent directory of SHARNESS_TEST_DIRECTORY. 666 | : ${SHARNESS_BUILD_DIRECTORY:="$SHARNESS_TEST_DIRECTORY/.."} 667 | PATH="$SHARNESS_BUILD_DIRECTORY:$PATH" 668 | export PATH SHARNESS_BUILD_DIRECTORY 669 | 670 | # Public: Path to test script currently executed. 671 | SHARNESS_TEST_FILE="./$(basename "$0")" 672 | export SHARNESS_TEST_FILE 673 | 674 | # Prepare test area. 675 | test_dir="trash directory.$(basename "$SHARNESS_TEST_FILE" ".$SHARNESS_TEST_EXTENSION")" 676 | test -n "$root" && test_dir="$root/$test_dir" 677 | case "$test_dir" in 678 | /*) SHARNESS_TRASH_DIRECTORY="$test_dir" ;; 679 | *) SHARNESS_TRASH_DIRECTORY="$SHARNESS_TEST_DIRECTORY/$test_dir" ;; 680 | esac 681 | test "$debug" = "t" || remove_trash="$SHARNESS_TRASH_DIRECTORY" 682 | rm -rf "$test_dir" || { 683 | EXIT_OK=t 684 | echo >&5 "FATAL: Cannot prepare test area" 685 | exit 1 686 | } 687 | 688 | # Public: Empty trash directory, the test area, provided for each test. The HOME 689 | # variable is set to that directory too. 690 | export SHARNESS_TRASH_DIRECTORY 691 | 692 | HOME="$SHARNESS_TRASH_DIRECTORY" 693 | export HOME 694 | 695 | mkdir -p "$test_dir" || exit 1 696 | # Use -P to resolve symlinks in our working directory so that the cwd 697 | # in subprocesses like git equals our $PWD (for pathname comparisons). 698 | cd -P "$test_dir" || exit 1 699 | 700 | this_test=${SHARNESS_TEST_FILE##*/} 701 | this_test=${this_test%.$SHARNESS_TEST_EXTENSION} 702 | for skp in $SKIP_TESTS; do 703 | case "$this_test" in 704 | $skp) 705 | say_color info >&3 "skipping test $this_test altogether" 706 | skip_all="skip all tests in $this_test" 707 | test_done 708 | esac 709 | done 710 | 711 | # vi: set ts=4 sw=4 noet : 712 | -------------------------------------------------------------------------------- /tests/t0001-sanity-checks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | test_description='Sanity checks' 4 | cd "$(dirname "$0")" 5 | . ./setup.sh 6 | 7 | test_expect_success 'Make sure we can run pass' ' 8 | "$PASS" --help | grep "pass: the standard unix password manager" 9 | ' 10 | 11 | test_expect_success 'Make sure we can initialize our test store' ' 12 | "$PASS" init $KEY1 && 13 | [[ -e "$PASSWORD_STORE_DIR/.gpg-id" ]] && 14 | [[ $(cat "$PASSWORD_STORE_DIR/.gpg-id") == "$KEY1" ]] 15 | ' 16 | 17 | test_done 18 | -------------------------------------------------------------------------------- /tests/t0010-generate-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | test_description='Test generate' 4 | cd "$(dirname "$0")" 5 | . ./setup.sh 6 | 7 | test_expect_success 'Test "generate" command' ' 8 | "$PASS" init $KEY1 && 9 | "$PASS" generate cred 19 && 10 | [[ $("$PASS" show cred | wc -m) -eq 20 ]] 11 | ' 12 | 13 | test_expect_success 'Test replacement of first line' ' 14 | "$PASS" insert -m cred2 <<<"$(printf "this is a big\\npassword\\nwith\\nmany\\nlines\\nin it bla bla")" && 15 | "$PASS" generate -i cred2 23 && 16 | [[ $("$PASS" show cred2) == "$(printf "%s\\npassword\\nwith\\nmany\\nlines\\nin it bla bla" "$("$PASS" show cred2 | head -n 1)")" ]] 17 | ' 18 | 19 | test_done 20 | -------------------------------------------------------------------------------- /tests/t0020-show-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | test_description='Test show' 4 | cd "$(dirname "$0")" 5 | . ./setup.sh 6 | 7 | test_expect_success 'Test "show" command' ' 8 | "$PASS" init $KEY1 && 9 | "$PASS" generate cred1 20 && 10 | "$PASS" show cred1 11 | ' 12 | 13 | test_expect_success 'Test "show" command with spaces' ' 14 | "$PASS" insert -e "I am a cred with lots of spaces"<<<"BLAH!!" && 15 | [[ $("$PASS" show "I am a cred with lots of spaces") == "BLAH!!" ]] 16 | ' 17 | 18 | test_expect_success 'Test "show" command with unicode' ' 19 | "$PASS" generate 🏠 && 20 | "$PASS" show | grep -q '🏠' 21 | ' 22 | 23 | test_expect_success 'Test "show" of nonexistant password' ' 24 | test_must_fail "$PASS" show cred2 25 | ' 26 | 27 | test_done 28 | -------------------------------------------------------------------------------- /tests/t0050-mv-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | test_description='Test mv command' 4 | cd "$(dirname "$0")" 5 | . ./setup.sh 6 | 7 | INITIAL_PASSWORD="bla bla bla will we make it!!" 8 | 9 | test_expect_success 'Basic move command' ' 10 | "$PASS" init $KEY1 && 11 | "$PASS" git init && 12 | "$PASS" insert -e cred1 <<<"$INITIAL_PASSWORD" && 13 | "$PASS" mv cred1 cred2 && 14 | [[ -e $PASSWORD_STORE_DIR/cred2.gpg && ! -e $PASSWORD_STORE_DIR/cred1.gpg ]] 15 | ' 16 | 17 | test_expect_success 'Directory creation' ' 18 | "$PASS" mv cred2 directory/ && 19 | [[ -d $PASSWORD_STORE_DIR/directory && -e $PASSWORD_STORE_DIR/directory/cred2.gpg ]] 20 | ' 21 | 22 | test_expect_success 'Directory creation with file rename and empty directory removal' ' 23 | "$PASS" mv directory/cred2 "new directory with spaces"/cred && 24 | [[ -d $PASSWORD_STORE_DIR/"new directory with spaces" && -e $PASSWORD_STORE_DIR/"new directory with spaces"/cred.gpg && ! -e $PASSWORD_STORE_DIR/directory ]] 25 | ' 26 | 27 | test_expect_success 'Directory rename' ' 28 | "$PASS" mv "new directory with spaces" anotherdirectory && 29 | [[ -d $PASSWORD_STORE_DIR/anotherdirectory && -e $PASSWORD_STORE_DIR/anotherdirectory/cred.gpg && ! -e $PASSWORD_STORE_DIR/"new directory with spaces" ]] 30 | ' 31 | 32 | test_expect_success 'Directory move into new directory' ' 33 | "$PASS" mv anotherdirectory "new directory with spaces"/ && 34 | [[ -d $PASSWORD_STORE_DIR/"new directory with spaces"/anotherdirectory && -e $PASSWORD_STORE_DIR/"new directory with spaces"/anotherdirectory/cred.gpg && ! -e $PASSWORD_STORE_DIR/anotherdirectory ]] 35 | ' 36 | 37 | test_expect_success 'Multi-directory creation and multi-directory empty removal' ' 38 | "$PASS" mv "new directory with spaces"/anotherdirectory/cred new1/new2/new3/new4/thecred && 39 | "$PASS" mv new1/new2/new3/new4/thecred cred && 40 | [[ ! -d $PASSWORD_STORE_DIR/"new directory with spaces"/anotherdirectory && ! -d $PASSWORD_STORE_DIR/new1/new2/new3/new4 && -e $PASSWORD_STORE_DIR/cred.gpg ]] 41 | ' 42 | 43 | test_expect_success 'Password made it until the end' ' 44 | [[ $("$PASS" show cred) == "$INITIAL_PASSWORD" ]] 45 | ' 46 | 47 | test_expect_success 'Git is consistent' ' 48 | [[ -z $(git status --porcelain 2>&1) ]] 49 | ' 50 | 51 | test_done 52 | -------------------------------------------------------------------------------- /tests/t0060-rm-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | test_description='Test rm' 4 | cd "$(dirname "$0")" 5 | . ./setup.sh 6 | 7 | test_expect_success 'Test "rm" command' ' 8 | "$PASS" init $KEY1 && 9 | "$PASS" generate cred1 43 && 10 | "$PASS" rm cred1 && 11 | [[ ! -e $PASSWORD_STORE_DIR/cred1.gpg ]] 12 | ' 13 | 14 | test_expect_success 'Test "rm" command with spaces' ' 15 | "$PASS" generate "hello i have spaces" 43 && 16 | [[ -e $PASSWORD_STORE_DIR/"hello i have spaces".gpg ]] && 17 | "$PASS" rm "hello i have spaces" && 18 | [[ ! -e $PASSWORD_STORE_DIR/"hello i have spaces".gpg ]] 19 | ' 20 | 21 | test_expect_success 'Test "rm" of non-existent password' ' 22 | test_must_fail "$PASS" rm does-not-exist 23 | ' 24 | 25 | test_done 26 | -------------------------------------------------------------------------------- /tests/t0100-insert-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | test_description='Test insert' 4 | cd "$(dirname "$0")" 5 | . ./setup.sh 6 | 7 | test_expect_success 'Test "insert" command' ' 8 | "$PASS" init $KEY1 && 9 | echo "Hello world" | "$PASS" insert -e cred1 && 10 | [[ $("$PASS" show cred1) == "Hello world" ]] 11 | ' 12 | 13 | test_done 14 | -------------------------------------------------------------------------------- /tests/t0200-edit-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | test_description='Test edit' 4 | cd "$(dirname "$0")" 5 | . ./setup.sh 6 | 7 | test_expect_success 'Test "edit" command' ' 8 | "$PASS" init $KEY1 && 9 | "$PASS" generate cred1 90 && 10 | export FAKE_EDITOR_PASSWORD="big fat fake password" && 11 | export PATH="$TEST_HOME:$PATH" 12 | export EDITOR="fake-editor-change-password.sh" && 13 | "$PASS" edit cred1 && 14 | [[ $("$PASS" show cred1) == "$FAKE_EDITOR_PASSWORD" ]] 15 | ' 16 | 17 | test_done 18 | -------------------------------------------------------------------------------- /tests/t0300-reencryption.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | test_description='Reencryption consistency' 4 | cd "$(dirname "$0")" 5 | . ./setup.sh 6 | 7 | INITIAL_PASSWORD="will this password live? a big question indeed..." 8 | 9 | canonicalize_gpg_keys() { 10 | $GPG --list-keys --with-colons "$@" | sed -n 's/sub:[^:]*:[^:]*:[^:]*:\([^:]*\):[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[a-zA-Z]*e[a-zA-Z]*:.*/\1/p' | LC_ALL=C sort -u 11 | } 12 | gpg_keys_from_encrypted_file() { 13 | $GPG -v --no-secmem-warning --no-permission-warning --decrypt --list-only --keyid-format long "$1" 2>&1 | grep "public key is" | cut -d ' ' -f 5 | LC_ALL=C sort -u 14 | } 15 | gpg_keys_from_group() { 16 | local output="$($GPG --list-config --with-colons | sed -n "s/^cfg:group:$1:\\(.*\\)/\\1/p" | head -n 1)" 17 | local saved_ifs="$IFS" 18 | IFS=";" 19 | local keys=( $output ) 20 | IFS="$saved_ifs" 21 | canonicalize_gpg_keys "${keys[@]}" 22 | } 23 | 24 | test_expect_success 'Setup initial key and git' ' 25 | "$PASS" init $KEY1 && "$PASS" git init 26 | ' 27 | 28 | test_expect_success 'Root key encryption' ' 29 | "$PASS" insert -e folder/cred1 <<<"$INITIAL_PASSWORD" && 30 | [[ $(canonicalize_gpg_keys "$KEY1") == "$(gpg_keys_from_encrypted_file "$PASSWORD_STORE_DIR/folder/cred1.gpg")" ]] 31 | ' 32 | 33 | test_expect_success 'Reencryption root single key' ' 34 | "$PASS" init $KEY2 && 35 | [[ $(canonicalize_gpg_keys "$KEY2") == "$(gpg_keys_from_encrypted_file "$PASSWORD_STORE_DIR/folder/cred1.gpg")" ]] 36 | ' 37 | 38 | test_expect_success 'Reencryption root multiple key' ' 39 | "$PASS" init $KEY2 $KEY3 $KEY1 && 40 | [[ $(canonicalize_gpg_keys $KEY2 $KEY3 $KEY1) == "$(gpg_keys_from_encrypted_file "$PASSWORD_STORE_DIR/folder/cred1.gpg")" ]] 41 | ' 42 | 43 | test_expect_success 'Reencryption root multiple key with string' ' 44 | "$PASS" init $KEY2 $KEY3 $KEY1 "pass test key 4" && 45 | [[ $(canonicalize_gpg_keys $KEY2 $KEY3 $KEY1 $KEY4) == "$(gpg_keys_from_encrypted_file "$PASSWORD_STORE_DIR/folder/cred1.gpg")" ]] 46 | ' 47 | 48 | test_expect_success 'Reencryption root group' ' 49 | "$PASS" init group1 && 50 | [[ $(gpg_keys_from_group group1) == "$(gpg_keys_from_encrypted_file "$PASSWORD_STORE_DIR/folder/cred1.gpg")" ]] 51 | ' 52 | 53 | test_expect_success 'Reencryption root group with spaces' ' 54 | "$PASS" init "big group" && 55 | [[ $(gpg_keys_from_group "big group") == "$(gpg_keys_from_encrypted_file "$PASSWORD_STORE_DIR/folder/cred1.gpg")" ]] 56 | ' 57 | 58 | test_expect_success 'Reencryption root group with spaces and other keys' ' 59 | "$PASS" init "big group" $KEY3 $KEY1 $KEY2 && 60 | [[ $(canonicalize_gpg_keys $KEY3 $KEY1 $KEY2 $(gpg_keys_from_group "big group")) == "$(gpg_keys_from_encrypted_file "$PASSWORD_STORE_DIR/folder/cred1.gpg")" ]] 61 | ' 62 | 63 | test_expect_success 'Reencryption root group and other keys' ' 64 | "$PASS" init group2 $KEY3 $KEY1 $KEY2 && 65 | [[ $(canonicalize_gpg_keys $KEY3 $KEY1 $KEY2 $(gpg_keys_from_group group2)) == "$(gpg_keys_from_encrypted_file "$PASSWORD_STORE_DIR/folder/cred1.gpg")" ]] 66 | ' 67 | 68 | test_expect_success 'Reencryption root group to identical individual with no file change' ' 69 | oldfile="$SHARNESS_TRASH_DIRECTORY/$RANDOM.$RANDOM.$RANDOM.$RANDOM.$RANDOM" && 70 | "$PASS" init group1 && 71 | cp "$PASSWORD_STORE_DIR/folder/cred1.gpg" "$oldfile" && 72 | "$PASS" init $KEY4 $KEY2 && 73 | test_cmp "$PASSWORD_STORE_DIR/folder/cred1.gpg" "$oldfile" 74 | ' 75 | 76 | test_expect_success 'Reencryption subfolder multiple keys, copy' ' 77 | "$PASS" init -p anotherfolder $KEY3 $KEY1 && 78 | "$PASS" cp folder/cred1 anotherfolder/ && 79 | [[ $(canonicalize_gpg_keys $KEY1 $KEY3) == "$(gpg_keys_from_encrypted_file "$PASSWORD_STORE_DIR/anotherfolder/cred1.gpg")" ]] 80 | ' 81 | 82 | test_expect_success 'Reencryption subfolder multiple keys, move, deinit' ' 83 | "$PASS" init -p anotherfolder2 $KEY3 $KEY4 $KEY2 && 84 | "$PASS" mv -f anotherfolder anotherfolder2/ && 85 | [[ $(canonicalize_gpg_keys $KEY1 $KEY3) == "$(gpg_keys_from_encrypted_file "$PASSWORD_STORE_DIR/anotherfolder2/anotherfolder/cred1.gpg")" ]] && 86 | "$PASS" init -p anotherfolder2/anotherfolder "" && 87 | [[ $(canonicalize_gpg_keys $KEY3 $KEY4 $KEY2) == "$(gpg_keys_from_encrypted_file "$PASSWORD_STORE_DIR/anotherfolder2/anotherfolder/cred1.gpg")" ]] 88 | ' 89 | 90 | test_expect_success 'Reencryption skips links' ' 91 | ln -s "$PASSWORD_STORE_DIR/folder/cred1.gpg" "$PASSWORD_STORE_DIR/folder/linked_cred.gpg" && 92 | [[ -L $PASSWORD_STORE_DIR/folder/linked_cred.gpg ]] && 93 | git add "$PASSWORD_STORE_DIR/folder/linked_cred.gpg" && 94 | git commit "$PASSWORD_STORE_DIR/folder/linked_cred.gpg" -m "Added linked cred" && 95 | "$PASS" init -p folder $KEY3 && 96 | [[ -L $PASSWORD_STORE_DIR/folder/linked_cred.gpg ]] 97 | ' 98 | 99 | #TODO: test with more varieties of move and copy! 100 | 101 | test_expect_success 'Password lived through all transformations' ' 102 | [[ $("$PASS" show anotherfolder2/anotherfolder/cred1) == "$INITIAL_PASSWORD" ]] 103 | ' 104 | 105 | test_expect_success 'Git picked up all changes throughout' ' 106 | [[ -z $(git status --porcelain 2>&1) ]] 107 | ' 108 | 109 | test_done 110 | -------------------------------------------------------------------------------- /tests/t0400-grep.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | test_description='Grep check' 4 | cd "$(dirname "$0")" 5 | . ./setup.sh 6 | 7 | test_expect_success 'Make sure grep prints normal lines' ' 8 | "$PASS" init $KEY1 && 9 | "$PASS" insert -e blah1 <<<"hello" && 10 | "$PASS" insert -e blah2 <<<"my name is" && 11 | "$PASS" insert -e folder/blah3 <<<"I hate computers" && 12 | "$PASS" insert -e blah4 <<<"me too!" && 13 | "$PASS" insert -e folder/where/blah5 <<<"They are hell" && 14 | results="$("$PASS" grep hell)" && 15 | [[ $(wc -l <<<"$results") -eq 4 ]] && 16 | grep -q blah5 <<<"$results" && 17 | grep -q blah1 <<<"$results" && 18 | grep -q "They are" <<<"$results" 19 | ' 20 | 21 | test_expect_success 'Test passing the "-i" option to grep' ' 22 | "$PASS" init $KEY1 && 23 | "$PASS" insert -e blah1 <<<"I wonder..." && 24 | "$PASS" insert -e blah2 <<<"Will it ignore" && 25 | "$PASS" insert -e blah3 <<<"case when searching?" && 26 | "$PASS" insert -e folder/blah4 <<<"Yes, it does. Wonderful!" && 27 | results="$("$PASS" grep -i wonder)" && 28 | [[ $(wc -l <<<"$results") -eq 4 ]] && 29 | grep -q blah1 <<<"$results" && 30 | grep -q blah4 <<<"$results" 31 | ' 32 | 33 | test_done 34 | -------------------------------------------------------------------------------- /tests/t0500-find.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | test_description='Find check' 4 | cd "$(dirname "$0")" 5 | . ./setup.sh 6 | 7 | test_expect_success 'Make sure find resolves correct files' ' 8 | "$PASS" init $KEY1 && 9 | "$PASS" generate Something/neat 19 && 10 | "$PASS" generate Anotherthing/okay 38 && 11 | "$PASS" generate Fish 12 && 12 | "$PASS" generate Fishthings 122 && 13 | "$PASS" generate Fishies/stuff 21 && 14 | "$PASS" generate Fishies/otherstuff 1234 && 15 | [[ $("$PASS" find fish | sed "s/^[ \`|-]*//g;s/$(printf \\x1b)\\[[0-9;]*[a-zA-Z]//g" | tr "\\n" -) == "Search Terms: fish-Fish-Fishies-otherstuff-stuff-Fishthings-" ]] 16 | ' 17 | 18 | test_done 19 | --------------------------------------------------------------------------------