├── LICENSE ├── README.md ├── nsa ├── pics ├── banner.png ├── comparision.png ├── comparision.xcf ├── screenshot_init.png ├── screenshot_init.xcf ├── screenshot_someuse.png └── screenshot_someuse.xcf └── tests ├── Makefile ├── striptimestamps.py └── truth ├── -h ├── -v ├── data ├── test_db.add.str.cln ├── test_db.create.str.cln ├── test_db.del.str.cln └── test_db.mod.str.cln /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MIGRATED TO [_https://codeberg.org/caveman/nsapass_](https://codeberg.org/caveman/nsapass) 2 | 3 | ## WARNING: This repository is an outdated version of [nsapass](https://codeberg.org/caveman/nsapass), and is kept only to link visitors to migrate to the [newer repository](https://codeberg.org/caveman/nsapass). 4 | 5 | --- 6 | --- 7 | --- 8 | 9 | # Synopsis 10 | _nsapass_ is the simplest, most usable and most secure 11 | [CLI](https://en.wikipedia.org/wiki/Command-line_interface) passwords 12 | manager for GNU/Linux. Because all other password managers are either too 13 | complex to be audit-able, lack critical features, or both. 14 | 15 |

16 |

Fig 1. Initial setup.

17 | 18 |

19 |

Fig 2. Some use.

20 | 21 | ## Features 22 | 23 | - **No funny memory bugs:** Thanks implementing _nsapass_ 24 | entirely in Python, we easily win this by simply taking advantage of the 25 | decades already spent by Python's many highly-skilled monkeys in fighting 26 | all sorts of funny memory bugs and security issues. 27 | 28 | - **Easy to audit:** _nsapass_ is just a single file, in about `800` 29 | lines of code, including its configurations. This makes it actually 30 | auditable in a practical manner. 31 | 32 | Auditability of a password manager is extremely critical and must be a 33 | fundamental requirement. Compare this to the countless 34 | practically-unauditable C/C++ lines in the likes of 35 | [_keepassxc_](https://github.com/keepassxreboot/keepassxc) (yikes!). 36 | Would you entrust your passwords to them? I would not, henceforth 37 | _nsapass_. 38 | 39 | - **Very powerful:** _nsapass_ is simple, but not stupidly. It allows you 40 | to define your own external commands for encrypting, decrypting or doing 41 | whatever you want with your retrieved passwords/keyfiles/URIs entries, by 42 | simply editing configuration variables `ENCRYPT_COMMAND`, 43 | `DECRYPT_COMMAND`, `DO_COMMANDS` which are inside `nsa` script itself. 44 | 45 | This allows for neat automations. The argument `-c` allows to define a 46 | sequence of actions. E.g. with default `DO_COMMANDS`, `nsa do caveman -c 47 | copypass paste delclip` will 1st load the password into clipboard, paste 48 | it by emulating `Ctrl+V`, and then deleting the password in the 49 | clipboard. 50 | 51 | You can define your own automations, with your own magical external 52 | commands. E.g. you may even skip the clipboard and do it entirely using 53 | keyboard emulation? Your imagination is the limit! 54 | 55 | - **Less password typing:** First you load your passwords database into an 56 | _nsapass_ backend by`nsa start`, and then enjoy using it without password 57 | prompts by commands such a `nsa ls ...`, `nsa add ...`, etc. `nsa -h` 58 | for details. 59 | 60 | This not only enhances the convenience, but is necessary to practically 61 | increase our security, as password databases must be encrypted by 62 | high-entropy passwords that are _rarely_ typed. Minimising moments when 63 | we type such high-entropy password reduces password theft windows (e.g. 64 | look-behind-shoulder). 65 | 66 | - **Smart tags-based search for minimal typing:** All password entries are 67 | stored with tags. You don't have to fully type a tag's name. _nsapass_ 68 | has a smart tags lookup system. 69 | 70 | E.g. if you'd like to pick the entry associated with the tags `caveman 71 | protonmail`, you may identify it the boring way by `nsa do caveman protonmail`, or by 72 | just typing `nsa do c p` if it's the only entry with tags that begin with 73 | `c` and `p`. _nsapass_ will intelligently figure out that `c p` must 74 | have been referring to `caveman protonmail`, based on how unique the 75 | match is against tags of other entries in the database.. 76 | 77 | - **Common sense:** Your passwords database never touches the disk in plain 78 | text form. If you have disk swap memory, or cybernation, make sure 79 | they're encrypted (or disable them; who needs them these days?). 80 | 81 | ## A comparison 82 | 83 | - [_keepassxc_](https://github.com/keepassxreboot/keepassxc) has loads of 84 | lines of codes that makes it effectively not audit-able, and it's CLI is 85 | terrible. So it loses on both of the auditability and usability 86 | dimensions. 87 | - [_pass_](https://www.passwordstore.org/) has roughly about the same lines 88 | of codes as _nsapass_, so it is sort of auditable, but it: 89 | - Exposes each password entry as a file with a meaningful name (so that 90 | the user remembers it). The file names are obviously in plain text in 91 | the file system. If file names are relevant to passwords inside them, 92 | then it leaks information should the disk be stolen. If the file names 93 | are not relevant, then it becomes not usable as you'll need to remember 94 | odd names unrelated to passwords within them. 95 | 96 | _nsapass_ doesn't leak any information about the 97 | entries, as the whole database is stored in a single encrypted file. 98 | You will be free to choose the most memorable method to tag your entry, 99 | without conerns of leaking it in plain text in the file system. 100 | 101 | - Is limited to `gpg` for file encryption and decryption; a bloated tool. 102 | 103 | _nsapass_ not only allows you to use any file encryption and decryption 104 | tool of your choice, but also any key derivation function. 105 | 106 | - Lots of limitations: 107 | - E.g. _pass_'s password generation isn't able to generate desired 108 | passwords based on target entropy bits (at least not out of the box). 109 | 110 | _nsapass_ does this neatly with the `-b BITS` argument. 111 | 112 | - E.g. _pass_ uses the directory structure offered by the file system 113 | to organise its passwords. This requires too much typing to 114 | identify a given password. E.g. suppose that a password entry is 115 | stored in `path/to/foo/.../baz` and suppose that the entry is already 116 | made unique by `path/.../baz`, you will still need to type 117 | `path/to/foo/../baz` entirely. Why not just type `path baz` and let 118 | it figure out that you meant that? No good reason. 119 | 120 | _nsapass_ uses a smart tagging system that can effectively achieve 121 | that heirarichal partitioning of entries, without needing to type 122 | their names fully. E.g. in the example above, you can retrieve that 123 | entry by not only typing `path baz` (which is already great), but 124 | even by simply typing `p b` if those partial tags make the full tags 125 | unique already. 126 | 127 | - ... 128 | 129 | So, when considering the limited features of _pass_ compared to those of 130 | _nsapass_, _pass_ rather feels very bloated; _nsapass_ does much more 131 | with about the same size of about `800` lines of code. 132 | 133 | # Installation 134 | 135 | 1. **Optional:** Edit file `nsa` to apply your configurations. 136 | 1. Paste the file `nsa` in wherever you'd like it to be. Perhaps somewhere 137 | in `PATH`. 138 | 139 | # Tutorial 140 | 141 | ## Database creation and housekeeping 142 | 1. `nsa create` to create an empty database. This is done only once. 143 | 1. `nsa start` will load the nsapass server. 144 | 1. In a separate terminal, use commands `nsa (ls | add | del | mod | diff | 145 | commit | revert)` to modify the passwords database. Here is an example: 146 | 1. `nsa add -t caveman protonmail -b 256` will 147 | add an automatically generated password worth `256` many Shannon's 148 | entropy bits for my _ProtonMail_ account, using the default 149 | characters space (`printable`), and associates it with the tags 150 | `caveman protonmail` for convenient retrieval in the future. 151 | 1. `nsa diff` will view the total changes made so far to the passwords 152 | database. 153 | 1. If changes are not fine, undo them by `nsa del caveman 154 | protonmail`, or `nsa revert`. The latter will reset the database to 155 | latest committed version. 156 | 1. If changes are are good, then save them by `nsa commit`. Saving them 157 | is necessary to make the changes permanent. 158 | 1. When no longer in need of _nsapass_, execute `nsa stop` to stop the 159 | server. 160 | 161 | ## An example of convenient use 162 | 163 | I've added these shortcuts to my `i3` window manager: 164 | ``` 165 | bindsym $mod+i exec nsa do -c delclip 166 | bindsym --release $mod+comma exec nsa do -c copyuser 167 | bindsym --release $mod+period exec nsa do -c copypass 168 | bindsym --release $mod+g exec nsa do -c sleep copypass paste delclip enter 169 | bindsym --release $mod+shift+g exec nsa do -c sleep copyuser paste delclip enter copypass paste delclip enter 170 | ``` 171 | 172 | Suppose that I'd like use entries that associate with the tags `EXAMPLE TAGS`. 173 | I perform these in order: 174 | 175 | 1. I start the _nsapass_ server in some terminal by `nsa start`, and let it 176 | run there for as long as I want to access my passwords database. 177 | 1. I execute `nsa do EXAMPLE TAGS` (or just `nsa do E` if partial tags 178 | query `c p` uniquely identifies tags `EXAMPLE TAGS`) in order to load 179 | the entry. 180 | 1. Then I use the username, password or keyfile entries depending on the 181 | way the application works. 182 | - For general applications, I load username and password entries into 183 | the clipboard by `$mod+comma` and `$mod+period`, respectively, then 184 | paste them by `control+v`. I finally delete them by `$mod+i`. 185 | - For CLI applications that have a well defined order of prompts, such 186 | as `git`, I use `$mod+shift+g` to have _nsapass_ automatically 187 | copy-paste-delete username and password entries, successively, using a 188 | single shortcut. 189 | - For CLI applications that only need the password, such as `ssh 190 | user@server`, I use `$mod+g` which only copy-pastes-deletes the 191 | password. 192 | 193 | 194 | It should be also possible to (but I personally don't do it this way): 195 | 196 | - Make it even more convenient, by configuring shortcuts with GUI prompts, 197 | that take advantage of the fact that steps `nsa do EXAMPLE TAGS` and, 198 | say, `nsa do -c copyuser` can be combined in a single step `nsa do 199 | EXAMPLE TAGS -c copyuser`. 200 | - Automate command execution. E.g. command `z` could be defined in such a 201 | way that `nsa do EXAMPLE TAGS -c z ...` would execute `git push` and feed 202 | it with passwords as earlier. 203 | 204 | # Dependencies 205 | 206 | - Python. 207 | - Any key derivation function tool. Default: 208 | [`argon2`](https://github.com/p-h-c/phc-winner-argon2). 209 | - Any file encryption and decryption tool. Default: 210 | [`openssl`](https://www.openssl.org/). 211 | - Any external commands to do whatever you want with your entries. 212 | Default: [`xclip`](https://github.com/astrand/xclip) and 213 | [`xdotool`](http://www.semicomplete.com/projects/xdotool) for clipboard 214 | management and keyboard emulation, respectively. 215 | 216 | # Manual 217 | 218 | ``` 219 | usage: nsa [-h] [-v] [-V] [-C] [-i DIR] 220 | {create,chpass,start,stop,do,add,del,mod,ls,diff,commit,revert} ... 221 | 222 | optional arguments: 223 | -h, --help show this help message and exit 224 | -v show information about nsapass 225 | -V show debugging information 226 | -C disable colourful output 227 | -i DIR ipc directory 228 | 229 | commands: 230 | {create,chpass,start,stop,do,add,del,mod,ls,diff,commit,revert} 231 | create create a databases 232 | chpass change databases's password 233 | start starts nsapass 234 | stop stops nsapass and discards any uncommitted changes 235 | do do things (e.g. type passwords) 236 | add add an entry 237 | del delete an entry 238 | mod modify an entry 239 | ls view entries 240 | diff show modifications done so far 241 | commit commit changes to the database 242 | revert revert all uncommitted changed back to original 243 | ``` 244 | ``` 245 | usage: nsa create [-h] [-d DB] [-s] 246 | 247 | optional arguments: 248 | -h, --help show this help message and exit 249 | -d DB set passwords database path 250 | -s input from stdin 251 | ``` 252 | ``` 253 | usage: nsa chpass [-h] [-s] 254 | 255 | optional arguments: 256 | -h, --help show this help message and exit 257 | -s input from stdin 258 | ``` 259 | ``` 260 | usage: nsa start [-h] [-s] [-d DB] 261 | 262 | optional arguments: 263 | -h, --help show this help message and exit 264 | -s input from stdin 265 | -d DB set passwords database path 266 | ``` 267 | ``` 268 | usage: nsa stop [-h] [-s] 269 | 270 | optional arguments: 271 | -h, --help show this help message and exit 272 | -s input from stdin 273 | ``` 274 | ``` 275 | usage: nsa do [-h] [-s] [-c COMMANDS [COMMANDS ...]] [QUERY ...] 276 | 277 | positional arguments: 278 | QUERY query tags 279 | 280 | optional arguments: 281 | -h, --help show this help message and exit 282 | -s input from stdin 283 | -c COMMANDS [COMMANDS ...] 284 | perform actions specified in COMMANDS in order from 285 | left to right. COMMANDS are defined in option 286 | DO_COMMANDS, which currently are: copyuser, copypass, 287 | copyuri, delclip, paste, enter, keyfile, sleep 288 | ``` 289 | ``` 290 | usage: nsa add [-h] [-t TAG [TAG ...]] [-u USERNAME] [-p SET] [-o LETTERS] 291 | [-b BIT] [-l LEN] [-m] [-f PATH] [-r URI] [-n NOTE] [-s] [-z] 292 | 293 | optional arguments: 294 | -h, --help show this help message and exit 295 | -t TAG [TAG ...] new tags 296 | -u USERNAME new username 297 | -p SET pre-defined password letters set name 298 | -o LETTERS raw password letter options 299 | -b BIT generate BIT-entropy password from SET 300 | -l LEN generate LEN-long password from SET 301 | -m user-defined password 302 | -f PATH key/data file in PATH, or STDIN if "-" 303 | -r URI a uniform resource identifier 304 | -n NOTE a note 305 | -s input from stdin 306 | -z show passwords 307 | ``` 308 | ``` 309 | usage: nsa del [-h] [-s] [-z] [QUERY ...] 310 | 311 | positional arguments: 312 | QUERY query tags 313 | 314 | optional arguments: 315 | -h, --help show this help message and exit 316 | -s input from stdin 317 | -z show passwords 318 | ``` 319 | ``` 320 | usage: nsa mod [-h] [-t TAG [TAG ...]] [-u USERNAME] [-p SET] [-o LETTERS] 321 | [-b BIT] [-l LEN] [-m] [-f PATH] [-r URI] [-n NOTE] [-s] [-z] 322 | [QUERY ...] 323 | 324 | positional arguments: 325 | QUERY query tags 326 | 327 | optional arguments: 328 | -h, --help show this help message and exit 329 | -t TAG [TAG ...] new tags 330 | -u USERNAME new username 331 | -p SET pre-defined password letters set name 332 | -o LETTERS raw password letter options 333 | -b BIT generate BIT-entropy password from SET 334 | -l LEN generate LEN-long password from SET 335 | -m user-defined password 336 | -f PATH key/data file in PATH, or STDIN if "-" 337 | -r URI a uniform resource identifier 338 | -n NOTE a note 339 | -s input from stdin 340 | -z show passwords 341 | ``` 342 | ``` 343 | usage: nsa ls [-h] [-s] [-z] [QUERY ...] 344 | 345 | positional arguments: 346 | QUERY query tags 347 | 348 | optional arguments: 349 | -h, --help show this help message and exit 350 | -s input from stdin 351 | -z show passwords 352 | ``` 353 | ``` 354 | usage: nsa diff [-h] [-s] [-z] 355 | 356 | optional arguments: 357 | -h, --help show this help message and exit 358 | -s input from stdin 359 | -z show passwords 360 | ``` 361 | ``` 362 | usage: nsa commit [-h] [-s] [-d DB] 363 | 364 | optional arguments: 365 | -h, --help show this help message and exit 366 | -s input from stdin 367 | -d DB set passwords database path 368 | ``` 369 | ``` 370 | usage: nsa revert [-h] [-s] 371 | 372 | optional arguments: 373 | -h, --help show this help message and exit 374 | -s input from stdin 375 | ``` 376 | 377 | # Contact 378 | 379 | - Web: https://codeberg.org/caveman/nsapass 380 | 381 | -------------------------------------------------------------------------------- /nsa: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # nsapass - the simplest, most usable and most secure passwords manager. 4 | # Copyright (C) 2021 caveman 5 | # https://github.com/al-caveman/nsapass 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 3 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 . 19 | 20 | import argparse 21 | import os 22 | import sys 23 | import string 24 | import re 25 | import json 26 | import subprocess 27 | import time 28 | import datetime 29 | import math 30 | import secrets 31 | import hashlib 32 | import atexit 33 | import getpass 34 | import base64 35 | import signal 36 | import fcntl 37 | 38 | ####################################### 39 | # __ # 40 | # configs below <(o )___ # 41 | # ( ._> / # 42 | ####################################### 43 | 44 | # location of ipc's named pipes 45 | IPC_DIR = '/tmp/nsapass' 46 | 47 | # encoding of strings 48 | ENCODING = 'utf-8' 49 | 50 | # location of encrypted passwords db 51 | DATABASE_PATH = os.path.expanduser('~/.local/share/nsapass/db') 52 | 53 | # password character sets 54 | PASSWORD_SETS = { 55 | 'alphanumerics' : string.ascii_letters + string.digits, 56 | 'hexdigits' : string.hexdigits, 57 | } 58 | 59 | # default passwords set 60 | DEFAULT_SET = 'alphanumerics' 61 | 62 | # server timeout in seconds. 0 for no timeout. 63 | TIMEOUT = 30 64 | 65 | # key derivation function command. set it to None to disable it 66 | KDF_COMMAND = { 67 | 'COMMAND' : ['argon2', 'nsapasssalt', '-d', '-t', '20', 68 | '-k', '1048576', '-p', '4', '-r'], 69 | 'STDIN' : '{DATABASE_PASSWORD}', 70 | 'TAKE_STDERR' : False, 71 | 'TAKE_STDOUT' : True, 72 | 'RETURN_VALUES' : {0:None, '*':'argon2 error'}} 73 | 74 | # encrypt command 75 | ENCRYPT_COMMAND = { 76 | 'COMMAND' : ['openssl', 'enc', '-chacha20', 77 | '-out={DATABASE_PATH}', '-e', 78 | '-k={DATABASE_PASSWORD}', '-iter=1'], 79 | 'STDIN' : '{DATABASE_DATA}', 80 | 'TAKE_STDERR' : False, 81 | 'TAKE_STDOUT' : True, 82 | 'RETURN_VALUES' : {0:None, '*':'openssl error'}} 83 | 84 | # decrypt command 85 | DECRYPT_COMMAND = dict(ENCRYPT_COMMAND) 86 | DECRYPT_COMMAND['COMMAND'] = ['openssl', 'enc', '-chacha20', 87 | '-in={DATABASE_PATH}', '-d', 88 | '-k={DATABASE_PASSWORD}', '-iter=1'] 89 | DECRYPT_COMMAND['STDIN'] = '' 90 | 91 | # commands used by `nsa do -c COMMANDS`. each command must be a single 92 | # letter. below is an example of defining commands to copy and paste 93 | # various fields from the database. 94 | template_command = { 95 | 'COMMAND' : ['xdotool', '-'], 96 | 'STDIN' : '', 97 | 'TAKE_STDERR' : False, 98 | 'TAKE_STDOUT' : False, 99 | 'RETURN_VALUES' : {0:None, '*':'external command error'}} 100 | DO_COMMANDS = {c:dict(template_command) for c in [ 101 | 'uri', 'user', 'pass', 'cli', 'web', 'cat']} 102 | DO_COMMANDS['uri' ]['STDIN'] = "type '{URI}'" 103 | DO_COMMANDS['user']['STDIN'] = "type '{USERNAME}'" 104 | DO_COMMANDS['pass']['STDIN'] = "type '{PASSWORD}'" 105 | DO_COMMANDS['cli' ]['STDIN'] = \ 106 | 'type "{USERNAME}"\nkey Return\ntype "{PASSWORD}"\nkey Return' 107 | DO_COMMANDS['web' ]['STDIN'] = \ 108 | 'type "{USERNAME}"\nkey Tab\ntype "{PASSWORD}"\nkey Return' 109 | DO_COMMANDS['cat' ]['COMMAND'] = ['cat'] 110 | DO_COMMANDS['cat' ]['STDIN' ] = '{DATA}' 111 | 112 | # theme 113 | TIME = '%Y-%m-%d %H:%M' 114 | COLOURS = {'RED' : '\33[0;31m', 'BRED' : '\33[1;31m', 115 | 'BGRED' : '\33[0;41m', 'YELLOW' : '\33[0;33m', 116 | 'BGREEN' : '\33[1;32m', 'BLUE' : '\33[0;34m', 117 | 'GRAY' : '\33[0;90m', 'MAGENTA': '\33[0;35m', 118 | 'RESET' : '\33[0m'} 119 | FORMAT = { 120 | 'DEBUG' : '{MAGENTA}[debug]{RESET} {}\n', 121 | 'INFO' : '{BGREEN}*{RESET} {}\n', 122 | 'WARN' : '{YELLOW}*{RESET} {}\n', 123 | 'ERROR' : '{RED}[error]{RESET} {}\n', 124 | 'PROMPT' : '{BLUE}< {}{RESET} ', 125 | 'PROMPTSTDIN' : '{BLUE}< {} (stdin){RESET}\n', 126 | 'TAGS' : '{BGREEN}{}{RESET}', 127 | 'USERNAME' : '{YELLOW}[{}{YELLOW}]{RESET}', 128 | 'PASSWORD' : '{RED}[{BGRED}{}{RED}]{RESET}', 129 | 'DIGEST' : '{RED}[{:.10}{RED}]{RESET}', 130 | 'TIME' : '{BLUE}{}{RESET}', 131 | 'URI' : '{MAGENTA}[{}{MAGENTA}]{RESET}', 132 | 'NOTE' : '{GRAY}[{}]{RESET}', 133 | 'HIDDEN' : '{GRAY}******{RESET}', 134 | 'NONE' : '', 135 | 'ADD' : '{BGREEN}+ {RESET}', 136 | 'DEL' : '{BRED}- {RESET}', 137 | 'LIST' : '{DIFF}{TIME} {TAGS} {USERNAME} {PASSWORD} ' 138 | '{DIGEST} {URI} {NOTE}\n'} 139 | 140 | ######################################### 141 | # # 142 | # WARNING: DEVS ONLY # 143 | # # 144 | ######################################### 145 | 146 | VERSION = '10' 147 | YEAR = '2022' 148 | 149 | CMD_CREATE = 'create' 150 | CMD_CHPASS = 'chpass' 151 | CMD_START = 'start' 152 | CMD_STOP = 'stop' 153 | CMD_DO = 'do' 154 | CMD_ADD = 'add' 155 | CMD_DEL = 'del' 156 | CMD_MOD = 'mod' 157 | CMD_LS = 'ls' 158 | CMD_DIFF = 'diff' 159 | CMD_COMMIT = 'commit' 160 | CMD_DROP = 'drop' 161 | 162 | ARG_INFO = 'v' 163 | ARG_STDIN_INPUT = 's' 164 | ARG_DB_PATH = 'd' 165 | ARG_QUERY = 'query' 166 | ARG_SHOWPASS = 'z' 167 | ARG_TAGS = 't' 168 | ARG_USERNAME = 'u' 169 | ARG_PASS_SET = 'p' 170 | ARG_PASS_OPT = 'o' 171 | ARG_PASS_BIT = 'b' 172 | ARG_PASS_LEN = 'l' 173 | ARG_PASS_MAN = 'm' 174 | ARG_PASS_KEY = 'f' 175 | ARG_URI = 'r' 176 | ARG_NOTE = 'n' 177 | ARG_COMMANDS = 'c' 178 | ARG_DEBUG = 'V' 179 | ARG_NOCOLOUR = 'C' 180 | ARG_IPC_DIR = 'i' 181 | 182 | KEY_DB_USERNAME = 'username' 183 | KEY_DB_PASS = 'password' 184 | KEY_DB_DATAB64 = 'datab64s' 185 | KEY_DB_DIGEST = 'digest' 186 | KEY_DB_URI = 'uri' 187 | KEY_DB_NOTE = 'note' 188 | KEY_DB_TIME = 'timestamp' 189 | 190 | KEY_IPC_CMD = 'command' 191 | KEY_IPC_ARGS = 'arguments' 192 | KEY_IPC_PATH = 'path' 193 | KEY_IPC_REPLY_TYPE = 'type' 194 | KEY_IPC_REPLY_DATA = 'data' 195 | 196 | VAL_IPC_REPLY_TYPE_OUT = 'out' 197 | VAL_IPC_REPLY_TYPE_DEBUG = 'debug' 198 | VAL_IPC_REPLY_TYPE_INFO = 'info' 199 | VAL_IPC_REPLY_TYPE_WARN = 'warn' 200 | VAL_IPC_REPLY_TYPE_ERR = 'err' 201 | VAL_IPC_REPLY_TYPE_CMD = 'command' 202 | VAL_IPC_REPLY_TYPE_ENTRY = 'entry' 203 | 204 | FRMT2KEY = {'USERNAME': KEY_DB_USERNAME, 'PASSWORD': KEY_DB_PASS, 205 | 'DATA' : KEY_DB_DATAB64, 'DIGEST' : KEY_DB_DIGEST, 206 | 'URI' : KEY_DB_URI, 'NOTE' : KEY_DB_NOTE, 207 | 'TIME' : KEY_DB_TIME} 208 | KEY2FRMT = {value:key for key, value in FRMT2KEY.items()} 209 | 210 | enable_debug = True 211 | 212 | def frmtstr(s, frmt, show=True): 213 | """formats a value""" 214 | if not s: s = FORMAT['NONE'].format(s, **COLOURS) 215 | elif not show: s = FORMAT['HIDDEN'].format(s, **COLOURS) 216 | return FORMAT[frmt].format(s, **COLOURS) 217 | 218 | def frmtentry(tags, entry, showpass, diff=None): 219 | """formats a database entry""" 220 | local_time = datetime.datetime.fromtimestamp( 221 | entry[KEY_DB_TIME], datetime.timezone.utc).astimezone() 222 | values = {'DIFF': frmtstr(diff, diff) if diff else '', 223 | 'TAGS': frmtstr(tags, 'TAGS')} 224 | for frmt, key in FRMT2KEY.items(): 225 | if key == KEY_DB_DATAB64: continue 226 | show = showpass if key == KEY_DB_PASS else True 227 | s = local_time.strftime(TIME) if key == KEY_DB_TIME else entry[key] 228 | values[frmt] = frmtstr(s, frmt, show) 229 | return FORMAT['LIST'].format(**values) 230 | 231 | def log(msg, frmt): 232 | """log various message types to terminal""" 233 | sys.stderr.write(frmt.format(msg, **COLOURS)) 234 | sys.stderr.flush() 235 | def debug(msg): 236 | if enable_debug: log(msg, FORMAT['DEBUG']) 237 | def info(msg): log(msg, FORMAT['INFO']) 238 | def warn(msg): log(msg, FORMAT['WARN']) 239 | def err(msg): log(msg, FORMAT['ERROR']) 240 | 241 | def cleanwords(words): 242 | """guarantee that words are single-space-separated words or None""" 243 | if not words: return None 244 | if type(words) is list: words = ' '.join(words) 245 | return ' '.join(words.split()) 246 | 247 | def find(db, query): 248 | """finds tags""" 249 | tags_list = list(db) 250 | tags_list.sort() 251 | query = cleanwords(query) 252 | if not query: return tags_list 253 | if query in tags_list: return [query] 254 | words_escaped = [re.escape(word) for word in query.split()] 255 | query_re = r'.*? .*?'.join(words_escaped) 256 | return [tags for tags in tags_list if re.search(query_re, tags)] 257 | 258 | def askpass(name, confirm=False, stdin=False): 259 | """prompts users for password""" 260 | f = input if stdin else getpass.getpass 261 | prompt = 'PROMPTSTDIN' if stdin else 'PROMPT' 262 | pass1 = f(frmtstr(f'{name} password:', prompt)) 263 | pass2 = pass1 264 | if confirm: 265 | pass1 = f(frmtstr(f'confirm password:', prompt)) 266 | if pass1 == pass2: 267 | return pass1 268 | err('passwords mismatched') 269 | sys.exit(1) 270 | 271 | def genpass(bits, length, letters): 272 | """dynamically generates passwords""" 273 | if length and length < 1: 274 | err(f'password length "{length}" is not positive') 275 | sys.exit(1) 276 | if bits: 277 | if bits < 1: 278 | err(f'password bits count "{bits}" is not positive') 279 | sys.exit(1) 280 | bits_to_length = math.ceil(bits / math.log2(len(letters))) 281 | if not length or bits_to_length > length: length = bits_to_length 282 | password = ''.join(secrets.choice(letters) for i in range(0, length)) 283 | return password 284 | 285 | def bin2str(data): 286 | """encode binary data into a string""" 287 | return base64.b64encode(data).decode() 288 | 289 | def str2bin(s): 290 | """decode binary data from a string""" 291 | return base64.b64decode(s) if s else None 292 | 293 | def readbin(path): 294 | """read binary data""" 295 | debug(f'reading binary data from "{path}"...') 296 | try: 297 | with open(path, 'rb') as f: 298 | debug(f'shared-locking "{path}"...') 299 | fcntl.lockf(f, fcntl.LOCK_SH) 300 | data = f.read() 301 | except FileNotFoundError: 302 | if path != '-': 303 | err(f'file "{path}" not found') 304 | sys.exit(1) 305 | data = sys.stdin.buffer.read() 306 | return data 307 | 308 | def run(command, values): 309 | """run an external command""" 310 | debug(f'running command "{command}"') 311 | debug(f'command values "{values}"') 312 | command_formatted = [a.format(**values) for a in command['COMMAND']] 313 | try: 314 | p = subprocess.Popen(command_formatted, 315 | stdin = subprocess.PIPE if command['STDIN'] else None, 316 | stdout = subprocess.PIPE if command['TAKE_STDOUT'] else None, 317 | stderr = subprocess.PIPE if command['TAKE_STDERR'] else None) 318 | except FileNotFoundError: 319 | err(f'command `{command_formatted[0]}` not found') 320 | sys.exit(1) 321 | except IndexError: 322 | err(f'command is not defined') 323 | sys.exit(1) 324 | except KeyError as e: 325 | err(f'command `{command_formatted[0]}` lacks key {e}') 326 | sys.exit(1) 327 | stdin = b'' 328 | for before, field, _, _ in string.Formatter().parse(command['STDIN']): 329 | stdin += before.encode(ENCODING) 330 | if field is None: continue 331 | if field not in values: 332 | err(f'unknown command format field "{field}"') 333 | sys.exit(1) 334 | value = values[field] 335 | if field == 'DATA': stdin += str2bin(value) 336 | elif type(values[field]) is str: stdin += value.encode(ENCODING) 337 | elif type(values[field]) is bytes: stdin += value 338 | else: 339 | err(f'unknown command field data type "{type(part)}"') 340 | sys.exit(1) 341 | p_stdout, _ = p.communicate(input=stdin) 342 | debug(f'return code "{p.returncode}"') 343 | r = p.returncode if p.returncode in command['RETURN_VALUES'] else '*' 344 | return p_stdout, command['RETURN_VALUES'][r] 345 | 346 | def kdf(password): 347 | """key derivation function""" 348 | if KDF_COMMAND: 349 | values = {'DATABASE_PASSWORD':password} 350 | password, kdf_err = run(KDF_COMMAND, values) 351 | if kdf_err: 352 | err(kdf_err) 353 | sys.exit(1) 354 | password = password.decode(ENCODING).rstrip('\n') 355 | debug(f'derived key "{password}"') 356 | return password 357 | 358 | def load_json(path): 359 | """loads a json file""" 360 | debug(f'loading json file "{path}"...') 361 | if not os.path.exists(path): 362 | err(f'could not find file "{path}"') 363 | sys.exit(1) 364 | with open(path, 'r') as f: 365 | try: 366 | return json.load(f) 367 | except json.JSONDecodeError: 368 | err(f'failed to decode json file "{path}"') 369 | sys.exit(1) 370 | 371 | def load_db(path, stdin, password=None): 372 | """load database""" 373 | debug(f'loading "{path}"...') 374 | if not os.path.exists(path): 375 | err(f'database "{path}" not found. did you create it?') 376 | sys.exit(1) 377 | if not password: 378 | password = kdf(askpass('database', stdin=stdin)) 379 | values = {'DATABASE_PATH' : path, 'DATABASE_PASSWORD' : password} 380 | data, err_msg = run(DECRYPT_COMMAND, values) 381 | if err_msg: 382 | err(err_msg) 383 | sys.exit(1) 384 | try: 385 | db = json.loads(data) 386 | return db, password 387 | except (json.JSONDecodeError, UnicodeDecodeError): 388 | err(f'error decoding database "{path}". bad password?') 389 | sys.exit(1) 390 | 391 | def save_db(db, path, password): 392 | """save database""" 393 | debug(f'saving to "{path}"...') 394 | data = json.dumps(db, indent=4) 395 | values = {'DATABASE_PATH' : path, 396 | 'DATABASE_PASSWORD' : password, 397 | 'DATABASE_DATA' : data} 398 | _, err_msg = run(ENCRYPT_COMMAND, values) 399 | if err_msg: 400 | err(err_msg) 401 | sys.exit(1) 402 | 403 | def ipc_init(path): 404 | """prepares stuff needed for ipc""" 405 | dirname = os.path.dirname(path) 406 | if len(dirname): os.makedirs(dirname, exist_ok=True) 407 | try: 408 | debug(f'creating ipc file "{path}"...') 409 | os.mkfifo(path, mode=400) 410 | except NotImplementedError: 411 | err("your operating system doesn't support named pipes") 412 | sys.exit(1) 413 | except FileExistsError: 414 | err(f'file "{path}" exists. delete it if not used') 415 | sys.exit(1) 416 | atexit.register(os.unlink, path) 417 | atexit.register(debug, f'deleting named pipe "{path}"...') 418 | 419 | def ipc_uninit(path): 420 | """uninitialises ipc""" 421 | debug(f'deleting ipc file "{path}"...') 422 | atexit.unregister(os.unlink) 423 | os.unlink(path) 424 | 425 | def ipc_request(path, command, rargs, stdin=False): 426 | """define ipc request messages""" 427 | return {KEY_IPC_CMD:command, KEY_IPC_ARGS:rargs, KEY_IPC_PATH:path} 428 | 429 | def ipc_reply(reply_type, data): 430 | """define ipc reply messages""" 431 | return [{KEY_IPC_REPLY_TYPE : reply_type, KEY_IPC_REPLY_DATA : data}] 432 | def ipc_out(message) : return ipc_reply(VAL_IPC_REPLY_TYPE_OUT, message) 433 | def ipc_debug(message): return ipc_reply(VAL_IPC_REPLY_TYPE_DEBUG, message) 434 | def ipc_info(message) : return ipc_reply(VAL_IPC_REPLY_TYPE_INFO, message) 435 | def ipc_warn(message) : return ipc_reply(VAL_IPC_REPLY_TYPE_WARN, message) 436 | def ipc_err(message) : return ipc_reply(VAL_IPC_REPLY_TYPE_ERR, message) 437 | def ipc_command(values): return ipc_reply(VAL_IPC_REPLY_TYPE_CMD, values) 438 | def ipc_entry(db, tags, showpass, diff=None): 439 | entry = dict(db[tags]) 440 | if entry[KEY_DB_PASS] and not showpass: entry[KEY_DB_PASS] = '****' 441 | del entry[KEY_DB_DATAB64] 442 | return ipc_reply(VAL_IPC_REPLY_TYPE_ENTRY, (tags, entry, diff)) 443 | 444 | def ipc_do(db, last_query, rargs): 445 | """implements `do` sub-command""" 446 | args, db_keys_list = rargs 447 | query = args[ARG_QUERY] if args[ARG_QUERY] else last_query 448 | tags_list = find(db, query) 449 | if not tags_list: return ipc_err('nothing matched') 450 | if len(tags_list) > 1: return ipc_err('too many matched') 451 | tags = tags_list[0] 452 | info(f'do {FORMAT["TAGS"].format(tags, **COLOURS)}') 453 | replies = [] 454 | for keys in db_keys_list: 455 | replies += ipc_command({k:db[tags][k] for k in keys}) 456 | return replies 457 | 458 | def ipc_add(db, rargs): 459 | """implements `add` sub-command""" 460 | args, password, datab64s, digest = rargs 461 | tags, username, uri, note, showpass = (args[k] for k in 462 | (ARG_TAGS, ARG_USERNAME, ARG_URI, ARG_NOTE, ARG_SHOWPASS)) 463 | tags = cleanwords(tags) 464 | if tags in db: return ipc_err(f'tags "{tags}" already exists') 465 | info(f'add {FORMAT["TAGS"].format(tags, **COLOURS)}') 466 | db[tags] = {KEY_DB_USERNAME : username, 467 | KEY_DB_PASS : password, 468 | KEY_DB_DATAB64 : datab64s, 469 | KEY_DB_DIGEST : digest, 470 | KEY_DB_URI : uri, 471 | KEY_DB_NOTE : note, 472 | KEY_DB_TIME : time.time()} 473 | return ipc_entry(db, tags, showpass, 'ADD') 474 | 475 | def ipc_del(db, rargs): 476 | """implements `del` sub-command""" 477 | query, showpass = rargs[ARG_QUERY], rargs[ARG_SHOWPASS] 478 | tags_list = find(db, query) 479 | if not tags_list: 480 | return ipc_err('nothing matched') 481 | replies = [] 482 | for tags in tags_list: 483 | info(f'del {FORMAT["TAGS"].format(tags, **COLOURS)}') 484 | replies += ipc_entry(db, tags, showpass, 'DEL') 485 | del db[tags] 486 | return replies 487 | 488 | def ipc_mod(db, rargs): 489 | """implements `mod` sub-command""" 490 | args, password, datab64s, digest = rargs 491 | query, tags, username, uri, note, showpass = (args[k] for k in 492 | (ARG_QUERY, ARG_TAGS, ARG_USERNAME, ARG_URI, ARG_NOTE, 493 | ARG_SHOWPASS)) 494 | tags = cleanwords(tags) 495 | tags_list = find(db, query) 496 | if not tags_list: 497 | return ipc_err('nothing matched') 498 | replies = [] 499 | for cur_tags in tags_list: 500 | info(f'mod {FORMAT["TAGS"].format(cur_tags, **COLOURS)}') 501 | entry = db[cur_tags] 502 | replies += ipc_entry(db, cur_tags, showpass, 'DEL') 503 | entry[KEY_DB_TIME] = time.time() 504 | if username: entry[KEY_DB_USERNAME] = username 505 | if password: entry[KEY_DB_PASS] = password 506 | if datab64s: 507 | entry[KEY_DB_DATAB64] = datab64s 508 | entry[KEY_DB_DIGEST] = digest 509 | if uri: entry[KEY_DB_URI] = uri 510 | if note: entry[KEY_DB_NOTE] = note 511 | if tags and tags != cur_tags: 512 | db[tags] = db[cur_tags] 513 | del db[cur_tags] 514 | cur_tags = tags 515 | replies += ipc_entry(db, cur_tags, showpass, 'ADD') 516 | return replies 517 | 518 | def ipc_ls(db, rargs): 519 | """implements `ls` sub-command""" 520 | query, showpass = rargs[ARG_QUERY], rargs[ARG_SHOWPASS] 521 | formatted_query = FORMAT['TAGS'].format(' '.join(query), **COLOURS) 522 | info(f'ls {formatted_query}') 523 | tags_list = find(db, query) 524 | if not tags_list: return ipc_err('nothing matched') 525 | replies = [] 526 | for tags in tags_list: 527 | replies += ipc_entry(db, tags, showpass) 528 | return replies 529 | 530 | def ipc_diff(db_tmp, path_orig, password, password_orig, rargs): 531 | """implements `diff` sub-command""" 532 | info('diff') 533 | showpass = rargs[ARG_SHOWPASS] 534 | db_orig, _ = load_db(path_orig, False, password_orig) 535 | replies = [] 536 | for tags in db.keys() - db_orig.keys(): 537 | replies += ipc_entry(db, tags, showpass, 'ADD') 538 | for tags in db_orig.keys() - db.keys(): 539 | replies += ipc_entry(db_orig, tags, showpass, 'DEL') 540 | for tags in db.keys() & db_orig.keys(): 541 | if [k for k in db[tags] if db[tags][k] != db_orig[tags][k]]: 542 | replies += ipc_entry(db_orig, tags, showpass, 'DEL') 543 | replies += ipc_entry(db, tags, showpass, 'ADD') 544 | if password != password_orig: 545 | passfrmt = frmtstr(password, 'PASSWORD', showpass) 546 | replies += ipc_warn(f'updated database password to {passfrmt}') 547 | return replies 548 | 549 | def handler_sigint(signum, frame): 550 | """pretty interrupt handling""" 551 | info('received interrupt. exiting...') 552 | sys.exit(0) 553 | 554 | def handler_sigalrm(signum, frame): 555 | """suspend server upon timeout""" 556 | info('server timed out. suspending...') 557 | ipc_uninit(ipc_server_path) 558 | getpass.getpass(frmtstr('hit enter to resume the server', 'PROMPT')) 559 | ipc_init(ipc_server_path) 560 | info('server resumed') 561 | signal.alarm(TIMEOUT) 562 | 563 | # parse arguments 564 | cmdname = os.path.basename(sys.argv[0]) 565 | allcmds = [ 566 | [CMD_CREATE, [ARG_DB_PATH, ARG_STDIN_INPUT], 567 | {'help':"create a databases"}], 568 | [CMD_CHPASS, [ARG_STDIN_INPUT], 569 | {'help':"change databases's password"}], 570 | [CMD_START, [ARG_STDIN_INPUT, ARG_DB_PATH], 571 | {'help':'starts nsapass'}], 572 | [CMD_STOP, [ARG_STDIN_INPUT], 573 | {'help':'stops nsapass and discards any uncommitted changes'}], 574 | [CMD_DO , [ARG_QUERY, ARG_STDIN_INPUT, ARG_COMMANDS], 575 | {'help':'do things (e.g. type passwords)'}], 576 | [CMD_ADD, [ARG_TAGS, ARG_USERNAME, ARG_PASS_SET, ARG_PASS_OPT, 577 | ARG_PASS_BIT, ARG_PASS_LEN, ARG_PASS_MAN, ARG_PASS_KEY, ARG_URI, 578 | ARG_NOTE, ARG_STDIN_INPUT, ARG_SHOWPASS], {'help':'add an entry'}], 579 | [CMD_DEL, [ARG_QUERY, ARG_STDIN_INPUT, ARG_SHOWPASS], 580 | {'help':'delete an entry'}], 581 | [CMD_MOD, [ARG_QUERY, ARG_TAGS, ARG_USERNAME, ARG_PASS_SET, 582 | ARG_PASS_OPT, ARG_PASS_BIT, ARG_PASS_LEN, ARG_PASS_MAN, 583 | ARG_PASS_KEY, ARG_URI, ARG_NOTE, ARG_STDIN_INPUT, ARG_SHOWPASS], 584 | {'help':'modify an entry'}], 585 | [CMD_LS, [ARG_QUERY, ARG_STDIN_INPUT, ARG_SHOWPASS], 586 | {'help':'view entries'}], 587 | [CMD_DIFF, [ARG_STDIN_INPUT, ARG_SHOWPASS], 588 | {'help':'show modifications done so far'}], 589 | [CMD_COMMIT, [ARG_STDIN_INPUT], 590 | {'help':'commit changes to the database'}], 591 | [CMD_DROP, [ARG_STDIN_INPUT], 592 | {'help':'discard all uncommitted changed'}], 593 | ] 594 | allargs = { 595 | ARG_QUERY : {'metavar':'QUERY', 'type':str, 'nargs':'*', 596 | 'help':'query tags'}, 597 | ARG_TAGS : {'metavar':'TAG', 'type':str, 'nargs':'+', 598 | 'help':"new tags"}, 599 | ARG_USERNAME: {'metavar':'USERNAME', 'type':str, 600 | 'help':'new username'}, 601 | ARG_PASS_SET: {'metavar':'SET', 'type':str, 'default':DEFAULT_SET, 602 | 'help':'pre-defined password letters set name'}, 603 | ARG_PASS_OPT: {'metavar':'LETTERS', 'type':str, 604 | 'help':'raw password letter options'}, 605 | ARG_PASS_BIT: {'metavar':'BIT', 'type':int, 606 | 'help':'generate BIT-entropy password from SET'}, 607 | ARG_PASS_LEN: {'metavar':'LEN', 'type':int, 608 | 'help':'generate LEN-long password from SET'}, 609 | ARG_PASS_MAN: {'action':'store_true', 'help':'user-defined password'}, 610 | ARG_PASS_KEY: {'metavar':'PATH', 'type':str, 611 | 'help':'data file in PATH, or STDIN if "-"'}, 612 | ARG_URI : {'metavar':'URI', 'type':str, 613 | 'help':"a uniform resource identifier"}, 614 | ARG_NOTE : {'metavar':'NOTE', 'type':str, 'help':"a note"}, 615 | ARG_COMMANDS: {'metavar':'COMMANDS', 'type':str, 'nargs':'+', 616 | 'default':[], 'help': f"""perform actions specified in 617 | COMMANDS in order from left to right. COMMANDS are 618 | defined in option DO_COMMANDS, which currently are: 619 | {', '.join(list(DO_COMMANDS))}"""}, 620 | ARG_SHOWPASS : {'action':'store_true', 'help':'show passwords'}, 621 | ARG_STDIN_INPUT : {'action':'store_true', 'help':'input from stdin'}, 622 | ARG_DB_PATH : {'metavar':'DB', 'type':str, 623 | 'help':'set database path'}} 624 | parser = argparse.ArgumentParser() 625 | parser.add_argument('-' + ARG_INFO, action='store_true', 626 | help='show information about nsapass') 627 | parser.add_argument('-' + ARG_DEBUG, action='store_true', 628 | help='show debugging information') 629 | parser.add_argument('-' + ARG_NOCOLOUR, action='store_true', 630 | help='disable colourful output') 631 | parser.add_argument('-' + ARG_IPC_DIR, metavar='DIR', type=str, 632 | help='ipc directory') 633 | subparsers = parser.add_subparsers(title='commands', dest='command') 634 | for cmd_name, cmd_args, cmd_named in allcmds : 635 | subp = subparsers.add_parser(cmd_name, **cmd_named) 636 | for arg in cmd_args: 637 | arg_prefix = '-' if len(arg) == 1 else '' 638 | subp.add_argument(arg_prefix + arg, **allargs[arg]) 639 | args = parser.parse_args() 640 | 641 | # some usability stuff 642 | if args.v: 643 | sys.stdout.write( 644 | f"nsapass v{VERSION} copyright (C) {YEAR} caveman\n" 645 | "https://github.com/Al-Caveman/nsapass\n\n" 646 | "this program comes with ABSOLUTELY NO WARRANTY; for details\n" 647 | "read https://github.com/Al-Caveman/nsapass/blob/master/LICENSE\n") 648 | sys.exit() 649 | if args.command is None: 650 | parser.print_help() 651 | sys.exit() 652 | enable_debug = args.V 653 | if args.C: 654 | for c in COLOURS: COLOURS[c] = '' 655 | if args.i: IPC_DIR = args.i 656 | ipc_server_path = f'{IPC_DIR}/server' 657 | 658 | # register signal handlers 659 | signal.signal(signal.SIGINT, handler_sigint) 660 | signal.signal(signal.SIGTERM, handler_sigint) 661 | signal.signal(signal.SIGALRM, handler_sigalrm) 662 | 663 | # starts the server loop 664 | if args.command == CMD_START: 665 | ipc_init(ipc_server_path) 666 | path = args.d if args.d else DATABASE_PATH 667 | path_tmp = path + '.temp' 668 | if os.path.exists(path_tmp): 669 | warn(f'found uncommitted changes from a previous session in "{path_tmp}"') 670 | warn(f're-loading the uncommitted changes...') 671 | db, password = load_db(path_tmp, args.s) 672 | warn(f'execute:') 673 | warn(f' `{cmdname} {CMD_DIFF}` to see the uncommitted changes') 674 | warn(f' `{cmdname} {CMD_COMMIT}` to commit them') 675 | warn(f' `{cmdname} {CMD_DROP}` to discard them') 676 | else: 677 | db, password = load_db(path, args.s) 678 | password_orig = password 679 | last_do_query = None 680 | info('waiting for ipc commands...') 681 | while True: # runs indefinitely until stopped by SIGINT or `CMD_STOP` 682 | signal.alarm(TIMEOUT) 683 | request = load_json(ipc_server_path) 684 | rcmd = request[KEY_IPC_CMD] 685 | rargs = request[KEY_IPC_ARGS] 686 | client_ipc_path = request[KEY_IPC_PATH] 687 | debug(f'got ipc request {rcmd} {rargs}') 688 | 689 | # implement nsapass's sub-commands 690 | replies = [] 691 | if rcmd == CMD_CHPASS: 692 | info('updating database password...') 693 | password = kdf(rargs) 694 | save_db(db, path_tmp, password) 695 | replies += ipc_info( 696 | 'database password change staged') 697 | elif rcmd == CMD_DO : 698 | if rargs[0][ARG_QUERY]: last_do_query = rargs[0][ARG_QUERY] 699 | replies += ipc_do(db, last_do_query, rargs) 700 | elif rcmd == CMD_ADD: 701 | replies += ipc_add(db, rargs) 702 | save_db(db, path_tmp, password) 703 | elif rcmd == CMD_DEL: 704 | replies += ipc_del(db, rargs) 705 | save_db(db, path_tmp, password) 706 | elif rcmd == CMD_MOD: 707 | replies += ipc_mod(db, rargs) 708 | save_db(db, path_tmp, password) 709 | elif rcmd == CMD_LS : 710 | replies += ipc_ls(db, rargs) 711 | elif rcmd == CMD_DIFF: 712 | replies += ipc_diff(db, path, password, password_orig, rargs) 713 | elif rcmd == CMD_COMMIT: 714 | if not os.path.exists(path_tmp): 715 | replies += ipc_warn('no changes to commit') 716 | else: 717 | debug(f'moving "{path_tmp}" to "{path}"...') 718 | os.rename(path_tmp, path) 719 | replies += ipc_info(f'changes committed to "{path}"') 720 | elif rcmd == CMD_DROP: 721 | if not os.path.exists(path_tmp): 722 | replies += ipc_warn('no changes to drop') 723 | else: 724 | db, _ = load_db(path, False, password_orig) 725 | debug(f'deleting "{path_tmp}"...') 726 | os.unlink(path_tmp) 727 | replies += ipc_info(f'changes dropped') 728 | 729 | # write replies back to client 730 | debug(f'writing to "{client_ipc_path}" ipc reply {replies}...') 731 | try: 732 | with open(client_ipc_path, 'w') as f: 733 | debug(f'exclusive-locking "{client_ipc_path}"...') 734 | fcntl.lockf(f, fcntl.LOCK_EX) 735 | json.dump(replies, f) 736 | except BrokenPipeError: 737 | err("client's pipe is broken") 738 | 739 | # stop nsapass 740 | if rcmd == CMD_STOP: 741 | info(f'stop') 742 | sys.exit() 743 | 744 | # create nsapass passwords database 745 | if args.command == CMD_CREATE: 746 | path = args.d if args.d else DATABASE_PATH 747 | if os.path.exists(path): 748 | err(f'database "{path}" already exists') 749 | sys.exit(1) 750 | password = kdf(askpass('new database', confirm=True, stdin=args.s)) 751 | save_db({}, path, password) 752 | info(f'database created in "{path}"') 753 | sys.exit(0) 754 | 755 | # define values needed for various commands 756 | if args.command == CMD_CHPASS: 757 | new_db_pass = askpass('new database', confirm=True, stdin=args.s) 758 | elif args.command in {CMD_ADD, CMD_MOD}: 759 | password, datab64s, digest = None, None, None 760 | if args.p not in PASSWORD_SETS: 761 | err(f'set "{args.p}" is not among: {", ".join(PASSWORD_SETS)}') 762 | sys.exit(1) 763 | letters = args.o if args.o else PASSWORD_SETS[args.p] 764 | if args.b or args.l: password = genpass(args.b, args.l, letters) 765 | if args.m: password = askpass('new entry', confirm=True, stdin=args.s) 766 | if args.f: 767 | data = readbin(args.f) 768 | datab64s = bin2str(data) 769 | digest = hashlib.sha224(data).hexdigest() 770 | elif args.command == CMD_DO: 771 | db_keys_list = [] 772 | for command in args.c: 773 | frmt = ''.join(DO_COMMANDS[command]['COMMAND']) 774 | frmt += DO_COMMANDS[command]['STDIN'] 775 | keys = [] 776 | for _, field, _, _ in string.Formatter().parse(frmt): 777 | if field is None: continue 778 | if field not in FRMT2KEY: 779 | err(f'unknown do format field "{field}"') 780 | sys.exit(1) 781 | keys.append(FRMT2KEY[field]) 782 | db_keys_list.append(keys) 783 | 784 | # terminate useless cases early on 785 | if args.command == CMD_ADD and not args.t: 786 | err('tags are required') 787 | sys.exit(1) 788 | if args.command == CMD_DO and args.c: 789 | unknown_commands = list(set(args.c) - DO_COMMANDS.keys()) 790 | if unknown_commands: 791 | err(f'unknown do commands: {", ".join(unknown_commands)}') 792 | sys.exit(1) 793 | 794 | # prepare things to talk to server 795 | ipc_client_path = f'{IPC_DIR}/client.{os.getpid()}' 796 | ipc_init(ipc_client_path) 797 | if args.command == CMD_CHPASS: rargs = new_db_pass 798 | elif args.command in {CMD_ADD, CMD_MOD}: 799 | rargs = [vars(args), password, datab64s, digest] 800 | elif args.command == CMD_DO: rargs = [vars(args), db_keys_list] 801 | else: rargs = vars(args) 802 | request = ipc_request(ipc_client_path, args.command, rargs) 803 | 804 | # send ipc request 805 | debug(f'sending ipc request: {request}') 806 | try: 807 | fd = os.open(ipc_server_path, os.O_WRONLY | os.O_NONBLOCK) 808 | fcntl.lockf(fd, fcntl.LOCK_EX) 809 | os.write(fd, json.dumps(request).encode(ENCODING)) 810 | os.close(fd) 811 | except OSError: 812 | err(f'"{ipc_server_path}" not found. nsapass stopped or suspended?') 813 | sys.exit(1) 814 | except BrokenPipeError: 815 | err("server's pipe is broken") 816 | sys.exit(1) 817 | 818 | # processing server's ipc replies 819 | debug(f'waiting for ipc replies...') 820 | replies = load_json(ipc_client_path) 821 | debug(f'got replies: {replies}') 822 | command_i = 0 823 | for reply in replies: 824 | reply_type = reply[KEY_IPC_REPLY_TYPE] 825 | reply_data = reply[KEY_IPC_REPLY_DATA] 826 | if reply_type == VAL_IPC_REPLY_TYPE_CMD: 827 | command = args.c[command_i] 828 | command_i += 1 829 | values = {KEY2FRMT[key]:value for key, value in reply_data.items()} 830 | stdout, err_msg = run(DO_COMMANDS[command], values) 831 | if stdout: sys.stdout.write(stdout) 832 | if err_msg: 833 | err(err_msg) 834 | sys.exit(1) 835 | continue 836 | fout = sys.stdout.write 837 | if reply_type == VAL_IPC_REPLY_TYPE_DEBUG : fout = debug 838 | elif reply_type == VAL_IPC_REPLY_TYPE_INFO : fout = info 839 | elif reply_type == VAL_IPC_REPLY_TYPE_WARN : fout = warn 840 | elif reply_type == VAL_IPC_REPLY_TYPE_ERR : fout = err 841 | elif reply_type == VAL_IPC_REPLY_TYPE_ENTRY: 842 | tags, entry, diff = reply_data 843 | reply_data = frmtentry(tags, entry, args.z, diff) 844 | fout(reply_data) 845 | -------------------------------------------------------------------------------- /pics/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-caveman/nsapass/982d99be4a66cc50e9f6856e9b4fec89f227a8b2/pics/banner.png -------------------------------------------------------------------------------- /pics/comparision.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-caveman/nsapass/982d99be4a66cc50e9f6856e9b4fec89f227a8b2/pics/comparision.png -------------------------------------------------------------------------------- /pics/comparision.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-caveman/nsapass/982d99be4a66cc50e9f6856e9b4fec89f227a8b2/pics/comparision.xcf -------------------------------------------------------------------------------- /pics/screenshot_init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-caveman/nsapass/982d99be4a66cc50e9f6856e9b4fec89f227a8b2/pics/screenshot_init.png -------------------------------------------------------------------------------- /pics/screenshot_init.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-caveman/nsapass/982d99be4a66cc50e9f6856e9b4fec89f227a8b2/pics/screenshot_init.xcf -------------------------------------------------------------------------------- /pics/screenshot_someuse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-caveman/nsapass/982d99be4a66cc50e9f6856e9b4fec89f227a8b2/pics/screenshot_someuse.png -------------------------------------------------------------------------------- /pics/screenshot_someuse.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-caveman/nsapass/982d99be4a66cc50e9f6856e9b4fec89f227a8b2/pics/screenshot_someuse.xcf -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | IPC=./ipc_dir 2 | TRU=./truth 3 | CUR=./current 4 | DB=test_db 5 | 6 | NSA =../nsa -i $(IPC) 7 | NSA_CREATE=$(NSA) create -s -d $(CUR)/$(DB) 8 | NSA_START =$(NSA) start -s -d $(CUR)/$(DB) & 9 | NSA_STOP =$(NSA) stop 10 | NSA_CHPASS=$(NSA) chpass -s 11 | NSA_ADD =$(NSA) add -s 12 | NSA_DEL =$(NSA) del 13 | NSA_MOD =$(NSA) mod -s 14 | NSA_DIFF =$(NSA) diff 15 | NSA_DROP =$(NSA) drop 16 | NSA_COMMIT=$(NSA) commit 17 | NSA_DO =$(NSA) do 18 | NSA_LS =$(NSA) ls 19 | NSA_REVERT=$(NSA) revert 20 | NSA_COMMIT=$(NSA) commit 21 | 22 | PASS =echo -n "lol" 23 | PASS2 =echo -ne "lol\nlol" 24 | PASS_NEW =echo -n "rofl" 25 | PASS_NEW2=echo -ne "rofl\nrofl" 26 | PASS_USR =echo -ne "lel\nlel" 27 | 28 | KDF=argon2 nsapasssalt -d -k 1048576 -p 4 -t 20 -r 29 | DEC=$(KDF) | openssl enc -chacha20 -in=$(CUR)/$(DB) -d -kfile=- -iter=1 30 | CLN=python ./striptimestamps.py 31 | DIFF=diff --color=always -u 32 | 33 | GR="\033[1;32m" 34 | NC="\033[0m" 35 | PREFIX="\n\n>>>>>>>> " 36 | 37 | .PHONY: test 38 | test: clean 39 | @echo -e $(GR)$(PREFIX)prepare tests..$(NC) 40 | mkdir -p $(CUR) $(IPC) 41 | @echo -e $(GR)$(PREFIX)test docs..$(NC) 42 | $(NSA) -h > $(CUR)/-h 43 | $(NSA) -v > $(CUR)/-v 44 | $(DIFF) $(TRU)/-h $(CUR)/-h 45 | $(DIFF) $(TRU)/-v $(CUR)/-v 46 | @echo -e $(GR)$(PREFIX)test create..$(NC) 47 | $(PASS2) | $(NSA_CREATE) 48 | $(PASS) | $(DEC) > $(CUR)/$(DB).create.str 49 | $(CLN) $(CUR)/$(DB).create.str > $(CUR)/$(DB).create.str.cln 50 | $(DIFF) $(TRU)/$(DB).create.str.cln $(CUR)/$(DB).create.str.cln 51 | @echo -e $(GR)$(PREFIX)test start..$(NC) 52 | $(PASS) | $(NSA_START) 53 | sleep 10 54 | @echo -e $(GR)$(PREFIX)test ignorable actions..$(NC) 55 | $(NSA_DIFF) 56 | $(NSA_DROP) 57 | $(NSA_COMMIT) 58 | @echo -e $(GR)$(PREFIX)test add without a commit..$(NC) 59 | $(NSA_ADD) -t tag1 tag2 -n "oy vey" -f $(TRU)/data 60 | $(NSA_ADD) -t test asd dea s 61 | $(NSA_ADD) -t test 62 | $(PASS_USR) | $(NSA_ADD) -t asdfa lel -m 63 | $(PASS) | $(DEC) > $(CUR)/$(DB).add_wo_commit.str 64 | $(CLN) $(CUR)/$(DB).add_wo_commit.str > $(CUR)/$(DB).add_wo_commit.str.cln 65 | $(DIFF) $(TRU)/$(DB).add_wo_commit.str.cln $(CUR)/$(DB).add_wo_commit.str.cln 66 | @echo -e $(GR)$(PREFIX)test restart midways..$(NC) 67 | $(NSA_STOP) 68 | $(PASS) | $(NSA_START) 69 | sleep 10 70 | @echo -e $(GR)$(PREFIX)test commit the pending add..$(NC) 71 | $(NSA_COMMIT) 72 | $(PASS) | $(DEC) > $(CUR)/$(DB).add.str 73 | $(CLN) $(CUR)/$(DB).add.str > $(CUR)/$(DB).add.str.cln 74 | $(DIFF) $(TRU)/$(DB).add.str.cln $(CUR)/$(DB).add.str.cln 75 | @echo -e $(GR)$(PREFIX)test del without a commit..$(NC) 76 | $(NSA_DEL) test 77 | $(PASS) | $(DEC) > $(CUR)/$(DB).del_wo_commit.str 78 | $(CLN) $(CUR)/$(DB).del_wo_commit.str > $(CUR)/$(DB).del_wo_commit.str.cln 79 | $(DIFF) $(TRU)/$(DB).del_wo_commit.str.cln $(CUR)/$(DB).del_wo_commit.str.cln 80 | @echo -e $(GR)$(PREFIX)test restart midways..$(NC) 81 | $(NSA_STOP) 82 | $(PASS) | $(NSA_START) 83 | sleep 10 84 | @echo -e $(GR)$(PREFIX)test commit the pending del..$(NC) 85 | $(NSA_COMMIT) 86 | $(PASS) | $(DEC) > $(CUR)/$(DB).del.str 87 | $(CLN) $(CUR)/$(DB).del.str > $(CUR)/$(DB).del.str.cln 88 | $(DIFF) $(TRU)/$(DB).del.str.cln $(CUR)/$(DB).del.str.cln 89 | @echo -e $(GR)$(PREFIX)test mod without a commit..$(NC) 90 | $(NSA_MOD) t t -u barak -r https://mossad.gov.il 91 | $(NSA_MOD) t d -f $(TRU)/data 92 | $(PASS_USR) | $(NSA_MOD) t t -m 93 | $(PASS) | $(DEC) > $(CUR)/$(DB).mod_wo_commit.str 94 | $(CLN) $(CUR)/$(DB).mod_wo_commit.str > $(CUR)/$(DB).mod_wo_commit.str.cln 95 | $(DIFF) $(TRU)/$(DB).mod_wo_commit.str.cln $(CUR)/$(DB).mod_wo_commit.str.cln 96 | @echo -e $(GR)$(PREFIX)test restart midways..$(NC) 97 | $(NSA_STOP) 98 | $(PASS) | $(NSA_START) 99 | sleep 10 100 | @echo -e $(GR)$(PREFIX)test commit the pending mod..$(NC) 101 | $(NSA_COMMIT) 102 | $(PASS) | $(DEC) > $(CUR)/$(DB).mod.str 103 | $(CLN) $(CUR)/$(DB).mod.str > $(CUR)/$(DB).mod.str.cln 104 | $(DIFF) $(TRU)/$(DB).mod.str.cln $(CUR)/$(DB).mod.str.cln 105 | @echo -e $(GR)$(PREFIX)test do..$(NC) 106 | $(NSA_DO) t t -c cat > $(CUR)/data-t_t 107 | $(NSA_DO) t d -c cat > $(CUR)/data-t_d 108 | $(DIFF) $(TRU)/data $(CUR)/data-t_t 109 | $(DIFF) $(TRU)/data $(CUR)/data-t_d 110 | @echo -e $(GR)$(PREFIX)test ls..$(NC) 111 | $(NSA_LS) 112 | @echo -e $(GR)$(PREFIX)test chpass..$(NC) 113 | $(PASS_NEW2) | $(NSA_CHPASS) 114 | $(NSA_COMMIT) 115 | $(NSA_STOP) 116 | $(PASS_NEW) | $(NSA_START) 117 | sleep 5 118 | @echo -e $(GR)$(PREFIX)test stop..$(NC) 119 | $(NSA_STOP) 120 | 121 | .PHONY: clean 122 | clean: 123 | # clean 124 | rm -rf $(CUR) $(IPC) 125 | -------------------------------------------------------------------------------- /tests/striptimestamps.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | input_file = sys.argv[1] 4 | with open(input_file, 'r') as f: 5 | db = json.load(f) 6 | for tags in db: 7 | del db[tags]['timestamp'] 8 | print(json.dumps(db, indent=4)) 9 | -------------------------------------------------------------------------------- /tests/truth/-h: -------------------------------------------------------------------------------- 1 | usage: nsa [-h] [-v] [-V] [-C] [-i DIR] 2 | {create,chpass,start,stop,do,add,del,mod,ls,diff,commit,drop} ... 3 | 4 | options: 5 | -h, --help show this help message and exit 6 | -v show information about nsapass 7 | -V show debugging information 8 | -C disable colourful output 9 | -i DIR ipc directory 10 | 11 | commands: 12 | {create,chpass,start,stop,do,add,del,mod,ls,diff,commit,drop} 13 | create create a databases 14 | chpass change databases's password 15 | start starts nsapass 16 | stop stops nsapass and discards any uncommitted changes 17 | do do things (e.g. type passwords) 18 | add add an entry 19 | del delete an entry 20 | mod modify an entry 21 | ls view entries 22 | diff show modifications done so far 23 | commit commit changes to the database 24 | drop discard all uncommitted changed 25 | -------------------------------------------------------------------------------- /tests/truth/-v: -------------------------------------------------------------------------------- 1 | nsapass v10 copyright (C) 2022 caveman 2 | https://github.com/Al-Caveman/nsapass 3 | 4 | this program comes with ABSOLUTELY NO WARRANTY; for details 5 | read https://github.com/Al-Caveman/nsapass/blob/master/LICENSE 6 | -------------------------------------------------------------------------------- /tests/truth/data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al-caveman/nsapass/982d99be4a66cc50e9f6856e9b4fec89f227a8b2/tests/truth/data -------------------------------------------------------------------------------- /tests/truth/test_db.add.str.cln: -------------------------------------------------------------------------------- 1 | { 2 | "tag1 tag2": { 3 | "username": null, 4 | "password": null, 5 | "datab64s": "AVRi7QPOIDF0FmxaxTBTjga33bY21iTavJQ7nyccemiRJpJko1iN/2NoTaLdXCqPG5FbgcA2fCOYZqFbhVogl+Xeub9sJ17IbGV8suMdyv36MZfq36Sqfeko1zcpEgsXd8m6cW0czniFZO1BvAjqafMGaGcWo5RooyW3c0rYzgpCJw+yFrjHadB/SEpaEE1jJq9T7lcYf1wbzTnc4Rn7hR4I5gBX31Mxv/lQilLagE76B16AW5hNjdECdCmen+vaWxm+MNNV+Lq/FE2t83BB8+8vOcd0y1LXB6KH7cBz7nFA7V5n8BzLwf7X/qWUOOhnpUMdTl1KTFFgwgvZzjw61ExOWyKR/TM0IVr7qCWW9+BdpYouv2FK7poTIMx5O88rYUnnLWKU7NykMkmX5kJVCzCpHehNOQAa9ihaq5KVJkf9Pq1xS5YejNVl9yJF9ut7T22UKMFToVitaT+//L6Jx15cug4vQIu2KpFtSJ2GW+b5xMyytWIj4dyrjWLJw4QcEwT1Vn8sOrL3mi6UZnQoIiWN3l+WVarswSzFKjyU/MdVkOPIUVv2ntUOorGw88MgkZyQ+GcHUbryFiEpb7Dvzc+Zb+GigfPcnHAcXJh5FWQNvINROftIcr/2c1XGqGq9MQN0miH179g5SgxZe/uBwWKqsL+cngE6NH0AtYXfOEvyT9PsB8070E1aJf0hBH5jRlnv1svJV6fX4N78aNz+1NXfNP4rq59B3qbqFB8i3yL/PPk8Z6dPpRn28H7P06QOEkM5ON12Nts+Yo49QrFkmnzSw1rrqlQSCUF9qA5T/W7m68gogVYLjfrERvm9MMso90iIkLscDPF7hrpaPJZUMfRRqUzmtz8zff0HOcQPq5TA/1amfPwLzChwTrBJQo/Qb1BaSHjEqPk2TZi9MoRBjhAuSoE2xSuY9q58jMeBsEGyp0Bm1g03l3kmc8SP4WUQJIqCNkSgKr+fMJ/D00euXeT/Cn029IMGGFbbf3PkPkjjA7icqh5mHtqyEnwNtq7ud1mzFHtbs81Vvx4HAaYvu92sm44Tq2eat8kAjJcP3VaN2Dx3QEHsIUjZCMWaUhm2Y04Q5rpyNn0G5nhsiTlKSqp8j2VKgBLPEgo0zTkH6mgTL4Qz5rZFebEIlj/4TZzxPyz/aWw0M50fIAA+Zf+MaCUNSrJd7xkrujn2vg2s+9q9K6wbL1Cplfc6GHQEj2AcViiFxWWxRr2ZmVbfLQa4bDLZpFq7jEEUd9Doa+/eweey9EwI/gYAR6SdSe/BpiZEbUNPqkZts3/E9AQgTlSlV1YetnovsgcoyRpmSoVh7OkKBG/uvOOFBrB5Qs+utFerf6bbvgnVde6Nv/hXawPv/Q==", 6 | "digest": "8c2b82f0d28afad29d48a2565cf866f34af65f32d2178c369e77d747", 7 | "uri": null, 8 | "note": "oy vey" 9 | }, 10 | "test asd dea s": { 11 | "username": null, 12 | "password": null, 13 | "datab64s": null, 14 | "digest": null, 15 | "uri": null, 16 | "note": null 17 | }, 18 | "test": { 19 | "username": null, 20 | "password": null, 21 | "datab64s": null, 22 | "digest": null, 23 | "uri": null, 24 | "note": null 25 | }, 26 | "asdfa lel": { 27 | "username": null, 28 | "password": "lel", 29 | "datab64s": null, 30 | "digest": null, 31 | "uri": null, 32 | "note": null 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/truth/test_db.create.str.cln: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/truth/test_db.del.str.cln: -------------------------------------------------------------------------------- 1 | { 2 | "tag1 tag2": { 3 | "username": null, 4 | "password": null, 5 | "datab64s": "AVRi7QPOIDF0FmxaxTBTjga33bY21iTavJQ7nyccemiRJpJko1iN/2NoTaLdXCqPG5FbgcA2fCOYZqFbhVogl+Xeub9sJ17IbGV8suMdyv36MZfq36Sqfeko1zcpEgsXd8m6cW0czniFZO1BvAjqafMGaGcWo5RooyW3c0rYzgpCJw+yFrjHadB/SEpaEE1jJq9T7lcYf1wbzTnc4Rn7hR4I5gBX31Mxv/lQilLagE76B16AW5hNjdECdCmen+vaWxm+MNNV+Lq/FE2t83BB8+8vOcd0y1LXB6KH7cBz7nFA7V5n8BzLwf7X/qWUOOhnpUMdTl1KTFFgwgvZzjw61ExOWyKR/TM0IVr7qCWW9+BdpYouv2FK7poTIMx5O88rYUnnLWKU7NykMkmX5kJVCzCpHehNOQAa9ihaq5KVJkf9Pq1xS5YejNVl9yJF9ut7T22UKMFToVitaT+//L6Jx15cug4vQIu2KpFtSJ2GW+b5xMyytWIj4dyrjWLJw4QcEwT1Vn8sOrL3mi6UZnQoIiWN3l+WVarswSzFKjyU/MdVkOPIUVv2ntUOorGw88MgkZyQ+GcHUbryFiEpb7Dvzc+Zb+GigfPcnHAcXJh5FWQNvINROftIcr/2c1XGqGq9MQN0miH179g5SgxZe/uBwWKqsL+cngE6NH0AtYXfOEvyT9PsB8070E1aJf0hBH5jRlnv1svJV6fX4N78aNz+1NXfNP4rq59B3qbqFB8i3yL/PPk8Z6dPpRn28H7P06QOEkM5ON12Nts+Yo49QrFkmnzSw1rrqlQSCUF9qA5T/W7m68gogVYLjfrERvm9MMso90iIkLscDPF7hrpaPJZUMfRRqUzmtz8zff0HOcQPq5TA/1amfPwLzChwTrBJQo/Qb1BaSHjEqPk2TZi9MoRBjhAuSoE2xSuY9q58jMeBsEGyp0Bm1g03l3kmc8SP4WUQJIqCNkSgKr+fMJ/D00euXeT/Cn029IMGGFbbf3PkPkjjA7icqh5mHtqyEnwNtq7ud1mzFHtbs81Vvx4HAaYvu92sm44Tq2eat8kAjJcP3VaN2Dx3QEHsIUjZCMWaUhm2Y04Q5rpyNn0G5nhsiTlKSqp8j2VKgBLPEgo0zTkH6mgTL4Qz5rZFebEIlj/4TZzxPyz/aWw0M50fIAA+Zf+MaCUNSrJd7xkrujn2vg2s+9q9K6wbL1Cplfc6GHQEj2AcViiFxWWxRr2ZmVbfLQa4bDLZpFq7jEEUd9Doa+/eweey9EwI/gYAR6SdSe/BpiZEbUNPqkZts3/E9AQgTlSlV1YetnovsgcoyRpmSoVh7OkKBG/uvOOFBrB5Qs+utFerf6bbvgnVde6Nv/hXawPv/Q==", 6 | "digest": "8c2b82f0d28afad29d48a2565cf866f34af65f32d2178c369e77d747", 7 | "uri": null, 8 | "note": "oy vey" 9 | }, 10 | "test asd dea s": { 11 | "username": null, 12 | "password": null, 13 | "datab64s": null, 14 | "digest": null, 15 | "uri": null, 16 | "note": null 17 | }, 18 | "asdfa lel": { 19 | "username": null, 20 | "password": "lel", 21 | "datab64s": null, 22 | "digest": null, 23 | "uri": null, 24 | "note": null 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/truth/test_db.mod.str.cln: -------------------------------------------------------------------------------- 1 | { 2 | "tag1 tag2": { 3 | "username": "barak", 4 | "password": "lel", 5 | "datab64s": "AVRi7QPOIDF0FmxaxTBTjga33bY21iTavJQ7nyccemiRJpJko1iN/2NoTaLdXCqPG5FbgcA2fCOYZqFbhVogl+Xeub9sJ17IbGV8suMdyv36MZfq36Sqfeko1zcpEgsXd8m6cW0czniFZO1BvAjqafMGaGcWo5RooyW3c0rYzgpCJw+yFrjHadB/SEpaEE1jJq9T7lcYf1wbzTnc4Rn7hR4I5gBX31Mxv/lQilLagE76B16AW5hNjdECdCmen+vaWxm+MNNV+Lq/FE2t83BB8+8vOcd0y1LXB6KH7cBz7nFA7V5n8BzLwf7X/qWUOOhnpUMdTl1KTFFgwgvZzjw61ExOWyKR/TM0IVr7qCWW9+BdpYouv2FK7poTIMx5O88rYUnnLWKU7NykMkmX5kJVCzCpHehNOQAa9ihaq5KVJkf9Pq1xS5YejNVl9yJF9ut7T22UKMFToVitaT+//L6Jx15cug4vQIu2KpFtSJ2GW+b5xMyytWIj4dyrjWLJw4QcEwT1Vn8sOrL3mi6UZnQoIiWN3l+WVarswSzFKjyU/MdVkOPIUVv2ntUOorGw88MgkZyQ+GcHUbryFiEpb7Dvzc+Zb+GigfPcnHAcXJh5FWQNvINROftIcr/2c1XGqGq9MQN0miH179g5SgxZe/uBwWKqsL+cngE6NH0AtYXfOEvyT9PsB8070E1aJf0hBH5jRlnv1svJV6fX4N78aNz+1NXfNP4rq59B3qbqFB8i3yL/PPk8Z6dPpRn28H7P06QOEkM5ON12Nts+Yo49QrFkmnzSw1rrqlQSCUF9qA5T/W7m68gogVYLjfrERvm9MMso90iIkLscDPF7hrpaPJZUMfRRqUzmtz8zff0HOcQPq5TA/1amfPwLzChwTrBJQo/Qb1BaSHjEqPk2TZi9MoRBjhAuSoE2xSuY9q58jMeBsEGyp0Bm1g03l3kmc8SP4WUQJIqCNkSgKr+fMJ/D00euXeT/Cn029IMGGFbbf3PkPkjjA7icqh5mHtqyEnwNtq7ud1mzFHtbs81Vvx4HAaYvu92sm44Tq2eat8kAjJcP3VaN2Dx3QEHsIUjZCMWaUhm2Y04Q5rpyNn0G5nhsiTlKSqp8j2VKgBLPEgo0zTkH6mgTL4Qz5rZFebEIlj/4TZzxPyz/aWw0M50fIAA+Zf+MaCUNSrJd7xkrujn2vg2s+9q9K6wbL1Cplfc6GHQEj2AcViiFxWWxRr2ZmVbfLQa4bDLZpFq7jEEUd9Doa+/eweey9EwI/gYAR6SdSe/BpiZEbUNPqkZts3/E9AQgTlSlV1YetnovsgcoyRpmSoVh7OkKBG/uvOOFBrB5Qs+utFerf6bbvgnVde6Nv/hXawPv/Q==", 6 | "digest": "8c2b82f0d28afad29d48a2565cf866f34af65f32d2178c369e77d747", 7 | "uri": "https://mossad.gov.il", 8 | "note": "oy vey" 9 | }, 10 | "test asd dea s": { 11 | "username": null, 12 | "password": null, 13 | "datab64s": "AVRi7QPOIDF0FmxaxTBTjga33bY21iTavJQ7nyccemiRJpJko1iN/2NoTaLdXCqPG5FbgcA2fCOYZqFbhVogl+Xeub9sJ17IbGV8suMdyv36MZfq36Sqfeko1zcpEgsXd8m6cW0czniFZO1BvAjqafMGaGcWo5RooyW3c0rYzgpCJw+yFrjHadB/SEpaEE1jJq9T7lcYf1wbzTnc4Rn7hR4I5gBX31Mxv/lQilLagE76B16AW5hNjdECdCmen+vaWxm+MNNV+Lq/FE2t83BB8+8vOcd0y1LXB6KH7cBz7nFA7V5n8BzLwf7X/qWUOOhnpUMdTl1KTFFgwgvZzjw61ExOWyKR/TM0IVr7qCWW9+BdpYouv2FK7poTIMx5O88rYUnnLWKU7NykMkmX5kJVCzCpHehNOQAa9ihaq5KVJkf9Pq1xS5YejNVl9yJF9ut7T22UKMFToVitaT+//L6Jx15cug4vQIu2KpFtSJ2GW+b5xMyytWIj4dyrjWLJw4QcEwT1Vn8sOrL3mi6UZnQoIiWN3l+WVarswSzFKjyU/MdVkOPIUVv2ntUOorGw88MgkZyQ+GcHUbryFiEpb7Dvzc+Zb+GigfPcnHAcXJh5FWQNvINROftIcr/2c1XGqGq9MQN0miH179g5SgxZe/uBwWKqsL+cngE6NH0AtYXfOEvyT9PsB8070E1aJf0hBH5jRlnv1svJV6fX4N78aNz+1NXfNP4rq59B3qbqFB8i3yL/PPk8Z6dPpRn28H7P06QOEkM5ON12Nts+Yo49QrFkmnzSw1rrqlQSCUF9qA5T/W7m68gogVYLjfrERvm9MMso90iIkLscDPF7hrpaPJZUMfRRqUzmtz8zff0HOcQPq5TA/1amfPwLzChwTrBJQo/Qb1BaSHjEqPk2TZi9MoRBjhAuSoE2xSuY9q58jMeBsEGyp0Bm1g03l3kmc8SP4WUQJIqCNkSgKr+fMJ/D00euXeT/Cn029IMGGFbbf3PkPkjjA7icqh5mHtqyEnwNtq7ud1mzFHtbs81Vvx4HAaYvu92sm44Tq2eat8kAjJcP3VaN2Dx3QEHsIUjZCMWaUhm2Y04Q5rpyNn0G5nhsiTlKSqp8j2VKgBLPEgo0zTkH6mgTL4Qz5rZFebEIlj/4TZzxPyz/aWw0M50fIAA+Zf+MaCUNSrJd7xkrujn2vg2s+9q9K6wbL1Cplfc6GHQEj2AcViiFxWWxRr2ZmVbfLQa4bDLZpFq7jEEUd9Doa+/eweey9EwI/gYAR6SdSe/BpiZEbUNPqkZts3/E9AQgTlSlV1YetnovsgcoyRpmSoVh7OkKBG/uvOOFBrB5Qs+utFerf6bbvgnVde6Nv/hXawPv/Q==", 14 | "digest": "8c2b82f0d28afad29d48a2565cf866f34af65f32d2178c369e77d747", 15 | "uri": null, 16 | "note": null 17 | }, 18 | "asdfa lel": { 19 | "username": null, 20 | "password": "lel", 21 | "datab64s": null, 22 | "digest": null, 23 | "uri": null, 24 | "note": null 25 | } 26 | } 27 | --------------------------------------------------------------------------------