diff --git a/.docker-test b/.docker-test new file mode 100644 index 0000000..d36fad8 --- /dev/null +++ b/.docker-test @@ -0,0 +1,32 @@ +#!/bin/bash + +ruby_version=${1:-2.4.2} + +if ! rvm use ruby-${ruby_version} &>/dev/null ; then + echo "The ruby version '${ruby_version}' doesn't exist!" + echo "Available versions are:" + rvm list rubies strings | cut -d '-' -f2 + exit 2 +fi + +echo '# ---------------------------------' +echo "# Use ruby version: ${ruby_version}" +echo '# ---------------------------------' + +cp -r /mpw ~/mpw +cd ~/mpw +gem install bundler --no-ri --no-rdoc +bundle install +gem build mpw.gemspec +gem install mpw-$(cat VERSION).gem +cp -a /dev/urandom /dev/random + +rubocop +ruby ./test/init.rb +ruby ./test/test_config.rb +ruby ./test/test_item.rb +ruby ./test/test_mpw.rb +ruby ./test/test_translate.rb +ruby ./test/init.rb +ruby ./test/test_cli.rb +ruby ./test/test_import.rb diff --git a/.gitignore b/.gitignore index b844b14..afd83c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ Gemfile.lock +*.gem +.yardoc +doc diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..56d1540 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,38 @@ + +AllCops: + Exclude: + - db/**/* + - config/**/* + - Vagrantfile + TargetRubyVersion: 2.3 + +Naming/AccessorMethodName: + Enabled: false + +Lint/RescueWithoutErrorClass: + Enabled: false + +Metrics/LineLength: + Max: 120 +Metrics/CyclomaticComplexity: + Enabled: false +Metrics/PerceivedComplexity: + Enabled: false +Metrics/MethodLength: + Enabled: false +Metrics/BlockLength: + Enabled: false +Metrics/ClassLength: + Enabled: false +Metrics/AbcSize: + Enabled: false + +Style/NumericLiteralPrefix: + Enabled: false +Style/FrozenStringLiteralComment: + Enabled: false +Style/CommandLiteral: + Enabled: true + EnforcedStyle: percent_x +Style/Documentation: + Enabled: false diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..14d696a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +language: ruby +dist: precise +rvm: + - 2.4.2 + - 2.3.5 + - 2.2.8 + - 2.1.10 +install: + - sudo cp -a /dev/urandom /dev/random + - sudo apt-get purge -y gnupg-agent gnupg2 + - bundle install + - gem build mpw.gemspec + - gem install mpw-$(cat VERSION).gem +script: + - rubocop + - ruby ./test/init.rb + - ruby ./test/test_config.rb + - ruby ./test/test_item.rb + - ruby ./test/test_mpw.rb + - ruby ./test/test_translate.rb + - ruby ./test/init.rb + - ruby ./test/test_cli.rb + - ruby ./test/test_import.rb diff --git a/CHANGELOG b/CHANGELOG deleted file mode 100644 index 1ce4809..0000000 --- a/CHANGELOG +++ /dev/null @@ -1,13 +0,0 @@ -= CHANGELOG = -== v2.0.0 == - -* change format csv to yaml -* easy install with gem -* add sync with ftp and ssh -* many improvement - -== v1.1.0 == - -* Add sync with MPW Server -* Add MPW Server -* Fix minors bugs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..199b863 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,98 @@ +# CHANGELOG +## v4.2.2 (2017-08-15) + + * minor improvements in the interface + +## v4.2.1 (2017-07-30) + + * fix bug in otp generator + +## v4.2.0 (2017-06-06) + + * feat: improve the interface + * feat: add copy url + * feat: add unit tests for cli + * feat: comment the code with yarn syntax + * fix several bugs + * fix translations + +## v4.1.1 (2017-05-03) + + * fix bug in init + +## v4.1.0 (2017-04-22) + + * feat: add options to update or add an item in command line + * feat: print config + * feat: add a specific path for a wallet + * feat: add rubocop to fix syntax + * fix: pinentry mode with gpg >= 2.1 + * remove SSH and FTP synchronization + +## v4.0.0 (2017-03-09) + + * feature: set default wallet + * add option for generate a random password when you update an item + * fix encryption when you share an existing wallet + * several bugs fix + +## v4.0.0-beta1 (2017-02-16) + + * add manage share key with new interface + +## v4.0.0-beta (2016-11-11) + + * new interface with a table + * new command line interface + * use text editor for add or update an item + * fix generate gpg key with RSA + * several bugs fix + * add unit tests + +## v3.2.1 (2016-08-06) + + * fix bug when add a new item + +## v3.2.0 (2016-08-03) + + * add support OTP + * fix bug in synchronize + * improve interface + +## v3.1.0 (2016-07-09) + + * add clipboard + * can change gpg version + * minor change in interface + * several bugs fix + +## v3.0.0 (2016-07-05) + + * new storage format + * new share system + * remove MPW server + +## v2.0.3 (2015-09-27) + + * add no-sync option + +## v2.0.1 (2015-06-23) + + * fix mpw-ssh + +## v2.0.0 (2015-06-22) + + * change format csv to yaml + * easy install with gem + * add sync with ftp and ssh + * many improvement + +## v1.1.0 (2014-01-28) + + * Add sync with MPW Server + * Add MPW Server + * Fix minors bugs + +## v1.0.0 (2014-01-15) + + * first release diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6654780 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM debian:stretch +MAINTAINER Adrien Waksberg "mpw@yae.im" + +RUN apt update +RUN apt dist-upgrade -y + +RUN apt install -y procps gnupg1 curl git +RUN ln -snvf /usr/bin/gpg1 /usr/bin/gpg +RUN gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB +RUN curl -sSL https://get.rvm.io | bash -s stable +RUN echo 'source "/usr/local/rvm/scripts/rvm"' >> /etc/bash.bashrc + +RUN /bin/bash -l -c "rvm install 2.4.2" +RUN /bin/bash -l -c "rvm install 2.3.5" +RUN /bin/bash -l -c "rvm install 2.2.8" +RUN /bin/bash -l -c "rvm install 2.1.10" diff --git a/Gemfile b/Gemfile index 1ccb9c5..a76b2c1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,14 @@ source 'https://rubygems.org' -gem 'highline' -gem 'i18n', '0.6.9' -gem 'locale' -gem 'gpgme' -gem 'colorize' +gem 'clipboard', '~> 1.1', '>= 1.1.1' +gem 'colorize', '~> 0.8', '>= 0.8.1' +gem 'gpgme', '~> 2.0', '>= 2.0.14' +gem 'highline', '~> 1.7', '>= 1.7.8' +gem 'i18n', '~> 0.9', '>= 0.9.1' +gem 'locale', '~> 2.1', '>= 2.1.2' +gem 'rotp', '~> 3.3', '>= 3.3.0' -group :ssh do - gem 'net-sftp' +group :development do + gem 'rubocop', '0.50.0' + gem 'test-unit' + gem 'yard' end diff --git a/LICENSE b/LICENSE index d159169..0c5ea8e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,339 +1,201 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - Preamble + 1. Definitions. - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - The precise terms and conditions for copying, distribution and -modification follow. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) + END OF TERMS AND CONDITIONS -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. + APPENDIX: How to apply the Apache License to your work. -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. + Copyright 2017 Adrien Waksberg - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. + http://www.apache.org/licenses/LICENSE-2.0 - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - <one line to give the program's name and a brief idea of what it does.> - Copyright (C) <year> <name of author> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - <signature of Ty Coon>, 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index cbf6e64..be9e7cb 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,195 @@ -# Manage your passwords! +# MPW: Manage your passwords! +[](https://github.com/nishiki/manage-password/releases) +[](https://travis-ci.org/nishiki/manage-password) +[](https://github.com/nishiki/manage-password/blob/master/LICENSE) -MPW is a little software which stores your passwords in an GPG encrypted file. -MPW can synchronize your password with a MPW Server or via SSH or via FTP. +mpw is a little software which stores your passwords in [GnuPG](http://www.gnupg.org/) encrypted files. -# Installation +## Features -You must generate a GPG Key with GPG or with Seahorse (GUI on linux). -This program work with ruby >= 1.9 + * generate random password + * generate OTP code + * copy your login, password or otp in clipboard + * manage many wallets + * share a wallet with others GPG keys -* install ruby and rubygems on your computer -* gem install mpw +## Install -If you want use mpw-ssh, you must install sshpass +On debian or ubuntu: +``` +apt install ruby ruby-dev xclip +gem install mpw +``` -# How to use +## How to use +### First steps -mpw -h +Initialize your first wallet: +``` +mpw config --init user@host.com +``` + +Add your first item: +``` +mpw add --host assurance.com --port 443 --user user_2132 --protocol https --random +mpw add --host fric.com --user 230403 --otp-code 23434113 --protocol https --comment 'I love my bank' --random +``` + +And list your items: +``` +mpw list +``` +or search an item with +``` +mpw list --pattern love +mpw list --group bank +``` + +Output: +``` +Assurance + ========================================================================== + ID | Host | User | OTP | Comment + ========================================================================== + 1 | https://assurance.com:443 | user_2132 | | + +Bank + ========================================================================== + ID | Host | User | OTP | Comment + ========================================================================== + 3 | https://fric.com | 230403 | X | I love my bank +``` + +Copy a password, login or OTP code: +``` +mpw copy -p assurance.com +``` + +Update an item: +``` +mpw update -p assurance.com +``` + +Delete an item: +``` +mpw delete -p assurance.com +``` + +### Manage wallets + +List all available wallets: +``` +mpw wallet +``` + +List all GPG keys in wallet: +``` +mpw wallet --list-keys [--wallet NAME] +``` + +Share with an other GPG key: +``` +mpw wallet --add-gpg-key test42@localhost.com + or +mpw wallet --add-gpg-key /path/to/file +``` + +Remove a GPG key: +``` +mpw wallet --delete-gpg-key test42@localhost.com +``` + +### Export and import data + +You can export your data in yaml file with your passwords in clear text: +``` +mpw export --file export.yml +``` + +Import data from an yaml file: +``` +mpw import --file import.yml +``` + +Example yaml file for mpw: + +``` +--- +1: + host: fric.com + user: 230403 + group: Bank + password: 5XdiTQOubRDw9B0aJoMlcEyL + protocol: https + port: + otp_key: 330223432 + comment: I love my bank +2: + host: assurance.com + user: user_2132 + group: Assurance + password: DMyK6B3v4bWO52VzU7aTHIem + protocol: https + port: 443 + otp_key: + comment: +``` + +### Config + +Print the current config +``` +mpw config +``` + +Output: + +``` +Configuration + ============================================== + lang | fr + gpg_key | mpw@yae.im + default_wallet | + config_dir | /home/mpw/.config/mpw + pinmode | true + gpg_exe | + path_wallet_test | /tmp/test.mpw + password_numeric | true + password_alpha | true + password_special | false + password_length | 16 + +``` + +## Development + +Don't run the tests on your local machine, you risk to lost your datas. + +### Test on local machine with docker + + * install [docker](https://docs.docker.com/engine/installation/) + * run the tests + +``` +docker run -v $(pwd):/mpw:ro -it nishiki/ruby:stretch /bin/bash -l /mpw/.docker-test +``` + +## License + +``` +* Author:: Adrien Waksberg <mpw@yae.im> + +Copyright (c) 2013-2017 Adrien Waksberg + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/VERSION b/VERSION index 38f77a6..af8c8ec 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.1 +4.2.2 diff --git a/bin/mpw b/bin/mpw index d8a76c7..11bc726 100755 --- a/bin/mpw +++ b/bin/mpw @@ -1,17 +1,30 @@ -#!/usr/bin/ruby -# author: nishiki -# mail: nishiki@yaegashi.fr -# info: a simple script who manage your passwords +#!/usr/bin/env ruby +# +# Copyright:: 2013, Adrien Waksberg +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +$LOAD_PATH << File.expand_path('../../lib', __FILE__) -require 'rubygems' -require 'optparse' -require 'pathname' require 'locale' require 'set' require 'i18n' -require 'mpw/mpw' -require 'mpw/config' -require 'mpw/ui/cli' +require 'colorize' # --------------------------------------------------------- # # Set local @@ -20,13 +33,11 @@ require 'mpw/ui/cli' lang = Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1] if defined?(I18n.enforce_available_locales) - I18n.enforce_available_locales = true + I18n.enforce_available_locales = true end -APP_ROOT = File.dirname(Pathname.new(__FILE__).realpath) - I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks) -I18n.load_path = Dir["#{APP_ROOT}/../i18n/cli/*.yml"] +I18n.load_path = Dir["#{File.expand_path('../../i18n', __FILE__)}/*.yml"] I18n.default_locale = :en I18n.locale = lang.to_sym @@ -34,132 +45,28 @@ I18n.locale = lang.to_sym # Options # --------------------------------------------------------- # -options = {} -options[:force] = false -options[:format] = false -options[:group] = nil -options[:config] = nil +bin_dir = File.dirname(__FILE__) +command = "#{bin_dir}/mpw-#{ARGV[0]}" -OptionParser.new do |opts| - opts.banner = "#{I18n.t('option.usage')}: mpw [options]" +if Dir.glob("#{bin_dir}/mpw-*").include?(command.to_s) + begin + Kernel.load(command) + rescue OptionParser::ParseError => e + puts "#{I18n.t('display.error')}: #{e}".red + end +else + puts "#{I18n.t('option.usage')}: mpw COMMAND [options]\n\n" + puts 'Commands:' + puts " add #{I18n.t('command.add')}" + puts " config #{I18n.t('command.config')}" + puts " copy #{I18n.t('command.copy')}" + puts " delete #{I18n.t('command.delete')}" + puts " export #{I18n.t('command.export')}" + puts " genpwd #{I18n.t('command.genpwd')}" + puts " import #{I18n.t('command.import')}" + puts " list #{I18n.t('command.list')}" + puts " update #{I18n.t('command.update')}" + puts " wallet #{I18n.t('command.wallet')}" - opts.on('-s', '--show [SEARCH]', I18n.t('option.show')) do |search| - search.nil? ? (options[:show] = '') : (options[:show] = search) - end - - opts.on('-A', '--show-all', I18n.t('option.show_all')) do - options[:type] = nil - options[:show] = '' - end - - opts.on('-u', '--update ID', I18n.t('option.update')) do |id| - options[:update] = id - end - - opts.on('-d', '--delete ID', I18n.t('option.remove')) do |id| - options[:delete] = id - end - - opts.on('-g', '--group GROUP', I18n.t('option.group')) do |group| - options[:group] = group - end - - opts.on('-a', '--add', I18n.t('option.add')) do - options[:add] = true - end - - opts.on('-c', '--config CONFIG', I18n.t('option.config')) do |config| - options[:config] = config - end - - opts.on('-S', '--setup', I18n.t('option.setup')) do - options[:setup] = true - end - - opts.on('-p', '--protocol PROTOCOL', I18n.t('option.protocol')) do |protocol| - options[:protocol] = protocol - end - - opts.on('-e', '--export FILE', I18n.t('option.export')) do |file| - options[:export] = file - options[:type] = :yaml - end - - opts.on('-t', '--type TYPE', I18n.t('option.type')) do |type| - options[:type] = type.to_sym - end - - opts.on('-i', '--import FILE', I18n.t('option.import')) do |file| - options[:import] = file - options[:type] = :yaml - end - - opts.on('-f', '--force', I18n.t('option.force')) do - options[:force] = true - end - - opts.on('-G', '--generate-password [LENGTH]', I18n.t('option.generate_password')) do |length| - puts MPW::MPW::password(length) - exit 0 - end - - opts.on('-h', '--help', I18n.t('option.help')) do - puts opts - exit 0 - end -end.parse! - -# --------------------------------------------------------- # -# Main -# --------------------------------------------------------- # - -config = MPW::Config.new(options[:config]) -check_error = config.checkconfig - -cli = Cli.new(config) - -# Setup a new config -if not check_error or not options[:setup].nil? - cli.setup(lang) -elsif not config.check_gpg_key? - cli.setup_gpg_key + exit 3 end - -cli.decrypt -cli.sync - -# Display the item's informations -if not options[:show].nil? - opts = {search: options[:show], - group: options[:group], - protocol: options[:protocol], - } - - cli.display(opts) - -# Remove an item -elsif not options[:delete].nil? - cli.delete(options[:delete], options[:force]) - -# Update an item -elsif not options[:update].nil? - cli.update(options[:update]) - -# Add a new item -elsif not options[:add].nil? - cli.add - -# Export -elsif not options[:export].nil? - cli.export(options[:export], options[:type]) - -# Add a new item -elsif not options[:import].nil? - cli.import(options[:import], options[:type], options[:force]) - -# Interactive mode -end - -cli = nil - -exit 0 diff --git a/bin/mpw-add b/bin/mpw-add new file mode 100644 index 0000000..e08caae --- /dev/null +++ b/bin/mpw-add @@ -0,0 +1,84 @@ +# +# Copyright:: 2013, Adrien Waksberg +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'optparse' +require 'mpw/config' +require 'mpw/cli' + +# --------------------------------------------------------- # +# Options +# --------------------------------------------------------- # + +values = {} +options = {} +options[:text_editor] = true + +OptionParser.new do |opts| + opts.banner = "#{I18n.t('option.usage')}: mpw add [options]" + + opts.on('-c', '--config PATH', I18n.t('option.config')) do |config| + options[:config] = config + end + + opts.on('-C', '--comment COMMENT', I18n.t('option.comment')) do |comment| + values[:comment] = comment + end + + opts.on('-G', '--group NAME', I18n.t('option.new_group')) do |group| + values[:group] = group + end + + opts.on('-h', '--help', I18n.t('option.help')) do + puts opts + exit 0 + end + + opts.on('-o', '--otp-code CODE', I18n.t('option.otp_code')) do |otp| + values[:otp_key] = otp + end + + opts.on('-r', '--random', I18n.t('option.random_password')) do + options[:password] = true + end + + opts.on('-t', '--text-editor', I18n.t('option.text_editor')) do + options[:text_editor] = true + end + + opts.on('-u', '--url URL', I18n.t('option.url')) do |url| + values[:url] = url + end + + opts.on('-U', '--user USER', I18n.t('option.user')) do |user| + values[:user] = user + end + + opts.on('-w', '--wallet NAME', I18n.t('option.wallet')) do |wallet| + options[:wallet] = wallet + end +end.parse! + +config = MPW::Config.new(options[:config]) +cli = MPW::Cli.new(config) + +cli.load_config +cli.get_wallet(options[:wallet]) +cli.decrypt +cli.add(options[:password], options[:text_editor], values) diff --git a/bin/mpw-config b/bin/mpw-config new file mode 100644 index 0000000..2ac0f77 --- /dev/null +++ b/bin/mpw-config @@ -0,0 +1,121 @@ +# +# Copyright:: 2013, Adrien Waksberg +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'optparse' +require 'mpw/config' +require 'mpw/cli' + +# --------------------------------------------------------- # +# Options +# --------------------------------------------------------- # + +options = {} +values = {} + +OptionParser.new do |opts| + opts.banner = "#{I18n.t('option.usage')}: mpw config [options]" + + opts.on('-c', '--config PATH', I18n.t('option.config')) do |config| + options[:config] = config + end + + opts.on('-d', '--default-wallet NAME', I18n.t('option.default_wallet')) do |default_wallet| + values[:default_wallet] = default_wallet + end + + opts.on('-g', '--gpg-exe PATH', I18n.t('option.gpg_exe')) do |gpg_exe| + values[:gpg_exe] = gpg_exe + end + + opts.on('-h', '--help', I18n.t('option.help')) do + puts opts + exit 0 + end + + opts.on('-i', '--init GPG_KEY', I18n.t('option.init')) do |gpg_key| + options[:init] = true + values[:gpg_key] = gpg_key + end + + opts.on('-k', '--key GPG_KEY', I18n.t('option.gpg_key')) do |gpg_key| + values[:gpg_key] = gpg_key + end + + opts.on('-L', '--lang LANG', I18n.t('option.lang')) do |lang| + values[:lang] = lang + end + + opts.on('-P', '--enable-pinmode', I18n.t('option.pinmode')) do + values[:pinmode] = true + end + + opts.on('-p', '--disable-pinmode', I18n.t('option.disable_pinmode')) do + values[:pinmode] = false + end + + opts.on('-w', '--wallet-dir PATH', I18n.t('option.wallet_dir')) do |wallet_dir| + values[:wallet_dir] = wallet_dir + end + + opts.on('-l', '--length NUMBER', I18n.t('option.length')) do |length| + values[:pwd_length] = length.to_i + end + + opts.on('-n', '--numeric', I18n.t('option.numeric')) do + values[:pwd_numeric] = true + end + + opts.on('-N', '--disable-numeric', I18n.t('option.disable_numeric')) do + values[:pwd_numeric] = false + end + + opts.on('-s', '--special-chars', I18n.t('option.special_chars')) do + values[:pwd_special] = true + end + + opts.on('-S', '--disable-special-chars', I18n.t('option.special_chars')) do + values[:pwd_special] = false + end + + opts.on('-a', '--alpha', I18n.t('option.alpha')) do + values[:pwd_alpha] = true + end + + opts.on('-A', '--disable-alpha', I18n.t('option.disable_alpha')) do + values[:pwd_alpha] = false + end +end.parse! + +config = MPW::Config.new(options[:config]) +cli = MPW::Cli.new(config) + +if options.key?(:init) + cli.setup(values) + cli.load_config + cli.get_wallet + cli.setup_gpg_key(values[:gpg_key]) +else + cli.load_config + if values.empty? + cli.list_config + else + cli.set_config(values) + end +end diff --git a/bin/mpw-copy b/bin/mpw-copy new file mode 100644 index 0000000..fc3b6e0 --- /dev/null +++ b/bin/mpw-copy @@ -0,0 +1,68 @@ +# +# Copyright:: 2013, Adrien Waksberg +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'optparse' +require 'mpw/config' +require 'mpw/cli' + +# --------------------------------------------------------- # +# Options +# --------------------------------------------------------- # + +options = {} +options[:clipboard] = true +values = {} + +OptionParser.new do |opts| + opts.banner = "#{I18n.t('option.usage')}: mpw copy [options]" + + opts.on('-c', '--config PATH', I18n.t('option.config')) do |config| + options[:config] = config + end + + opts.on('-d', '--disable-clipboard', I18n.t('option.clipboard')) do + options[:clipboard] = false + end + + opts.on('-g', '--group NAME', I18n.t('option.group')) do |group| + values[:group] = group + end + + opts.on('-h', '--help', I18n.t('option.help')) do + puts opts + exit 0 + end + + opts.on('-p', '--pattern PATTERN', I18n.t('option.pattern')) do |pattern| + values[:pattern] = pattern + end + + opts.on('-w', '--wallet NAME', I18n.t('option.wallet')) do |wallet| + options[:wallet] = wallet + end +end.parse! + +config = MPW::Config.new(options[:config]) +cli = MPW::Cli.new(config) + +cli.load_config +cli.get_wallet(options[:wallet]) +cli.decrypt +cli.copy(options[:clipboard], values) diff --git a/bin/mpw-delete b/bin/mpw-delete new file mode 100644 index 0000000..48eb792 --- /dev/null +++ b/bin/mpw-delete @@ -0,0 +1,63 @@ +# +# Copyright:: 2013, Adrien Waksberg +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'optparse' +require 'mpw/config' +require 'mpw/cli' + +# --------------------------------------------------------- # +# Options +# --------------------------------------------------------- # + +options = {} +values = {} + +OptionParser.new do |opts| + opts.banner = "#{I18n.t('option.usage')}: mpw delete [options]" + + opts.on('-c', '--config PATH', I18n.t('option.config')) do |config| + options[:config] = config + end + + opts.on('-g', '--group NAME', I18n.t('option.group')) do |group| + values[:group] = group + end + + opts.on('-h', '--help', I18n.t('option.help')) do + puts opts + exit 0 + end + + opts.on('-p', '--pattern PATTERN', I18n.t('option.pattern')) do |pattern| + values[:pattern] = pattern + end + + opts.on('-w', '--wallet NAME', I18n.t('option.wallet')) do |wallet| + options[:wallet] = wallet + end +end.parse! + +config = MPW::Config.new(options[:config]) +cli = MPW::Cli.new(config) + +cli.load_config +cli.get_wallet(options[:wallet]) +cli.decrypt +cli.delete(values) diff --git a/bin/mpw-export b/bin/mpw-export new file mode 100644 index 0000000..92eb7bd --- /dev/null +++ b/bin/mpw-export @@ -0,0 +1,64 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'optparse' +require 'mpw/config' +require 'mpw/cli' + +# --------------------------------------------------------- # +# Options +# --------------------------------------------------------- # + +options = {} +values = {} + +OptionParser.new do |opts| + opts.banner = "#{I18n.t('option.usage')}: mpw wallet [options]" + + opts.on('-c', '--config PATH', I18n.t('option.config')) do |config| + options[:config] = config + end + + opts.on('-f', '--file PATH', I18n.t('option.file_export')) do |file| + options[:file] = file + end + + opts.on('-g', '--group GROUP', I18n.t('option.group')) do |group| + values[:group] = group + end + + opts.on('-h', '--help', I18n.t('option.help')) do + puts opts + exit 0 + end + + opts.on('-p', '--pattern PATTERN', I18n.t('option.pattern')) do |pattern| + values[:pattern] = pattern + end + + opts.on('-w', '--wallet NAME', I18n.t('option.wallet')) do |wallet| + options[:wallet] = wallet + end +end.parse! + +config = MPW::Config.new(options[:config]) +cli = MPW::Cli.new(config) + +cli.load_config +cli.get_wallet(options[:wallet]) +cli.decrypt +cli.export(options[:file], values) diff --git a/bin/mpw-genpwd b/bin/mpw-genpwd new file mode 100644 index 0000000..f6ca795 --- /dev/null +++ b/bin/mpw-genpwd @@ -0,0 +1,49 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'optparse' +require 'mpw/mpw' + +options = {} + +OptionParser.new do |opts| + opts.banner = "#{I18n.t('option.usage')}: mpw passwd [options]" + + opts.on('-h', '--help', I18n.t('option.help')) do + puts opts + exit 0 + end + + opts.on('-l', '--length NUMBER', I18n.t('option.length')) do |length| + options[:length] = length.to_i + end + + opts.on('-n', '--numeric', I18n.t('option.numeric')) do + options[:numeric] = true + end + + opts.on('-s', '--special-chars', I18n.t('option.special_chars')) do + options[:special] = true + end + + opts.on('-a', '--alpha', I18n.t('option.alpha')) do + options[:alpha] = true + end +end.parse! + +puts MPW::MPW.password(options) +exit 0 diff --git a/bin/mpw-import b/bin/mpw-import new file mode 100644 index 0000000..d0deae9 --- /dev/null +++ b/bin/mpw-import @@ -0,0 +1,65 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'optparse' +require 'mpw/config' +require 'mpw/cli' + +# --------------------------------------------------------- # +# Options +# --------------------------------------------------------- # + +formats = + Dir["#{File.expand_path('../../lib/mpw/import', __FILE__)}/*.rb"] + .map { |v| File.basename(v, '.rb') } + .join(', ') +options = { + format: 'mpw' +} + +OptionParser.new do |opts| + opts.banner = "#{I18n.t('option.usage')}: mpw import [options]" + + opts.on('-c', '--config PATH', I18n.t('option.config')) do |config| + options[:config] = config + end + + opts.on('-f', '--file PATH', I18n.t('option.file_import')) do |file| + options[:file] = file + end + + opts.on('-F', '--format STRING', I18n.t('option.file_format', formats: formats)) do |format| + options[:format] = format + end + + opts.on('-h', '--help', I18n.t('option.help')) do + puts opts + exit 0 + end + + opts.on('-w', '--wallet NAME', I18n.t('option.wallet')) do |wallet| + options[:wallet] = wallet + end +end.parse! + +config = MPW::Config.new(options[:config]) +cli = MPW::Cli.new(config) + +cli.load_config +cli.get_wallet(options[:wallet]) +cli.decrypt +cli.import(options[:file], options[:format]) diff --git a/bin/mpw-list b/bin/mpw-list new file mode 100644 index 0000000..fb7899c --- /dev/null +++ b/bin/mpw-list @@ -0,0 +1,60 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'optparse' +require 'mpw/config' +require 'mpw/cli' + +# --------------------------------------------------------- # +# Options +# --------------------------------------------------------- # + +options = {} +values = {} + +OptionParser.new do |opts| + opts.banner = "#{I18n.t('option.usage')}: mpw list [options]" + + opts.on('-c', '--config PATH', I18n.t('option.config')) do |config| + options[:config] = config + end + + opts.on('-g', '--group NAME', I18n.t('option.group')) do |group| + values[:group] = group + end + + opts.on('-h', '--help', I18n.t('option.help')) do + puts opts + exit 0 + end + + opts.on('-p', '--pattern PATTERN', I18n.t('option.pattern')) do |pattern| + values[:pattern] = pattern + end + + opts.on('-w', '--wallet NAME', I18n.t('option.wallet')) do |wallet| + options[:wallet] = wallet + end +end.parse! + +config = MPW::Config.new(options[:config]) +cli = MPW::Cli.new(config) + +cli.load_config +cli.get_wallet(options[:wallet]) +cli.decrypt +cli.list(values) diff --git a/bin/mpw-server b/bin/mpw-server deleted file mode 100755 index 3a7ce5a..0000000 --- a/bin/mpw-server +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/ruby -# author: nishiki -# mail: nishiki@yaegashi.fr -# info: a simple script who manage your passwords - -require 'rubygems' -require 'optparse' -require 'pathname' -require 'locale' -require 'i18n' -require 'mpw/server' - -APP_ROOT = File.dirname(Pathname.new(__FILE__).realpath) - -# --------------------------------------------------------- # -# Set local -# --------------------------------------------------------- # - -lang = Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1] - -if defined?(I18n.enforce_available_locales) - I18n.enforce_available_locales = true -end - -I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks) -I18n.load_path = Dir["#{APP_ROOT}/../i18n/server/*.yml"] -I18n.default_locale = :en -I18n.locale = lang.to_sym - -# --------------------------------------------------------- # -# Options -# --------------------------------------------------------- # - -options = {} -OptionParser.new do |opts| - opts.banner = "#{I18n.t('option.usage')}: mpw-server -c CONFIG [options]" - - opts.on("-c", "--config CONFIG", I18n.t('option.config')) do |config| - options[:config] = config - end - - opts.on("-t", "--checkconfig", I18n.t('option.checkconfig')) do |b| - options[:checkconfig] = b - end - - opts.on("-s", "--setup", I18n.t('option.setup')) do |b| - options[:setup] = b - end - - opts.on("-h", "--help", I18n.t('option.help')) do |b| - puts opts - exit 0 - end -end.parse! - -# --------------------------------------------------------- # -# Main -# --------------------------------------------------------- # - -if options[:config].nil? or options[:config].empty? - puts "#{I18n.t('option.usage')}: mpw-server -c CONFIG [options]" - exit 2 -end - -server = MPW::Server.new - -if options[:checkconfig] - server.checkconfig(options[:config]) -elsif options[:setup] - server.setup(options[:config]) -else - if server.checkconfig(options[:config]) - server.start - end -end - -server = nil -exit 0 diff --git a/bin/mpw-ssh b/bin/mpw-ssh deleted file mode 100755 index b5f1252..0000000 --- a/bin/mpw-ssh +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/ruby -# author: nishiki -# mail: nishiki@yaegashi.fr -# info: a simple script who manage your passwords - -require 'rubygems' -require 'optparse' -require 'pathname' -require 'locale' -require 'i18n' -require 'mpw/ui/clissh' -require 'mpw/config' - -# --------------------------------------------------------- # -# Set local -# --------------------------------------------------------- # - -APP_ROOT = File.dirname(Pathname.new(__FILE__).realpath) -lang = Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1] - -if defined?(I18n.enforce_available_locales) - I18n.enforce_available_locales = true -end - -I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks) -I18n.load_path = Dir["#{APP_ROOT}/../i18n/cli/*.yml"] -I18n.default_locale = :en -I18n.locale = lang.to_sym - -# --------------------------------------------------------- # -# Options -# --------------------------------------------------------- # - -options = {} -OptionParser.new do |opts| - opts.banner = "#{I18n.t('ssh.option.usage')}: mpw-ssh SEARCH [options]" - - opts.on("-l", "--login LOGIN", I18n.t('ssh.option.login')) do |login| - options[:login] = login - end - - opts.on("-s", "--server SERVER", I18n.t('ssh.option.server')) do |server| - options[:server] = server - end - - opts.on("-p", "--port PORT", I18n.t('ssh.option.port')) do |port| - options[:port] = port - end - - opts.on('-c', '--config CONFIG', I18n.t('cli.option.config')) do |config| - options[:config] = config - end - - opts.on("-h", "--help", I18n.t('ssh.option.help')) do - puts opts - exit 0 - end -end.parse! - -# --------------------------------------------------------- # -# Main -# --------------------------------------------------------- # - -config = MPW::Config.new(options[:config]) -check_error = config.checkconfig - -cli = CliSSH.new(config) -cli.login = options[:login] -cli.server = options[:server] -cli.port = options[:port] - -search = ARGV[0] - -# Setup a new config -if not check_error - cli.setup(lang) - -elsif ARGV.length < 1 - puts "#{I18n.t('ssh.option.usage')}: mpw-ssh SEARCH [options]" - exit 2 -else - cli.decrypt - cli.sync - cli.ssh(search) -end - -cli = nil - -exit 0 diff --git a/bin/mpw-update b/bin/mpw-update new file mode 100644 index 0000000..26a55c9 --- /dev/null +++ b/bin/mpw-update @@ -0,0 +1,92 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'optparse' +require 'mpw/config' +require 'mpw/cli' + +# --------------------------------------------------------- # +# Options +# --------------------------------------------------------- # + +values = {} +search = {} +options = {} +options[:text_editor] = false + +OptionParser.new do |opts| + opts.banner = "#{I18n.t('option.usage')}: mpw update [options]" + + opts.on('-c', '--config PATH', I18n.t('option.config')) do |config| + options[:config] = config + end + + opts.on('-C', '--comment COMMENT', I18n.t('option.comment')) do |comment| + values[:comment] = comment + end + + opts.on('-g', '--group NAME', I18n.t('option.group')) do |group| + search[:group] = group + end + + opts.on('-G', '--new-group NAME', I18n.t('option.new_group')) do |group| + values[:group] = group + end + + opts.on('-h', '--help', I18n.t('option.help')) do + puts opts + exit 0 + end + + opts.on('-o', '--otp-code CODE', I18n.t('option.otp_code')) do |otp| + values[:otp_key] = otp + end + + opts.on('-p', '--pattern PATTERN', I18n.t('option.pattern')) do |pattern| + search[:pattern] = pattern + end + + opts.on('-r', '--random', I18n.t('option.random_password')) do + options[:password] = true + end + + opts.on('-t', '--text-editor', I18n.t('option.text_editor')) do + options[:text_editor] = true + end + + opts.on('-u', '--url URL', I18n.t('option.url')) do |url| + values[:url] = url + end + + opts.on('-U', '--user USER', I18n.t('option.user')) do |user| + values[:user] = user + end + + opts.on('-w', '--wallet NAME', I18n.t('option.wallet')) do |wallet| + options[:wallet] = wallet + end +end.parse! + +config = MPW::Config.new(options[:config]) +cli = MPW::Cli.new(config) + +options[:text_editor] = true if values.empty? + +cli.load_config +cli.get_wallet(options[:wallet]) +cli.decrypt +cli.update(options[:password], options[:text_editor], search, values) diff --git a/bin/mpw-wallet b/bin/mpw-wallet new file mode 100644 index 0000000..6518283 --- /dev/null +++ b/bin/mpw-wallet @@ -0,0 +1,90 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'optparse' +require 'mpw/config' +require 'mpw/cli' + +# --------------------------------------------------------- # +# Options +# --------------------------------------------------------- # + +options = {} +options[:delete] = false + +OptionParser.new do |opts| + opts.banner = "#{I18n.t('option.usage')}: mpw wallet [options]" + + opts.on('-a', '--add-gpg-key NAME', I18n.t('option.add_gpg_key')) do |gpg_key| + options[:gpg_key] = gpg_key + end + + opts.on('-c', '--config PATH', I18n.t('option.config')) do |config| + options[:config] = config + end + + opts.on('-d', '--delete-gpg-key NAME', I18n.t('option.delete_gpg_key')) do |gpg_key| + options[:gpg_key] = gpg_key + options[:delete] = true + end + + opts.on('-h', '--help', I18n.t('option.help')) do + puts opts + exit 0 + end + + opts.on('-l', '--list', I18n.t('option.list')) do + options[:list] = true + end + + opts.on('-L', '--list-keys', I18n.t('option.list_keys')) do + options[:list_keys] = true + end + + opts.on('-p', '--path PATH', I18n.t('option.path')) do |path| + options[:path] = path + end + + opts.on('-P', '--default-path', I18n.t('option.default_path')) do + options[:path] = 'default' + end + + opts.on('-w', '--wallet NAME', I18n.t('option.wallet')) do |wallet| + options[:wallet] = wallet + end +end.parse! + +config = MPW::Config.new(options[:config]) +cli = MPW::Cli.new(config) + +cli.load_config + +if options.key?(:path) + cli.get_wallet(options[:wallet]) + cli.set_wallet_path(options[:path]) +elsif options.key?(:list_keys) || options.key?(:gpg_key) + cli.get_wallet(options[:wallet]) + cli.decrypt + + if options.key?(:list_keys) + cli.list_keys + elsif options.key?(:gpg_key) + options[:delete] ? cli.delete_key(options[:gpg_key]) : cli.add_key(options[:gpg_key]) + end +else + cli.list_wallet +end diff --git a/i18n/cli/en.yml b/i18n/cli/en.yml deleted file mode 100644 index 6ba6c08..0000000 --- a/i18n/cli/en.yml +++ /dev/null @@ -1,149 +0,0 @@ ---- -en: - error: - client: - no_authorized: "You aren't authorized." - config: - write: "Can't write the config file!" - check: "Checkconfig failed!" - key_bad_format: "The key string isn't in good format!" - no_key_public: "You haven't the public key of %{key}!" - genkey_gpg: - exception: "Can't create the GPG key!" - name: "You must define a name for your GPG key!" - password: "You must define a password for your GPG key!" - delete: - id_no_exist: "Can't delete the item %{id}, it doesn't exist!" - export: - unknown_type: "The data type %{type} is unknown!" - write: "Can't export, unable to write in %{file}!" - gpg_file: - decrypt: "Can't decrypt file!" - encrypt: "Can't encrypt the GPG file!" - import: - bad_format: "Can't import, the file is badly formated!" - read: "Can't import, unable to read %{file}!" - update: - name_empty: "You must define a name!" - sync: - connection: "Connection fail!" - communication: "A communication problem with the server is appeared!" - download: "Can't download the file!" - not_authorized: "You haven't the access to remote file!" - upload: "Can't upload the file on the server!" - unknown: "An unknown error is occured!" - unknown_type: "The sync type is unknown" - - warning: - select: 'Your choice is not a valid element!' - - option: - usage: "Usage" - show: "Search and show the items" - show_all: "Show all items" - update: "Update an item" - remove: "Delete an item" - group: "Search the items with specified group" - add: "Add an item" - config: "Specify the configuration file to use" - setup: "Create a new configuration file" - protocol: "Select the items with the specified protocol" - export: "Export all items in a file" - type: "Data's type export file [csv|yaml]" - import: "Import item since a yaml or csv file" - force: "Force an action" - format: "Change the display items format by an alternative format" - generate_password: "Generate a random password (default 8 characters)" - help: "Show this help message" - - form: - select: "Select the item: " - add: - title: "Add a new item" - name: "Enter the name: " - group: "Enter the group (optional): " - server: "Enter the hostname or ip: " - protocol: "Enter the protocol of the connection (ssh, http, other): " - login: "Enter the login connection: " - password: "Enter the the password: " - port: "Enter the connection port (optional): " - comment: "Enter a comment (optional): " - valid: "Item has been added!" - delete: - ask: "Are you sure you want to remove the item %{id} ?" - valid: "The item %{id} has been removed!" - not_valid: "The item %{id} hasn't been removed, because it doesn't exist!" - import: - ask: "Are you sure you want to import this file %{file} ?" - valid: "The import is succesfull!" - not_valid: "No data to import!" - setup: - title: "Setup a new config file" - lang: "Choose your language (en, fr, ...): " - gpg_key: "Enter the GPG key: " - share_gpg_keys: "Enter the GPG keys with who you want to share the passwords: " - gpg_file: "Enter the path to encrypt file [default=%{home}/db/default.gpg]: " - timeout: "Enter the timeout (in seconde) to GPG password [default=60]: " - sync_type: "Synchronization type (mpw, ssh, ftp, or nil): " - sync_host: "Synchronization server: " - sync_port: "Port of the synchronization server: " - sync_user: "Username for the synchronization: " - sync_pwd: "Password for the synchronization: " - sync_path: "File path for the synchronization : " - valid: "The config file has been created!" - setup_gpg_key: - title: "Setup a GPG key" - ask: "Do you want create your GPG key ? (Y/n)" - no_create: "You must create manually your GPG key or relaunch the software." - name: "Your name and lastname: " - password: "A password for the GPG key: " - confirm_password: "Confirm your password: " - error_password: "Your passwords aren't identical!" - length: "Size of the GPG key [default=2048]: " - expire: "Expire time of the GPG key [default=0 (unlimited)]: " - wait: "Please waiting during the GPG key generate, this process can take few minutes." - valid: "Your GPG key has been created ;-)" - update: - title: "Update an item" - name: "Enter the name [%{name}]: " - group: "Enter the group [%{group}]: " - server: "Enter the hostname or ip [%{server}]: " - protocol: "Enter the protocol of the connection [%{protocol}]: " - login: "Enter the login connection [%{login}]: " - password: "Enter the the password: " - port: "Enter the connection port [%{port}]: " - comment: "Enter a comment [%{comment}]: " - valid: "Item has been updated!" - export: - valid: "The export in %{file} is succesfull!" - - display: - comment: "Comment" - error: "ERROR" - gpg_password: "Password GPG: " - group: "Group" - login: "Login" - name: "Name" - nothing: "Nothing result!" - password: "Password" - port: "Port" - protocol: "Protocol" - server: "Server" - warning: "Warning" - - ssh: - option: - usage: "Usage" - login: "Change the login" - server: "Change the host or the ip" - port: "Change the port" - help: "Show this help message" - display: - connect: "Connection to:" - nothing: "Nothing result!" - - formats: - default: ! '%Y-%m-%d' - long: ! '%B %d, %Y' - short: ! '%b %d' - custom: ! '%A, %M %B, %Y @ %l:%M%P' diff --git a/i18n/cli/fr.yml b/i18n/cli/fr.yml deleted file mode 100644 index 1410993..0000000 --- a/i18n/cli/fr.yml +++ /dev/null @@ -1,149 +0,0 @@ ---- -fr: - error: - client: - no_authorized: "Vous n'êtes pas autorisé!" - config: - write: "Impossible d'écrire le fichier de configuration!" - check: "Le fichier de configuration est invalide!" - key_bad_format: "La clé GPG est invalide!" - no_key_public: "Vous ne possédez pas la clé publique de %{key}!" - genkey_gpg: - exception: "La création de la clé GPG n'a pas pu aboutir!" - name: "Vous devez définir un nom pour votre clé GPG!" - password: "Vous devez définir un mot de passe pour votre clé GPG!" - delete: - id_no_exist: "Impossible de supprimer l'élément %{id}, car il n'existe pas!" - export: - unknown_type: "Le type de donnée %{type} est inconnu!" - write: "Impossible d'exporter les données dans le fichier %{file}!" - gpg_file: - decrypt: "Impossible de déchiffrer le fichier GPG!" - encrypt: "Impossible de chiffrer le fichier GPG!" - import: - bad_format: "Impossible d'importer le fichier car son format est incorrect!" - read: "Impossible d'importer le fichier %{file}, car il n'est pas lisible!" - update: - name_empty: "Vous devez définir un nom!" - sync: - connection: "La connexion n'a pu être établie!" - communication: "Un problème de communication avec le serveur est apparu!" - download: "Impossible de télécharger le fichier!" - not_authorized: "Vous n'avez pas les autorisations d'accès au fichier distant!" - upload: "Impossible d'envoyer le fichier sur le serveur!" - unknown: "Une erreur inconnue est survenue!" - unknown_type: "Le type de synchronisation est inconnu" - - warning: - select: "Votre choix n'est pas un élément valide!" - - option: - usage: "Utilisation" - show: "Recherche et affiche les éléments" - show_all: "Affiche tous les éléments" - update: "Met à jour un élément" - remove: "Supprime un élément" - group: "Recherche les éléments appartenant au groupe spécifié" - add: "Ajoute un élément" - config: "Spécifie le fichier de configuration à utiliser" - setup: "Création d'un nouveau fichier de configuration" - protocol: "Sélectionne les éléments ayant le protocole spécifié" - export: "Exporte tous les éléments dans un fichier" - type: "Format des données du fichier d'export [csv|yaml]" - import: "Importe des éléments depuis un fichier yaml ou csv" - force: "Force une action, l'action ne demandera pas de confirmation" - format: "Change le format d'affichage des éléments par un alternatif" - generate_password: "Génére un mot de passe aléatoire (défaut 8 caractères)" - help: "Affiche ce message d'aide" - - form: - select: "Sélectionner l'élément: " - add: - title: "Ajout d'un nouvel élément" - name: "Entrez le nom: " - group: "Entrez le groupe (optionnel): " - server: "Entrez le nom de domaine ou l'ip: " - protocol: "Entrez le protocole de connexion (ssh, http, other): " - login: "Entrez l'identifiant de connexion: " - password: "Entrez le mot de passe: " - port: "Entrez le port de connexion (optionnel): " - comment: "Entrez un commentaire (optionnel): " - valid: "L'élément a bien été ajouté!" - delete: - ask: "Êtes vous sûre de vouloir supprimer l'élément %{id} ?" - valid: "L'élément %{id} a bien été supprimé!" - not_valid: "L'élément %{id} n'a pu être supprimé, car il n'existe pas!" - import: - ask: "Êtes vous sûre de vouloir importer le fichier %{file} ?" - valid: "L'import est un succès!" - not_valid: "Aucune donnée à importer!" - setup: - title: "Création d'un nouveau fichier de configuration" - lang: "Choisissez votre langue (en, fr, ...) [défaut=%{lang}]: " - gpg_key: "Entrez la clé GPG: " - share_gpg_keys: "Entrez les clés GPG avec qui vous voulez partager les mots de passe: " - gpg_file: "Entrez le chemin du fichier qui sera chiffré [défaut=%{home}/db/default.gpg]: " - timeout: "Entrez le temps (en seconde) du mot de passe GPG [défaut=60]: " - sync_type: "Type de synchronisation (mpw, ssh, ftp, or nil): " - sync_host: "Serveur de synchronisation: " - sync_port: "Port du serveur de synchronisation: " - sync_user: "Utilisateur pour la synchronisation: " - sync_pwd: "Mot de passe pour la synchronisation: " - sync_path: "Chemin du fichier pour la synchronisation: " - valid: "Le fichier de configuration a bien été créé!" - setup_gpg_key: - title: "Configuration d'une clé GPG" - ask: "Voulez vous créer votre clé GPG ? (O/n)" - no_create: "Veuillez créer manuellement votre clé GPG ou relancer le logiciel." - name: "Votre nom et prénom: " - password: "Mot de passe de la clé GPG: " - confirm_password: "Retapez votre mot de passe: " - error_password: "Vos deux mots de passes ne sont pas identiques!" - length: "Taille de la clé GPG [défaut=2048]: " - expire: "Expiration de la clé GPG [défaut=0 (illimité)]: " - wait: "Veuillez patienter durant la génération de votre clé GPG, ce processus peut prendre quelques minutes." - valid: "Votre clé GPG a bien été créée ;-)" - update: - title: "Mis à jour d'un élément" - name: "Entrez le nom [%{name}]: " - group: "Entrez le groupe [%{group}]: " - server: "Entrez le nom de domaine ou l'ip du serveur [%{server}]: " - protocol: "Entrez le protocole de connexion [%{protocol}]: " - login: "Entrez votre identifiant de connexion [%{login}]: " - password: "Entrez le mot de passe: " - port: "Entrez un port de connexion [%{port}]: " - comment: "Entrez un commentaire [%{comment}]: " - valid: "L'élément a bien été mis à jour!" - export: - valid: "L'export dans %{file} est un succès!" - - display: - comment: "Commentaire" - error: "ERREUR" - gpg_password: "Mot de passe GPG: " - group: "Groupe" - login: "Identifiant" - name: "Nom" - nothing: "Aucun résultat!" - password: "Mot de passe" - port: "Port" - protocol: "Protocol" - server: "Serveur" - warning: "Warning" - - ssh: - option: - usage: "Utilisation" - login: "Change l'identifiant de connexion" - server: "Change le nom de domaine ou l'ip du serveur" - port: "Change le port de connexion" - help: "Affiche ce message d'aide" - display: - connect: "Connexion à :" - nothing: "Aucun résultat!" - - formats: - default: ! '%Y-%m-%d' - long: ! '%B %d, %Y' - short: ! '%b %d' - custom: ! '%A, %M %B, %Y @ %l:%M%P' diff --git a/i18n/en.yml b/i18n/en.yml new file mode 100644 index 0000000..9df7bda --- /dev/null +++ b/i18n/en.yml @@ -0,0 +1,195 @@ +--- +en: + error: + bad_class: "The object class isn't valid!" + config: + write: "Can't write the config file!" + load: "Checkconfig failed!" + key_bad_format: "The key string isn't in the right format!" + no_key_public: "You haven't entered the public key of %{key}!" + genkey_gpg: + exception: "Can't create the GPG key!" + name: "You must define a name for your GPG key!" + password: "You must define a password for your GPG key!" + empty: "The class is empty" + export: "Can't export, unable to write in %{file}!" + export_key: "Can't export the GPG key" + gpg_file: + decrypt: "Can't decrypt file!" + encrypt: "Can't encrypt the GPG file!" + mpw_file: + read_data: "Can't read the MPW file!" + write_data: "Can't write the MPW file!" + import: "Can't import, unable to read %{file}!" + update: + host_and_comment_empty: "You must define a host or a comment!" + + warning: + select: 'Your choice is not a valid item!' + + command: + add: "Add a new item" + config: "Manage the general config" + copy: "Copy a login, password or OTP code" + delete: "Delete an item" + export: "Export the data in plain text" + genpwd: "Generate a password" + import: "Import data from a file" + list: "Print the items" + update: "Update an item" + wallet: "Manage the wallet config" + + option: + add: "Add an item or key" + add_gpg_key: "Share the wallet with another GPG key" + alpha: "Use letter to create a password" + comment: "Specify a comment" + config: "Specify the configuration file to use" + clipboard: "Disable the clipboard feature" + default_path: "Move the wallet to the default directory" + default_wallet: "Specify the default wallet to use" + delete_gpg_key: "Delete wallet sharing with an other GPG key" + disable_alpha: "Don't use letters to create a password" + disable_numeric: "Don't use numbers to generate a password" + disable_pinmode: "Disable the pinentry mode" + disable_special_chars: "Don't use special char to create a password" + export: "Export a wallet in an yaml file" + file_export: "Specify the file to export data" + file_format: "Format of import file (default: mpw; available: %{formats})" + file_import: "Specify the file to import" + force: "Do not ask confirmation when deleting an item" + generate_password: "Create a random password (default 8 characters)" + gpg_exe: "Set the gpg binary path to use" + gpg_key: "Specify a GPG key (ex: user@example.com)" + group: "Search the items with specified group" + help: "Show this help message" + init: "Initialize mpw" + import: "Import item from an yaml file" + key: "Define the key name" + lang: "Set the software language" + length: "Size of the password" + list: "List the wallets" + list_keys: "List the GPG keys in wallet" + new_group: "Define a group for the item" + numeric: "Use number to create a password" + otp_code: "Set an otp key" + path: "Move the wallet in new specify directory" + pattern: "Given search pattern" + pinmode: "Enable pinentry mode (available with gpg >= 2.1)" + random_password: "Generate a random password" + setup: "Create a new configuration file" + setup_wallet: "Create a new configuration file for a wallet" + special_chars: "Use special char to create a password" + show: "Search and display the items" + show_all: "Listing all items" + text_editor: "Use text editor to edit the item" + usage: "Use" + url: "Set an url (ex: https://example.com/path)" + user: "Set an user" + wallet: "Specify a wallet to use" + wallet_dir: "Set the wallets folder" + + form: + select: + choice: "Select the item: " + error: "No item selected" + add_key: + valid: "Key has been added!" + add_item: + name: "Item name (mandatory)" + group: "Group name" + host: "Hostname or ip" + protocol: "Connection protocol (ssh, http, ...)" + login: "Connection ID" + password: "Password" + port: "Connection port" + comment: "A comment" + otp_key: "The OTP secret" + valid: "Item has been added!" + clipboard: + choice: "What do you want to copy ? [q = quit, p = password, l = login]: " + clean: "The clipboard has been cleaned." + login: "The login has been copied in clipboard." + password: "The password has been copied in clipboard for 30s!" + otp: "The OTP code has been copied %{time}s!" + url: "The URL has been copied in clipboard." + help: + name: "Help" + url: "Press <u> to copy URL" + login: "Press <l> to copy the login" + password: "Press <p> to copy the password" + otp_code: "Press <o> to copy the otp code" + quit: "Press <q> to quit" + delete_key: + valid: "Key has been deleted!" + delete_item: + ask: "Are you sure you want to remove this item ?" + valid: "The item has been removed!" + import: + ask: "Are you sure you want to import this file %{file} ?" + file_empty: "The import file is empty!" + file_not_exist: "The import file doesn't exist!" + format_unknown: "The import format '%{file_format} is unknown!" + valid: "The import is successful!" + not_valid: "No data to import!" + set_config: + valid: "The config file has been edited!" + set_wallet_path: + valid: "The wallet has been moved!" + setup_config: + title: "Setup a new config file" + lang: "Choose your language (en, fr, ...) [default=%{lang}]: " + gpg_key: "Enter the GPG key [ex: test@host.local]: " + gpg_exe: "Enter the executable GPG path (optional): " + wallet_dir: "Enter the wallets's folder path [default=%{home}/wallets]: " + valid: "The config file has been created!" + setup_gpg_key: + title: "Setup a GPG key" + ask: "Do you want to create your GPG key ? (Y/n)" + no_create: "You must to create manually your GPG key or relaunch the software." + name: "Your name and lastname: " + password: "A password for the GPG key: " + confirm_password: "Confirm your password: " + error_password: "Your passwords aren't identical!" + length: "Size of the GPG key [default=2048]: " + expire: "Expire time of the GPG key [default=0 (unlimited)]: " + wait: "Please wait until GPG key is created, this process can take a few minutes." + valid: "Your GPG key has been created ;-)" + update_item: + name: "Item name (mandatory)" + group: "Group name" + host: "Hostname or ip" + protocol: "Connection protocol (ssh, http, ...)" + login: "Login id" + password: "Password (leave empty if you don't want to update it)" + port: "Connection port" + comment: "A comment" + otp_key: "Secret OTP (leave empty if you don't want to update it" + valid: "Item has been updated!" + export: + valid: "The export in %{file} is successful!" + + display: + comment: "Comment" + config: "Configuration" + error: "ERROR" + keys: "GPG keys" + gpg_password: "GPG password: " + group: "Group" + login: "Login" + name: "Name" + no_group: "Without group" + nothing: "No matches!" + otp_code: "OTP code" + password: "Password" + port: "Port" + protocol: "Protocol" + server: "Server" + wallets: "Wallets" + warning: "Warning" + + formats: + default: ! '%Y-%m-%d' + long: ! '%B %d, %Y' + short: ! '%b %d' + custom: ! '%A, %M %B, %Y @ %l:%M%P' diff --git a/i18n/fr.yml b/i18n/fr.yml new file mode 100644 index 0000000..04583ad --- /dev/null +++ b/i18n/fr.yml @@ -0,0 +1,195 @@ +--- +fr: + error: + bad_class: "La classe de l'objet n'est pas celle attendue!" + config: + write: "Impossible d'écrire le fichier de configuration!" + load: "Le fichier de configuration est invalide!" + key_bad_format: "La clé GPG est invalide!" + no_key_public: "Vous ne possédez pas la clé publique de %{key}!" + genkey_gpg: + exception: "La création de la clé GPG n'a pas pu aboutir!" + name: "Vous devez définir un nom pour votre clé GPG!" + password: "Vous devez définir un mot de passe pour votre clé GPG!" + empty: "La classe est vide" + export: "Impossible d'exporter les données dans le fichier %{file}!" + export_key: "Impossible d'exporter la clé GPG" + gpg_file: + decrypt: "Impossible de déchiffrer le fichier GPG!" + encrypt: "Impossible de chiffrer le fichier GPG!" + mpw_file: + read_data: "Impossible de lire le fichier MPW!" + write_data: "Impossible d'écrire le fichier MPW!" + import: "Impossible d'importer le fichier %{file}, car il n'est pas lisible!" + update: + host_and_comment_empty: "Vous devez définir un host ou un commentaire!" + + warning: + select: "Votre choix n'est pas un élément valide!" + + command: + add: "Ajoute un nouvel élément" + config: "Gère la configuration générale" + copy: "Copie un identifiant, un mot de passe ou un code OTP" + delete: "Supprimer un élément d'un portefeuille" + export: "Exporte les données" + genpwd: "Génére un mot de passe" + import: "Importe des données" + list: "Liste les éléments d'un portefeuille" + update: "Met à jour un élément" + wallet: "Gére la configuration d'un portefeuille" + + option: + add: "Ajoute un élément ou une clé" + add_gpg_key: "Partage le portefeuille avec une autre clé GPG" + alpha: "Utilise des lettres dans la génération d'un mot de passe" + config: "Spécifie le fichier de configuration à utiliser" + comment: "Spécifie un commentaire" + clipboard: "Désactive la fonction presse papier" + default_path: "Déplace le portefeuille dans le dossier par défaut" + default_wallet: "Spécifie le porte-feuille à utiliser par défaut" + delete_gpg_key: "Supprime le partage le portefeuille avec une autre clé GPG" + disable_alpha: "Désactive l'utilisation des lettres dans la génération d'un mot de passe" + disable_numeric: "Désactive l'utilisation des chiffre dans la génération d'un mot de passe" + disable_pinmode: "Désactive le mode pinentry" + disable_special_chars: "Désactive l'utilisation des charactères speciaux dans la génération d'un mot de passe" + export: "Exporte un portefeuille dans un fichier yaml" + file_export: "Spécifie le fichier où exporter les données" + file_format: "Format du fichier d'import (défault: mpw; disponible: %{formats})" + file_import: "Spécifie le fichier à importer" + force: "Ne demande pas de confirmation pour la suppression d'un élément" + generate_password: "Génére un mot de passe aléatoire (défaut 8 caractères)" + gpg_exe: "Spécifie le chemin du binaire gpg à utiliser" + gpg_key: "Spécifie une clé GPG (ex: user@example.com)" + group: "Recherche les éléments appartenant au groupe spécifié" + help: "Affiche ce message d'aide" + import: "Importe des éléments depuis un fichier yaml" + init: "Initialise mpw" + key: "Spécifie le nom d'une clé" + lang: "Spécifie la langue du logiciel (ex: fr)" + length: "Taille du mot de passe" + list: "Liste les portefeuilles" + list_keys: "Liste les clés GPG dans le portefeuille" + new_group: "Spécifie le groupe de l'item" + numeric: "Utilise des chiffre dans la génération d'un mot de passe" + otp_code: "Spécifie un code OTP" + path: "Déplace le portefeuille dans un nouveau dossier" + pattern: "Motif de donnée à chercher" + pinmode: "Active le mode pinentry (valable avec gpg >= 2.1)" + random_password: "Génére un mot de passe aléatoire" + setup: "Création d'un nouveau fichier de configuration" + setup_wallet: "Création d'un nouveau fichier de configuration pour un portefeuille" + special_chars: "Utilise des charactères speciaux dans la génération d'un mot de passe" + show: "Recherche et affiche les éléments" + show_all: "Liste tous les éléments" + text_editor: "Active l'édition avec un éditeur de texte" + usage: "Utilisation" + url: "Spécifie l'url (ex: http://example.com/path)" + user: "Spécifie un utilisateur" + wallet: "Spécifie le portefeuille à utiliser" + wallet_dir: "Spécifie le répertoire des portefeuilles" + + form: + select: + choice: "Sélectionner l'élément: " + error: "Aucun élément sélectionné" + add_key: + valid: "La clé a bien été ajoutée!" + add_item: + name: "Le nom de l'élément (obligatoire)" + group: "Le nom du groupe" + host: "Le nom de domaine ou l'ip" + protocol: "Le protocole de connexion (ssh, http, ...)" + login: "L'identifiant de connexion" + password: "Le mot de passe" + port: "Le port de connexion" + comment: "Un commentaire" + otp_key: "Le secret OTP" + valid: "L'élément a bien été ajouté!" + clipboard: + choice: "Que voulez-vous copier ? : " + clean: "Le presse papier a été nettoyé." + login: "L'identifiant a été copié dans le presse papier" + password: "Le mot de passe a été copié dans le presse papier pour 30s!" + otp: "Le code OTP a été copié dans le presse papier il est valable %{time}s!" + url: "L'URL a été copié dans le presse papier" + help: + name: "Aide" + url: "Pressez <u> pour copier l'URL" + login: "Pressez <l> pour copier l'identifiant" + password: "Pressez <p> pour copier le mot de passe" + otp_code: "Pressez <o> pour copier le code OTP" + quit: "Pressez <q> pour quitter" + delete_key: + valid: "La clé a bien été supprimée!" + delete_item: + ask: "Êtes vous sûre de vouloir supprimer l'élément ?" + valid: "L'élément a bien été supprimé!" + import: + ask: "Êtes vous sûre de vouloir importer le fichier %{file} ?" + file_empty: "Le fichier d'import est vide!" + file_not_exist: "Le fichier d'import n'existe pas" + format_unknown: "Le format d'import '%{file_format}' est inconnu!" + valid: "L'import est un succès!" + not_valid: "Aucune donnée à importer!" + set_config: + valid: "Le fichier de configuration a bien été modifié!" + set_wallet_path: + valid: "Le portefeuille a bien été déplacé!" + setup_config: + title: "Création d'un nouveau fichier de configuration" + lang: "Choisissez votre langue (en, fr, ...) [défaut=%{lang}]: " + gpg_key: "Entrez la clé GPG [ex: test@host.local]: " + gpg_exe: "Entrez le chemin de l'exécutable GPG (optionnel): " + wallet_dir: "Entrez le chemin du répertoire qui contiendra les porte-feuilles de mot de passe [défaut=%{home}/wallets]: " + valid: "Le fichier de configuration a bien été créé!" + setup_gpg_key: + title: "Configuration d'une nouvelle clé GPG" + ask: "Voulez vous créer votre clé GPG ? (O/n)" + no_create: "Veuillez créer manuellement votre clé GPG ou relancer le logiciel." + name: "Votre nom et prénom: " + password: "Mot de passe de la clé GPG: " + confirm_password: "Retapez votre mot de passe: " + error_password: "Vos deux mots de passes ne sont pas identiques!" + length: "Taille de la clé GPG [défaut=2048]: " + expire: "Expiration de la clé GPG [défaut=0 (illimité)]: " + wait: "Veuillez patienter durant la génération de votre clé GPG, ce processus peut prendre quelques minutes." + valid: "Votre clé GPG a bien été créée ;-)" + update_item: + name: "Le nom de l'élément (obligatoire)" + group: "Le nom du groupe" + host: "Le nom de domaine ou l'ip" + protocol: "Le protocole de connexion (ssh, http, ...)" + login: "L'identifiant de connexion" + password: "Le mot de passe (laissez vide si vous ne voulez pas le changer)" + port: "Le port de connexion" + comment: "Un commentaire" + otp_key: "Le secret OTP (laissez vide si vous ne voulez pas le changer)" + valid: "L'élément a bien été mis à jour!" + export: + valid: "L'export dans %{file} est un succès!" + + display: + comment: "Commentaire" + config: "Configuration" + error: "ERREUR" + keys: "Clés GPG" + gpg_password: "Mot de passe GPG: " + group: "Groupe" + login: "Identifiant" + name: "Nom" + no_group: "Sans groupe" + nothing: "Aucun résultat!" + otp_code: "Code OTP" + password: "Mot de passe" + port: "Port" + protocol: "Protocol" + server: "Serveur" + wallets: "Porte-feuilles" + warning: "Warning" + + formats: + default: ! '%Y-%m-%d' + long: ! '%B %d, %Y' + short: ! '%b %d' + custom: ! '%A, %M %B, %Y @ %l:%M%P' diff --git a/i18n/server/en.yml b/i18n/server/en.yml deleted file mode 100644 index df88dfe..0000000 --- a/i18n/server/en.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -en: - option: - usage: "Usage" - config: "Specifie the configuration file" - checkconfig: "Check the configuration" - setup: "Setup a new configuration file" - help: "Show this message help" - checkconfig: - fail: "Checkconfig failed:!" - empty: "ERROR: an importe option is missing!" - datadir: "ERROR: le data directory doesn't exist!" - form: - setup: - title: "Serveur configuration" - host: "IP listen: " - port: "Port listen: " - data_dir: "Data directory: " - timeout: "Timeout to second: " - log_file: "Log file path: " - not_valid: "ERROR: Impossible to write the configuration file!" - formats: - default: ! '%Y-%m-%d' - long: ! '%B %d, %Y' - short: ! '%b %d' - custom: ! '%A, %M %B, %Y @ %l:%M%P' diff --git a/i18n/server/fr.yml b/i18n/server/fr.yml deleted file mode 100644 index 667757c..0000000 --- a/i18n/server/fr.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -fr: - option: - usage: "Utilisation" - config: "Spécifie le fichier de configuration" - checkconfig: "Vérifie le fichier de configuration" - setup: "Permet de générer un nouveau fichier de configuration" - help: "Affiche ce message d'aide" - checkconfig: - fail: "Le fichier de configuration est invalide!" - empty: "ERREUR: Une option importante est manquante!" - datadir: "ERREUR: Le répertoire des données n'existe pas!" - form: - setup: - title: "Configuration du serveur" - host: "IP d'écoute: " - port: "Port d'écoute: " - data_dir: "Répertoire des données: " - log_file: "Chemin du ficier de log: " - timeout: "Timeout en seconde: " - not_valid: "ERREUR: Impossible d'écire le fichier de configuration!" - formats: - default: ! '%Y-%m-%d' - long: ! '%B %d, %Y' - short: ! '%b %d' - custom: ! '%A, %M %B, %Y @ %l:%M%P' diff --git a/lib/mpw/cli.rb b/lib/mpw/cli.rb new file mode 100644 index 0000000..e64d27c --- /dev/null +++ b/lib/mpw/cli.rb @@ -0,0 +1,623 @@ +# +# Copyright:: 2013, Adrien Waksberg +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'readline' +require 'locale' +require 'i18n' +require 'colorize' +require 'highline/import' +require 'clipboard' +require 'tmpdir' +require 'mpw/item' +require 'mpw/mpw' + +module MPW + class Cli + # @param config [Config] + def initialize(config) + @config = config + end + + # Change a parameter int the config after init + # @param options [Hash] param to change + def set_config(options) + @config.setup(options) + + puts I18n.t('form.set_config.valid').to_s.green + rescue => e + puts "#{I18n.t('display.error')} #15: #{e}".red + exit 2 + end + + # Change the wallet path + # @param path [String] new path + def set_wallet_path(path) + @config.set_wallet_path(path, @wallet) + + puts I18n.t('form.set_wallet_path.valid').to_s.green + rescue => e + puts "#{I18n.t('display.error')} #19: #{e}".red + exit 2 + end + + # Create a new config file + # @param options [Hash] + def setup(options) + options[:lang] = options[:lang] || Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1] + + I18n.locale = options[:lang].to_sym + + @config.setup(options) + + load_config + + puts I18n.t('form.setup_config.valid').to_s.green + rescue => e + puts "#{I18n.t('display.error')} #8: #{e}".red + exit 2 + end + + # Setup a new GPG key + # @param gpg_key [String] gpg key name + def setup_gpg_key(gpg_key) + return if @config.check_gpg_key? + + password = ask(I18n.t('form.setup_gpg_key.password')) { |q| q.echo = false } + confirm = ask(I18n.t('form.setup_gpg_key.confirm_password')) { |q| q.echo = false } + + raise I18n.t('form.setup_gpg_key.error_password') if password != confirm + + @password = password.to_s + + puts I18n.t('form.setup_gpg_key.wait') + + @config.setup_gpg_key(@password, gpg_key) + + puts I18n.t('form.setup_gpg_key.valid').to_s.green + rescue => e + puts "#{I18n.t('display.error')} #8: #{e}".red + exit 2 + end + + # List gpg keys in wallet + def list_keys + table_list('keys', @mpw.list_keys) + end + + # List config + def list_config + config = { + 'lang' => @config.lang, + 'gpg_key' => @config.gpg_key, + 'default_wallet' => @config.default_wallet, + 'wallet_dir' => @config.wallet_dir, + 'pinmode' => @config.pinmode, + 'gpg_exe' => @config.gpg_exe + } + + @config.wallet_paths.each { |k, v| config["path_wallet_#{k}"] = "#{v}/#{k}.mpw" } + @config.password.each { |k, v| config["password_#{k}"] = v } + + table_list('config', config) + end + + # Load config + def load_config + @config.load_config + rescue => e + puts "#{I18n.t('display.error')} #10: #{e}".red + exit 2 + end + + # Request the GPG password and decrypt the file + def decrypt + if defined?(@mpw) + @mpw.read_data + else + begin + @mpw = MPW.new(@config.gpg_key, @wallet_file, nil, @config.gpg_exe, @config.pinmode) + + @mpw.read_data + rescue + @password = ask(I18n.t('display.gpg_password')) { |q| q.echo = false } + @mpw = MPW.new(@config.gpg_key, @wallet_file, @password, @config.gpg_exe, @config.pinmode) + + @mpw.read_data + end + end + rescue => e + puts "#{I18n.t('display.error')} #11: #{e}".red + exit 2 + end + + # Format list on a table + # @param title [String] name of table + # @param list an array or hash + def table_list(title, list) + length = { k: 0, v: 0 } + + if list.is_a?(Array) + i = 0 + list = list.map do |item| + i += 1 + [i, item] + end.to_h + end + + list.each do |k, v| + length[:k] = k.to_s.length if length[:k] < k.to_s.length + length[:v] = v.to_s.length if length[:v] < v.to_s.length + end + + puts "\n#{I18n.t("display.#{title}")}".red + print ' ' + (length[:k] + length[:v] + 5).times { print '=' } + print "\n" + + list.each do |k, v| + print " #{k}".cyan + (length[:k] - k.to_s.length + 1).times { print ' ' } + puts "| #{v}" + end + + print "\n" + end + + # Format items on a table + # @param items [Array] + def table_items(items = []) + group = '.' + i = 1 + length_total = 10 + data = { id: { length: 3, color: 'cyan' }, + host: { length: 9, color: 'yellow' }, + user: { length: 7, color: 'green' }, + otp: { length: 4, color: 'white' }, + comment: { length: 14, color: 'magenta' } } + + items.each do |item| + data.each do |k, v| + case k + when :id, :otp + next + when :host + v[:length] = item.url.length + 3 if item.url.length >= v[:length] + else + v[:length] = item.send(k.to_s).to_s.length + 3 if item.send(k.to_s).to_s.length >= v[:length] + end + end + end + data[:id][:length] = items.length.to_s.length + 2 if items.length.to_s.length > data[:id][:length] + + data.each_value { |v| length_total += v[:length] } + items.sort! { |a, b| a.group.to_s.downcase <=> b.group.to_s.downcase } + + items.each do |item| + if group != item.group + group = item.group + + if group.to_s.empty? + puts "\n#{I18n.t('display.no_group')}".red + else + puts "\n#{group}".red + end + + print ' ' + length_total.times { print '=' } + print "\n " + data.each do |k, v| + case k + when :id + print ' ID' + when :otp + print '| OTP' + else + print "| #{k.to_s.capitalize}" + end + + (v[:length] - k.to_s.length).times { print ' ' } + end + print "\n " + length_total.times { print '=' } + print "\n" + end + + print " #{i}".send(data[:id][:color]) + (data[:id][:length] - i.to_s.length).times { print ' ' } + data.each do |k, v| + next if k == :id + + print '| ' + + case k + when :otp + item.otp ? (print ' X ') : 4.times { print ' ' } + + when :host + print "#{item.protocol}://".light_black if item.protocol + print item.host.send(v[:color]) + print ":#{item.port}".light_black if item.port + (v[:length] - item.url.to_s.length).times { print ' ' } + + else + print item.send(k.to_s).to_s.send(v[:color]) + (v[:length] - item.send(k.to_s).to_s.length).times { print ' ' } + end + end + print "\n" + + i += 1 + end + + print "\n" + end + + # Display the query's result + # @param options [Hash] the options to search + def list(**options) + result = @mpw.list(options) + + if result.empty? + puts I18n.t('display.nothing') + else + table_items(result) + end + end + + # Get an item when multiple choice + # @param items [Array] list of items + # @return [Item] an item + def get_item(items) + return items[0] if items.length == 1 + + items.sort! { |a, b| a.group.to_s.downcase <=> b.group.to_s.downcase } + choice = ask(I18n.t('form.select.choice')).to_i + + raise I18n.t('form.select.error') unless choice >= 1 && choice <= items.length + + items[choice - 1] + end + + # Print help message for clipboard mode + # @param item [Item] + def clipboard_help(item) + puts "----- #{I18n.t('form.clipboard.help.name')} -----".cyan + puts I18n.t('form.clipboard.help.url') + puts I18n.t('form.clipboard.help.login') + puts I18n.t('form.clipboard.help.password') + puts I18n.t('form.clipboard.help.otp_code') if item.otp + puts I18n.t('form.clipboard.help.quit') + end + + # Copy in clipboard the login and password + # @param item [Item] + # @param clipboard [Boolean] enable clipboard + def clipboard(item, clipboard = true) + # Security: force quit after 90s + Thread.new do + sleep 90 + exit + end + + Kernel.loop do + choice = ask(I18n.t('form.clipboard.choice')).to_s + + case choice + when 'q', 'quit' + break + + when 'u', 'url' + if clipboard + Clipboard.copy(item.url) + puts I18n.t('form.clipboard.url').green + else + puts item.url + end + + when 'l', 'login' + if clipboard + Clipboard.copy(item.user) + puts I18n.t('form.clipboard.login').green + else + puts item.user + end + + when 'p', 'password' + if clipboard + Clipboard.copy(@mpw.get_password(item.id)) + puts I18n.t('form.clipboard.password').yellow + + Thread.new do + sleep 30 + + Clipboard.clear + end + else + puts @mpw.get_password(item.id) + end + + when 'o', 'otp' + if !item.otp + clipboard_help(item) + next + elsif clipboard + Clipboard.copy(@mpw.get_otp_code(item.id)) + else + puts @mpw.get_otp_code(item.id) + end + puts I18n.t('form.clipboard.otp', time: @mpw.get_otp_remaining_time).yellow + + else + clipboard_help(item) + end + end + + Clipboard.clear + rescue SystemExit, Interrupt + Clipboard.clear + end + + # List all wallets + def list_wallet + wallets = @config.wallet_paths.keys + + Dir.glob("#{@config.wallet_dir}/*.mpw").each do |f| + wallet = File.basename(f, '.mpw') + wallet += ' *'.green if wallet == @config.default_wallet + wallets << wallet + end + + table_list('wallets', wallets) + end + + # Display the wallet + # @param wallet [String] wallet name + def get_wallet(wallet = nil) + @wallet = + if wallet.to_s.empty? + wallets = Dir.glob("#{@config.wallet_dir}/*.mpw") + if wallets.length == 1 + File.basename(wallets[0], '.mpw') + elsif !@config.default_wallet.to_s.empty? + @config.default_wallet + else + 'default' + end + else + wallet + end + + @wallet_file = + if @config.wallet_paths.key?(@wallet) + "#{@config.wallet_paths[@wallet]}/#{@wallet}.mpw" + else + "#{@config.wallet_dir}/#{@wallet}.mpw" + end + end + + # Add a new public key + # @param key [String] key name or key file to add + def add_key(key) + @mpw.add_key(key) + @mpw.write_data + + puts I18n.t('form.add_key.valid').to_s.green + rescue => e + puts "#{I18n.t('display.error')} #13: #{e}".red + end + + # Add new public key + # @param key [String] key name to delete + def delete_key(key) + @mpw.delete_key(key) + @mpw.write_data + + puts I18n.t('form.delete_key.valid').to_s.green + rescue => e + puts "#{I18n.t('display.error')} #15: #{e}".red + end + + # Text editor interface + # @param template_name [String] template name + # @param item [Item] the item to edit + # @param password [Boolean] disable field password + # @return [Hash] the values for an item + def text_editor(template_name, password = false, item = nil, **options) + editor = ENV['EDITOR'] || 'nano' + opts = {} + template_file = "#{File.expand_path('../../../templates', __FILE__)}/#{template_name}.erb" + template = ERB.new(IO.read(template_file)) + + Dir.mktmpdir do |dir| + tmp_file = "#{dir}/#{template_name}.yml" + + File.open(tmp_file, 'w') do |f| + f << template.result(binding) + end + + system("#{editor} #{tmp_file}") + + opts = YAML.load_file(tmp_file) + end + + opts.delete_if { |_, v| v.to_s.empty? } + + opts.each do |k, v| + options[k.to_sym] = v + end + + options + end + + # Form to add a new item + # @param password [Boolean] generate a random password + # @param text_editor [Boolean] enable text editor mode + # @param values [Hash] multiples value to set the item + def add(password = false, text_editor = false, **values) + options = text_editor('add_form', password, nil, values) if text_editor + item = Item.new(options) + options[:password] = MPW.password(@config.password) if password + + @mpw.add(item) + @mpw.set_password(item.id, options[:password]) if options.key?(:password) + @mpw.set_otp_key(item.id, options[:otp_key]) if options.key?(:otp_key) + @mpw.write_data + + puts I18n.t('form.add_item.valid').to_s.green + rescue => e + puts "#{I18n.t('display.error')} #13: #{e}".red + end + + # Update an item + # @param password [Boolean] generate a random password + # @param text_editor [Boolean] enable text editor mode + # @param options [Hash] the options to search + # @param values [Hash] multiples value to set the item + def update(password = false, text_editor = false, options = {}, **values) + items = @mpw.list(options) + + if items.empty? + puts I18n.t('display.nothing') + else + table_items(items) if items.length > 1 + + item = get_item(items) + values = text_editor('update_form', password, item, values) if text_editor + values[:password] = MPW.password(@config.password) if password + + item.update(values) + @mpw.set_password(item.id, values[:password]) if values.key?(:password) + @mpw.set_otp_key(item.id, values[:otp_key]) if values.key?(:otp_key) + @mpw.write_data + + puts I18n.t('form.update_item.valid').to_s.green + end + rescue => e + puts "#{I18n.t('display.error')} #14: #{e}".red + end + + # Remove an item + # @param options [Hash] the options to search + def delete(**options) + items = @mpw.list(options) + + if items.empty? + puts I18n.t('display.nothing') + else + table_items(items) + + item = get_item(items) + confirm = ask("#{I18n.t('form.delete_item.ask')} (y/N) ").to_s + + return unless confirm =~ /^(y|yes|YES|Yes|Y)$/ + + item.delete + @mpw.write_data + + puts I18n.t('form.delete_item.valid').to_s.green + end + rescue => e + puts "#{I18n.t('display.error')} #16: #{e}".red + end + + # Copy a password, otp, login + # @param clipboard [Boolean] enable clipboard + # @param options [Hash] the options to search + def copy(clipboard = true, **options) + items = @mpw.list(options) + + if items.empty? + puts I18n.t('display.nothing') + else + table_items(items) + + item = get_item(items) + clipboard(item, clipboard) + end + rescue => e + puts "#{I18n.t('display.error')} #14: #{e}".red + end + + # Export the items in an yaml file + # @param file [String] the path of destination file + # @param options [Hash] options to search + def export(file, options) + file = 'export-mpw.yml' if file.to_s.empty? + items = @mpw.list(options) + data = {} + + items.each do |item| + data.merge!( + item.id => { + 'comment' => item.comment, + 'created' => item.created, + 'group' => item.group, + 'last_edit' => item.last_edit, + 'otp_key' => @mpw.get_otp_key(item.id), + 'password' => @mpw.get_password(item.id), + 'url' => item.url, + 'user' => item.user + } + ) + end + + File.open(file, 'w') { |f| f << data.to_yaml } + + puts I18n.t('form.export.valid', file: file).to_s.green + rescue => e + puts "#{I18n.t('display.error')} #17: #{e}".red + end + + # Import items from an yaml file + # @param file [String] path of import file + # @param format [String] the software import file format + def import(file, format = 'mpw') + raise I18n.t('form.import.file_empty') if file.to_s.empty? + raise I18n.t('form.import.file_not_exist') unless File.exist?(file) + + begin + require "mpw/import/#{format}" + rescue LoadError + raise I18n.t('form.import.format_unknown', file_format: format) + end + + Import.send(format, file).each_value do |row| + item = Item.new( + comment: row['comment'], + group: row['group'], + url: row['url'], + user: row['user'] + ) + + next if item.empty? + + @mpw.add(item) + @mpw.set_password(item.id, row['password']) unless row['password'].to_s.empty? + @mpw.set_otp_key(item.id, row['otp_key']) unless row['otp_key'].to_s.empty? + end + + @mpw.write_data + + puts I18n.t('form.import.valid').to_s.green + rescue => e + puts "#{I18n.t('display.error')} #18: #{e}".red + end + end +end diff --git a/lib/mpw/config.rb b/lib/mpw/config.rb index d2b02a9..f3974b5 100644 --- a/lib/mpw/config.rb +++ b/lib/mpw/config.rb @@ -1,231 +1,190 @@ -#!/usr/bin/ruby -# author: nishiki -# mail: nishiki@yaegashi.fr +# +# Copyright:: 2013, Adrien Waksberg +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. -require 'rubygems' require 'gpgme' require 'yaml' require 'i18n' - +require 'fileutils' + module MPW - class Config - - attr_accessor :error_msg - - attr_accessor :key - attr_accessor :share_keys - attr_accessor :lang - attr_accessor :file_gpg - attr_accessor :last_update - attr_accessor :sync_type - attr_accessor :sync_host - attr_accessor :sync_port - attr_accessor :sync_user - attr_accessor :sync_pwd - attr_accessor :sync_path - attr_accessor :last_sync - attr_accessor :dir_config - - # Constructor - # @args: file_config -> the specify config file - def initialize(file_config=nil) - @error_msg = nil + class Config + attr_accessor :error_msg - if /darwin/ =~ RUBY_PLATFORM - @dir_config = "#{Dir.home}/Library/Preferences/mpw" - elsif /cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM - @dir_config = "#{Dir.home}/AppData/Local/mpw" - else - @dir_config = "#{Dir.home}/.config/mpw" - end - - @file_config = "#{@dir_config}/conf/default.cfg" - if not file_config.nil? and not file_config.empty? - @file_config = file_config - end - end - - # Create a new config file - # @args: key -> the gpg key to encrypt - # share_keys -> multiple keys to share the password with other people - # lang -> the software language - # file_gpg -> the file who is encrypted - # sync_type -> the type to synchronization - # sync_host -> the server host for synchronization - # sync_port -> the server port for synchronization - # sync_user -> the user for synchronization - # sync_pwd -> the password for synchronization - # sync_suffix -> the suffix file (optionnal) - # @rtrn: true if le config file is create - def setup(key, share_keys, lang, file_gpg, sync_type, sync_host, sync_port, sync_user, sync_pwd, sync_path) - - if not key =~ /[a-zA-Z0-9.-_]+\@[a-zA-Z0-9]+\.[a-zA-Z]+/ - @error_msg = I18n.t('error.config.key_bad_format') - return false - end + attr_accessor :gpg_key + attr_accessor :lang + attr_accessor :config_dir + attr_accessor :default_wallet + attr_accessor :wallet_dir + attr_accessor :wallet_paths + attr_accessor :gpg_exe + attr_accessor :password + attr_accessor :pinmode - if not check_public_gpg_key(share_keys) - return false - end - - if file_gpg.empty? - file_gpg = "#{@dir_config}/db/default.gpg" - end - - config = {'config' => {'key' => key, - 'share_keys' => share_keys, - 'lang' => lang, - 'file_gpg' => file_gpg, - 'sync_type' => sync_type, - 'sync_host' => sync_host, - 'sync_port' => sync_port, - 'sync_user' => sync_user, - 'sync_pwd' => sync_pwd, - 'sync_path' => sync_path, - 'last_sync' => 0 - } - } - - Dir.mkdir("#{@config_dir}/conf", 700) - Dir.mkdir("#{@config_dir}/db", 700) - File.open(@file_config, 'w') do |file| - file << config.to_yaml - end - - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.config.write')}\n#{e}" - return false - end + # @param config_file [String] path of config file + def initialize(config_file = nil) + @config_file = config_file + @config_dir = + if RUBY_PLATFORM =~ /darwin/ + "#{Dir.home}/Library/Preferences/mpw" + elsif RUBY_PLATFORM =~ /cygwin|mswin|mingw|bccwin|wince|emx/ + "#{Dir.home}/AppData/Local/mpw" + else + "#{Dir.home}/.config/mpw" + end - # Setup a new gpg key - # @args: password -> the GPG key password - # name -> the name of user - # length -> length of the GPG key - # expire -> the time of expire to GPG key - # @rtrn: true if the GPG key is create, else false - def setup_gpg_key(password, name, length = 2048, expire = 0) - if name.nil? or name.empty? - @error_msg = "#{I18n.t('error.config.genkey_gpg.name')}" - return false - elsif password.nil? or password.empty? - @error_msg = "#{I18n.t('error.config.genkey_gpg.password')}" - return false - end + @config_file = "#{@config_dir}/mpw.cfg" if @config_file.to_s.empty? + end - param = '' - param << '<GnupgKeyParms format="internal">' + "\n" - param << "Key-Type: DSA\n" - param << "Key-Length: #{length}\n" - param << "Subkey-Type: ELG-E\n" - param << "Subkey-Length: #{length}\n" - param << "Name-Real: #{name}\n" - param << "Name-Comment: #{name}\n" - param << "Name-Email: #{@key}\n" - param << "Expire-Date: #{expire}\n" - param << "Passphrase: #{password}\n" - param << "</GnupgKeyParms>\n" + # Create a new config file + # @param options [Hash] the value to set the config file + def setup(**options) + gpg_key = options[:gpg_key] || @gpg_key + lang = options[:lang] || @lang + wallet_dir = options[:wallet_dir] || @wallet_dir + default_wallet = options[:default_wallet] || @default_wallet + gpg_exe = options[:gpg_exe] || @gpg_exe + pinmode = options.key?(:pinmode) ? options[:pinmode] : @pinmode + password = { + numeric: true, + alpha: true, + special: false, + length: 16 + } - ctx = GPGME::Ctx.new - ctx.genkey(param, nil, nil) + %w[numeric special alpha length].each do |k| + if options.key?("pwd_#{k}".to_sym) + password[k.to_sym] = options["pwd_#{k}".to_sym] + elsif !@password.nil? && @password.key?(k.to_sym) + password[k.to_sym] = @password[k.to_sym] + end + end - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.config.genkey_gpg.exception')}\n#{e}" - return false - end - - # Check the config file - # @rtrn: true if the config file is correct - def checkconfig - config = YAML::load_file(@file_config) - @key = config['config']['key'] - @share_keys = config['config']['share_keys'] - @lang = config['config']['lang'] - @file_gpg = config['config']['file_gpg'] - @sync_type = config['config']['sync_type'] - @sync_host = config['config']['sync_host'] - @sync_port = config['config']['sync_port'] - @sync_user = config['config']['sync_user'] - @sync_pwd = config['config']['sync_pwd'] - @sync_path = config['config']['sync_path'] - @last_sync = config['config']['last_sync'].to_i + unless gpg_key =~ /[a-zA-Z0-9.-_]+\@[a-zA-Z0-9]+\.[a-zA-Z]+/ + raise I18n.t('error.config.key_bad_format') + end - if @key.empty? or @file_gpg.empty? - @error_msg = I18n.t('error.config.check') - return false - end - I18n.locale = @lang.to_sym + wallet_dir = "#{@config_dir}/wallets" if wallet_dir.to_s.empty? + config = { 'gpg_key' => gpg_key, + 'lang' => lang, + 'wallet_dir' => wallet_dir, + 'default_wallet' => default_wallet, + 'gpg_exe' => gpg_exe, + 'password' => password, + 'pinmode' => pinmode, + 'wallet_paths' => @wallet_paths } - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.config.check')}\n#{e}" - return false - end + FileUtils.mkdir_p(@config_dir, mode: 0700) + FileUtils.mkdir_p(wallet_dir, mode: 0700) - # Check if private key exist - # @rtrn: true if the key exist, else false - def check_gpg_key? - ctx = GPGME::Ctx.new - ctx.each_key(@key, true) do - return true - end + File.open(@config_file, 'w') do |file| + file << config.to_yaml + end + rescue => e + raise "#{I18n.t('error.config.write')}\n#{e}" + end - return false - end + # Setup a new gpg key + # @param password [String] gpg key password + # @param name [String] the name of user + # @param length [Integer] length of the gpg key + # @param expire [Integer] time of expire to gpg key + def setup_gpg_key(password, name, length = 4096, expire = 0) + raise I18n.t('error.config.genkey_gpg.name') if name.to_s.empty? + raise I18n.t('error.config.genkey_gpg.password') if password.to_s.empty? - # Check if private key exist - # @args: share_keys -> string with all public keys - # @rtrn: true if the key exist, else false - def check_public_gpg_key(share_keys = @share_keys) - ctx = GPGME::Ctx.new + param = '' + param << '<GnupgKeyParms format="internal">' + "\n" + param << "Key-Type: RSA\n" + param << "Key-Length: #{length}\n" + param << "Subkey-Type: ELG-E\n" + param << "Subkey-Length: #{length}\n" + param << "Name-Real: #{name}\n" + param << "Name-Comment: #{name}\n" + param << "Name-Email: #{@gpg_key}\n" + param << "Expire-Date: #{expire}\n" + param << "Passphrase: #{password}\n" + param << "</GnupgKeyParms>\n" - share_keys = share_keys.nil? ? '' : share_keys - if not share_keys.empty? - share_keys.split.each do |k| - if not k =~ /[a-zA-Z0-9.-_]+\@[a-zA-Z0-9]+\.[a-zA-Z]+/ - @error_msg = I18n.t('error.config.key_bad_format') - return false - end - - ctx.each_key(key, false) do - next - end + ctx = GPGME::Ctx.new + ctx.genkey(param, nil, nil) + rescue => e + raise "#{I18n.t('error.config.genkey_gpg.exception')}\n#{e}" + end - @error_msg = I18n.t('error.config.no_key_public', key: k) - return false - end - end + # Load the config file + def load_config + config = YAML.load_file(@config_file) + @gpg_key = config['gpg_key'] + @lang = config['lang'] + @wallet_dir = config['wallet_dir'] + @wallet_paths = config['wallet_paths'] || {} + @default_wallet = config['default_wallet'] + @gpg_exe = config['gpg_exe'] + @password = config['password'] || {} + @pinmode = config['pinmode'] || false - return true - end - - # Set the last update when there is a sync - # @rtrn: true is the file has been updated - def set_last_sync - config = {'config' => {'key' => @key, - 'share_keys' => @share_keys, - 'lang' => @lang, - 'file_gpg' => @file_gpg, - 'sync_type' => @sync_type, - 'sync_host' => @sync_host, - 'sync_port' => @sync_port, - 'sync_user' => @sync_user, - 'sync_pwd' => @sync_pwd, - 'sync_path' => @sync_path, - 'last_sync' => Time.now.to_i - } - } - - File.open(@file_config, 'w') do |file| - file << config.to_yaml - end + raise if @gpg_key.empty? || @wallet_dir.empty? - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.config.write')}\n#{e}" - return false - end - - end + I18n.locale = @lang.to_sym + rescue => e + raise "#{I18n.t('error.config.load')}\n#{e}" + end + + # Check if private key exist + # @return [Boolean] true if the key exist, else false + def check_gpg_key? + ctx = GPGME::Ctx.new + ctx.each_key(@gpg_key, true) do + return true + end + + false + end + + # Change the path of one wallet + # @param path [String]new directory path + # @param wallet [String] wallet name + def set_wallet_path(path, wallet) + path = @wallet_dir if path == 'default' + path = File.absolute_path(path) + + return if path == @wallet_dir && File.exist?("#{@wallet_dir}/#{wallet}.mpw") + return if path == @wallet_paths[wallet] + + old_wallet_file = + if @wallet_paths.key?(wallet) + "#{@wallet_paths[wallet]}/#{wallet}.mpw" + else + "#{@wallet_dir}/#{wallet}.mpw" + end + + FileUtils.mkdir_p(path) unless Dir.exist?(path) + FileUtils.mv(old_wallet_file, "#{path}/#{wallet}.mpw") if File.exist?(old_wallet_file) + + if path == @wallet_dir + @wallet_paths.delete(wallet) + else + @wallet_paths[wallet] = path + end + + setup + end + end end diff --git a/lib/mpw/import/gorilla.rb b/lib/mpw/import/gorilla.rb new file mode 100644 index 0000000..feffce6 --- /dev/null +++ b/lib/mpw/import/gorilla.rb @@ -0,0 +1,53 @@ +# +# Copyright:: 2013, Adrien Waksberg +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'csv' + +module MPW + module Import + # Import an export mpw file + # @param file [String] the file path to import + def self.gorilla(file) + data = {} + + CSV.foreach(file, headers: true) do |row| + id = row['uuid'] + comment = + if row['title'] && row['notes'] + "#{row['title']} #{row['notes']}" + elsif row['title'] + row['title'] + elsif row['notes'] + row['notes'] + end + + data[id] = { + 'comment' => comment, + 'group' => row['group'], + 'password' => row['password'], + 'url' => row['url'], + 'user' => row['user'] + } + end + + data + end + end +end diff --git a/lib/mpw/import/keepass.rb b/lib/mpw/import/keepass.rb new file mode 100644 index 0000000..0b961d4 --- /dev/null +++ b/lib/mpw/import/keepass.rb @@ -0,0 +1,53 @@ +# +# Copyright:: 2013, Adrien Waksberg +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'csv' + +module MPW + module Import + # Import an keepass2 export csv file + # @param file [String] the file path to import + def self.keepass(file) + data = {} + + CSV.foreach(file, headers: true) do |row| + id = "#{row['Group']} #{row['Title']}" + comment = + if row['Title'] && row['Notes'] + "#{row['Title']} #{row['Notes']}" + elsif row['Title'] + row['Title'] + elsif row['Notes'] + row['Notes'] + end + + data[id] = { + 'comment' => comment, + 'group' => row['Group'], + 'password' => row['Password'], + 'url' => row['URL'], + 'user' => row['Username'] + } + end + + data + end + end +end diff --git a/lib/mpw/import/mpw.rb b/lib/mpw/import/mpw.rb new file mode 100644 index 0000000..a287357 --- /dev/null +++ b/lib/mpw/import/mpw.rb @@ -0,0 +1,31 @@ +# +# Copyright:: 2013, Adrien Waksberg +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'yaml' + +module MPW + module Import + # Import an export mpw file + # @param file [String] the file path to import + def self.mpw(file) + YAML.load_file(file) + end + end +end diff --git a/lib/mpw/import/mpw_old.rb b/lib/mpw/import/mpw_old.rb new file mode 100644 index 0000000..923bf16 --- /dev/null +++ b/lib/mpw/import/mpw_old.rb @@ -0,0 +1,48 @@ +# +# Copyright:: 2013, Adrien Waksberg +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'yaml' + +module MPW + module Import + # Import an export mpw file + # @param file [String] the file path to import + def self.mpw_old(file) + data = {} + YAML.load_file(file).each do |id, item| + url = '' + url += "#{item['protocol']}://" if item['protocol'] + url += item['host'] + url += ":#{item['port']}" if item['port'] + + data[id] = { + 'comment' => item['comment'], + 'group' => item['group'], + 'otp' => item['otp'], + 'password' => item['password'], + 'url' => url, + 'user' => item['user'] + } + end + + data + end + end +end diff --git a/lib/mpw/item.rb b/lib/mpw/item.rb index 666d5b3..9da60b1 100644 --- a/lib/mpw/item.rb +++ b/lib/mpw/item.rb @@ -1,109 +1,108 @@ -#!/usr/bin/ruby -# author: nishiki -# mail: nishiki@yaegashi.fr +# +# Copyright:: 2013, Adrien Waksberg +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. -require 'rubygems' require 'i18n' - +require 'uri' + module MPW - class Item + class Item + attr_accessor :created + attr_accessor :comment + attr_accessor :group + attr_accessor :host + attr_accessor :id + attr_accessor :otp + attr_accessor :port + attr_accessor :protocol + attr_accessor :last_edit + attr_accessor :url + attr_accessor :user - attr_accessor :error_msg + # @param options [Hash] the option :host is required + def initialize(**options) + @host = '' - attr_accessor :id - attr_accessor :name - attr_accessor :group - attr_accessor :host - attr_accessor :protocol - attr_accessor :user - attr_accessor :password - attr_accessor :port - attr_accessor :comment - attr_accessor :last_edit - attr_accessor :last_sync - attr_accessor :created + if !options[:id] || !options[:created] + @id = generate_id + @created = Time.now.to_i + else + @id = options[:id] + @created = options[:created] + @last_edit = options[:last_edit] + options[:no_update_last_edit] = true + end - # Constructor - # Create a new item - # @args: options -> a hash of parameter - # raise an error if the hash hasn't the key name - def initialize(options={}) - if not options.has_key?(:name) or options[:name].to_s.empty? - @error_msg = I18n.t('error.update.name_empty') - raise @error_msg - end + update(options) + end - if not options.has_key?(:id) or options[:id].to_s.empty? or not options.has_key?(:created) or options[:created].to_s.empty? - @id = generate_id - @created = Time.now.to_i - else - @id = options[:id] - @created = options[:created] - @last_edit = options[:last_edit] - options[:no_update_last_edit] = true - end + # Update the item + # @param options [Hash] + def update(**options) + unless options[:url] || options[:comment] + raise I18n.t('error.update.host_and_comment_empty') + end - update(options) - end + if options[:url] + uri = URI(options[:url]) + @host = uri.host || options[:url] + @port = uri.port || nil + @protocol = uri.scheme || nil + @url = options[:url] + end - # Update the item - # @args: options -> a hash of parameter - # @rtrn: true if the item is update - def update(options={}) - if options.has_key?(:name) and options[:name].to_s.empty? - @error_msg = I18n.t('error.update.name_empty') - return false - end + @comment = options[:comment] if options.key?(:comment) + @group = options[:group] if options.key?(:group) + @last_edit = Time.now.to_i unless options.key?(:no_update_last_edit) + @otp = options[:otp] if options.key?(:otp) + @user = options[:user] if options.key?(:user) + end - @name = options[:name] if options.has_key?(:name) - @group = options[:group] if options.has_key?(:group) - @host = options[:host] if options.has_key?(:host) - @protocol = options[:protocol] if options.has_key?(:protocol) - @user = options[:user] if options.has_key?(:user) - @password = options[:password] if options.has_key?(:password) - @port = options[:port].to_i if options.has_key?(:port) and not options[:port].to_s.empty? - @comment = options[:comment] if options.has_key?(:comment) - @last_edit = Time.now.to_i if not options.has_key?(:no_update_last_edit) + # Delete all data + def delete + @id = nil + @comment = nil + @created = nil + @group = nil + @host = nil + @last_edit = nil + @otp = nil + @port = nil + @protocol = nil + @url = nil + @user = nil + end - return true - end + def empty? + @id.to_s.empty? + end - # Update last_sync - def set_last_sync - @last_sync = Time.now.to_i - end + def nil? + false + end - # Delete all data - # @rtrn: true - def delete - @id = nil - @name = nil - @group = nil - @host = nil - @protocol = nil - @user = nil - @password = nil - @port = nil - @comment = nil - @created = nil - @last_edit = nil - @last_sync = nil + private - return true - end - - def empty? - return @name.to_s.empty? - end - - def nil? - return false - end - - # Generate an random id - private - def generate_id - return ([*('A'..'Z'),*('a'..'z'),*('0'..'9')]).sample(16).join - end - end -end + # Generate an random id + # @return [String] random string + def generate_id + [*('A'..'Z'), *('a'..'z'), *('0'..'9')].sample(16).join + end + end +end diff --git a/lib/mpw/mpw.rb b/lib/mpw/mpw.rb index 57a6bce..191caae 100644 --- a/lib/mpw/mpw.rb +++ b/lib/mpw/mpw.rb @@ -1,332 +1,351 @@ -#!/usr/bin/ruby -# author: nishiki -# mail: nishiki@yaegashi.fr +# +# Copyright:: 2013, Adrien Waksberg +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. -require 'rubygems' +require 'rubygems/package' require 'gpgme' -require 'csv' require 'i18n' -require 'fileutils' require 'yaml' +require 'rotp' require 'mpw/item' - + module MPW - class MPW - - attr_accessor :error_msg - - # Constructor - def initialize(file_gpg, key, share_keys='') - @error_msg = nil - @file_gpg = file_gpg - @key = key - @share_keys = share_keys - @data = [] - end - - # Decrypt a gpg file - # @args: password -> the GPG key password - # @rtrn: true if data has been decrypted - def decrypt(password=nil) - @data = [] + class MPW + # @param key [String] gpg key name + # @param wallet_file [String] path of the wallet file + # @param gpg_pass [String] password of the gpg key + # @param gpg_exe [String] path of the gpg executable + # @param pinmode [Boolean] enable the gpg pinmode + def initialize(key, wallet_file, gpg_pass = nil, gpg_exe = nil, pinmode = false) + @key = key + @gpg_pass = gpg_pass + @gpg_exe = gpg_exe + @wallet_file = wallet_file + @pinmode = pinmode - if File.exist?(@file_gpg) and not File.zero?(@file_gpg) - crypto = GPGME::Crypto.new(armor: true) - data_decrypt = crypto.decrypt(IO.read(@file_gpg), password: password).read.force_encoding('utf-8') + GPGME::Engine.set_info(GPGME::PROTOCOL_OpenPGP, @gpg_exe, "#{Dir.home}/.gnupg") unless @gpg_exe.to_s.empty? + end - if not data_decrypt.to_s.empty? - YAML.load(data_decrypt).each_value do |d| - @data.push(Item.new(id: d['id'], - name: d['name'], - group: d['group'], - host: d['host'], - protocol: d['protocol'], - user: d['user'], - password: d['password'], - port: d['port'], - comment: d['comment'], - last_edit: d['last_edit'], - created: d['created'], - ) - ) - end - end - end - - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.gpg_file.decrypt')}\n#{e}" - return false - end - - # Encrypt a file - # @rtrn: true if the file has been encrypted - def encrypt - FileUtils.cp(@file_gpg, "#{@file_gpg}.bk") if File.exist?(@file_gpg) + # Read mpw file + def read_data + @data = [] + @keys = {} + @passwords = {} + @otp_keys = {} - data_to_encrypt = {} - - @data.each do |item| - next if item.empty? + data = nil - data_to_encrypt.merge!(item.id => {'id' => item.id, - 'name' => item.name, - 'group' => item.group, - 'host' => item.host, - 'protocol' => item.protocol, - 'user' => item.user, - 'password' => item.password, - 'port' => item.port, - 'comment' => item.comment, - 'last_edit' => item.last_edit, - 'created' => item.created, - } - ) - end - - recipients = [] - recipients.push(@key) - if not @share_keys.nil? - @share_keys.split.each { |k| recipients.push(k) } - end + return unless File.exist?(@wallet_file) - crypto = GPGME::Crypto.new(armor: true) - file_gpg = File.open(@file_gpg, 'w+') - crypto.encrypt(data_to_encrypt.to_yaml, recipients: recipients, output: file_gpg) - file_gpg.close - - FileUtils.rm("#{@file_gpg}.bk") if File.exist?("#{@file_gpg}.bk") - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.gpg_file.encrypt')}\n#{e}" - FileUtils.mv("#{@file_gpg}.bk", @file_gpg) if File.exist?("#{@file_gpg}.bk") - return false - end - - # Add a new item - # @args: item -> Object MPW::Item - # @rtrn: true if add item - def add(item) - if not item.instance_of?(Item) - @error_msg = I18n.t('error.bad_class') - return false - elsif item.empty? - @error_msg = I18n.t('error.add.empty') - return false - else - @data.push(item) - return true - end - end + Gem::Package::TarReader.new(File.open(@wallet_file)) do |tar| + tar.each do |f| + case f.full_name + when 'wallet/meta.gpg' + data = decrypt(f.read) - # Search in some csv data - # @args: options -> a hash with paramaters - # @rtrn: a list with the resultat of the search - def list(options={}) - result = [] - - search = options[:search].to_s.downcase - group = options[:group].to_s.downcase - protocol = options[:protocol].to_s.downcase + when %r{^wallet/keys/(?<key>.+)\.pub$} + key = Regexp.last_match('key') - @data.each do |item| - next if item.empty? + if GPGME::Key.find(:public, key).empty? + GPGME::Key.import(f.read, armor: true) + end - next if not group.empty? and not group.eql?(item.group.downcase) - next if not protocol.empty? and not protocol.eql?(item.protocol.downcase) - - name = item.name.to_s.downcase - host = item.host.to_s.downcase - comment = item.comment.to_s.downcase + @keys[key] = f.read - if not name =~ /^.*#{search}.*$/ and not host =~ /^.*#{search}.*$/ and not comment =~ /^.*#{search}.*$/ - next - end + when %r{^wallet/passwords/(?<id>[a-zA-Z0-9]+)\.gpg$} + @passwords[Regexp.last_match('id')] = f.read - result.push(item) - end - - return result - end - - # Search in some csv data - # @args: id -> the id item - # @rtrn: a row with the result of the search - def search_by_id(id) - @data.each do |item| - return item if item.id == id - end - - return nil - end - - # Export to csv - # @args: file -> file where you export the data - # type -> udata type - # @rtrn: true if export work - def export(file, type=:yaml) - case type - when :csv - CSV.open(file, 'w', write_headers: true, - headers: ['name', 'group', 'protocol', 'host', 'user', 'password', 'port', 'comment']) do |csv| - @data.each do |item| - csv << [item.name, item.group, item.protocol, item.host, item.user, item.password, item.port, item.comment] - end - end + when %r{^wallet/otp_keys/(?<id>[a-zA-Z0-9]+)\.gpg$} + @otp_keys[Regexp.last_match('id')] = f.read - when :yaml - data = {} - @data.each do |item| - data.merge!(item.id => {'id' => item.id, - 'name' => item.name, - 'group' => item.group, - 'host' => item.host, - 'protocol' => item.protocol, - 'user' => item.user, - 'password' => item.password, - 'port' => item.port, - 'comment' => item.comment, - 'last_edit' => item.last_edit, - 'created' => item.created, - } - ) - end + else + next + end + end + end - File.open(file, 'w') {|f| f << data.to_yaml} + unless data.to_s.empty? + YAML.safe_load(data).each_value do |d| + @data.push( + Item.new( + id: d['id'], + group: d['group'], + user: d['user'], + url: d['url'], + otp: @otp_keys.key?(d['id']), + comment: d['comment'], + last_edit: d['last_edit'], + created: d['created'] + ) + ) + end + end - else - @error_msg = "#{I18n.t('error.export.unknown_type', type: type)}" - return false - end + add_key(@key) unless @keys.key?(@key) + rescue => e + raise "#{I18n.t('error.mpw_file.read_data')}\n#{e}" + end - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.export.write', file: file)}\n#{e}" - return false - end - - # Import to csv - # @args: file -> path to file import - # type -> udata type - # @rtrn: true if the import work - def import(file, type=:yaml) - case type - when :csv - CSV.foreach(file, {headers: true}) do |row| - item = Item.new(name: row['name'], - group: row['group'], - host: row['host'], - protocol: row['protocol'], - user: row['user'], - password: row['password'], - port: row['port'], - comment: row['comment'], - ) + # Encrypt all data in tarball + def write_data + data = {} + tmp_file = "#{@wallet_file}.tmp" - return false if item.empty? + @data.each do |item| + next if item.empty? - @data.push(item) - end + data.merge!( + item.id => { + 'id' => item.id, + 'group' => item.group, + 'user' => item.user, + 'url' => item.url, + 'comment' => item.comment, + 'last_edit' => item.last_edit, + 'created' => item.created + } + ) + end - when :yaml - YAML::load_file(file).each_value do |row| - item = Item.new(name: row['name'], - group: row['group'], - host: row['host'], - protocol: row['protocol'], - user: row['user'], - password: row['password'], - port: row['port'], - comment: row['comment'], - ) + Gem::Package::TarWriter.new(File.open(tmp_file, 'w+')) do |tar| + data_encrypt = encrypt(data.to_yaml) + tar.add_file_simple('wallet/meta.gpg', 0400, data_encrypt.length) do |io| + io.write(data_encrypt) + end - return false if item.empty? + @passwords.each do |id, password| + tar.add_file_simple("wallet/passwords/#{id}.gpg", 0400, password.length) do |io| + io.write(password) + end + end - @data.push(item) - end + @otp_keys.each do |id, key| + tar.add_file_simple("wallet/otp_keys/#{id}.gpg", 0400, key.length) do |io| + io.write(key) + end + end - else - @error_msg = "#{I18n.t('error.export.unknown_type', type: type)}" - return false - end - - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.import.read', file: file)}\n#{e}" - return false - end - - # Return a preview import - # @args: file -> path to file import - # @rtrn: a hash with the items to import, if there is an error return false - def import_preview(file, type=:yaml) - data = [] + @keys.each do |id, key| + tar.add_file_simple("wallet/keys/#{id}.pub", 0400, key.length) do |io| + io.write(key) + end + end + end - case type - when :csv - CSV.foreach(file, {headers: true}) do |row| - item = Item.new(name: row['name'], - group: row['group'], - host: row['host'], - protocol: row['protocol'], - user: row['user'], - password: row['password'], - port: row['port'], - comment: row['comment'], - ) + File.rename(tmp_file, @wallet_file) + rescue => e + File.unlink(tmp_file) if File.exist?(tmp_file) - return false if item.empty? + raise "#{I18n.t('error.mpw_file.write_data')}\n#{e}" + end - data.push(item) - end + # Get a password + # @param id [String] the item id + def get_password(id) + password = decrypt(@passwords[id]) - when :yaml - YAML::load_file(file).each_value do |row| - item = Item.new(name: row['name'], - group: row['group'], - host: row['host'], - protocol: row['protocol'], - user: row['user'], - password: row['password'], - port: row['port'], - comment: row['comment'], - ) + if /^\$[a-zA-Z0-9]{4,9}::(?<password>.+)$/ =~ password + Regexp.last_match('password') + else + password + end + end - return false if item.empty? + # Set a new password for an item + # @param id [String] the item id + # @param password [String] the new password + def set_password(id, password) + salt = MPW.password(length: Random.rand(4..9)) + password = "$#{salt}::#{password}" - data.push(item) - end + @passwords[id] = encrypt(password) + end - else - @error_msg = "#{I18n.t('error.export.unknown_type', type: type)}" - return false - end - - return data - rescue Exception => e - @error_msg = "#{I18n.t('error.import.read', file: file)}\n#{e}" - return false - end - - # Generate a random password - # @args: length -> the length password - # @rtrn: a random string - def self.password(length=8) - if length.to_i <= 0 - length = 8 - else - length = length.to_i - end - - result = '' - while length > 62 do - result << ([*('A'..'Z'),*('a'..'z'),*('0'..'9')]).sample(62).join - length -= 62 - end - result << ([*('A'..'Z'),*('a'..'z'),*('0'..'9')]).sample(length).join - - return result - end + # Return the list of all gpg keys + # @return [Array] the gpg keys name + def list_keys + @keys.keys + end - end + # Add a public key + # @param key [String] new public key file or name + def add_key(key) + if File.exist?(key) + data = File.open(key).read + key_import = GPGME::Key.import(data, armor: true) + key = GPGME::Key.get(key_import.imports[0].fpr).uids[0].email + else + data = GPGME::Key.export(key, armor: true).read + end + + raise I18n.t('error.export_key') if data.to_s.empty? + + @keys[key] = data + @passwords.each_key { |id| set_password(id, get_password(id)) } + @otp_keys.each_key { |id| set_otp_key(id, get_otp_key(id)) } + end + + # Delete a public key + # @param key [String] public key to delete + def delete_key(key) + @keys.delete(key) + @passwords.each_key { |id| set_password(id, get_password(id)) } + @otp_keys.each_key { |id| set_otp_key(id, get_otp_key(id)) } + end + + # Add a new item + # @param item [Item] + def add(item) + raise I18n.t('error.bad_class') unless item.instance_of?(Item) + raise I18n.t('error.empty') if item.empty? + + @data.push(item) + end + + # Search in some csv data + # @param options [Hash] + # @return [Array] a list with the resultat of the search + def list(**options) + result = [] + + search = options[:pattern].to_s.downcase + group = options[:group].to_s.downcase + + @data.each do |item| + next if item.empty? + next unless group.empty? || group.eql?(item.group.to_s.downcase) + + host = item.host.to_s.downcase + comment = item.comment.to_s.downcase + + next unless host =~ /^.*#{search}.*$/ || comment =~ /^.*#{search}.*$/ + + result.push(item) + end + + result + end + + # Search an item with an id + # @param id [String]the id item + # @return [Item] an item or nil + def search_by_id(id) + @data.each do |item| + return item if item.id == id + end + + nil + end + + # Set a new opt key + # @param id [String] the item id + # @param key [String] the new key + def set_otp_key(id, key) + @otp_keys[id] = encrypt(key.to_s) unless key.to_s.empty? + end + + # Get an opt key + # @param id [String] the item id + def get_otp_key(id) + @otp_keys.key?(id) ? decrypt(@otp_keys[id]) : nil + end + + # Get an otp code + # @param id [String] the item id + # @return [String] an otp code + def get_otp_code(id) + @otp_keys.key?(id) ? ROTP::TOTP.new(decrypt(@otp_keys[id])).now : 0 + end + + # Get remaining time before expire otp code + # @return [Integer] time in seconde + def get_otp_remaining_time + (Time.now.utc.to_i / 30 + 1) * 30 - Time.now.utc.to_i + end + + # Generate a random password + # @param options [Hash] :length, :special, :alpha, :numeric + # @return [String] a random string + def self.password(**options) + length = + if !options.include?(:length) || options[:length].to_i <= 0 + 8 + elsif options[:length].to_i >= 32_768 + 32_768 + else + options[:length].to_i + end + + chars = [] + chars += [*('!'..'?')] - [*('0'..'9')] if options[:special] + chars += [*('A'..'Z'), *('a'..'z')] if options[:alpha] + chars += [*('0'..'9')] if options[:numeric] + chars = [*('A'..'Z'), *('a'..'z'), *('0'..'9')] if chars.empty? + + result = '' + length.times do + result << chars.sample + end + + result + end + + private + + # Decrypt a gpg file + # @param data [String] data to decrypt + # @return [String] data decrypted + def decrypt(data) + return nil if data.to_s.empty? + + password = + if /^(1\.[0-9.]+|2\.0)(\.[0-9]+)?/ =~ GPGME::Engine.info.first.version || @pinmode + { password: @gpg_pass } + else + { password: @gpg_pass, + pinentry_mode: GPGME::PINENTRY_MODE_LOOPBACK } + end + + crypto = GPGME::Crypto.new(armor: true) + crypto + .decrypt(data, password) + .read.force_encoding('utf-8') + rescue => e + raise "#{I18n.t('error.gpg_file.decrypt')}\n#{e}" + end + + # Encrypt a file + # @param data [String] data to encrypt + # @return [String] data encrypted + def encrypt(data) + recipients = [] + crypto = GPGME::Crypto.new(armor: true, always_trust: true) + + recipients.push(@key) + @keys.each_key do |key| + next if key == @key + recipients.push(key) + end + + crypto.encrypt(data, recipients: recipients).read + rescue => e + raise "#{I18n.t('error.gpg_file.encrypt')}\n#{e}" + end + end end diff --git a/lib/mpw/server.rb b/lib/mpw/server.rb deleted file mode 100644 index 415bb26..0000000 --- a/lib/mpw/server.rb +++ /dev/null @@ -1,343 +0,0 @@ -#!/usr/bin/ruby - -module MPW - - require 'socket' - require 'json' - require 'highline/import' - require 'digest' - require 'logger' - - - class Server - - attr_accessor :error_msg - - # Constructor - def initialize - YAML::ENGINE.yamler='syck' - end - - # Start the server - def start - server = TCPServer.open(@host, @port) - @log.info("The server is started on #{@host}:#{@port}") - - loop do - Thread.start(server.accept) do |client| - @log.info("#{client.peeraddr[3]} is connected") - - while true do - msg = get_client_msg(client) - - next if not msg - - if msg['gpg_key'].nil? or msg['gpg_key'].empty? or msg['password'].nil? or msg['password'].empty? - @log.warning("#{client.peeraddr[3]} is disconnected because no password or no gpg_key") - close_connection(client) - next - end - - case msg['action'] - when 'get' - @log.debug("#{client.peeraddr[3]} GET gpg_key=#{msg['gpg_key']} suffix=#{msg['suffix']}") - client.puts get_file(msg) - when 'update' - @log.debug("#{client.peeraddr[3]} UPDATE gpg_key=#{msg['gpg_key']} suffix=#{msg['suffix']}") - client.puts update_file(msg) - when 'delete' - @log.debug("#{client.peeraddr[3]} DELETE gpg_key=#{msg['gpg_key']} suffix=#{msg['suffix']}") - client.puts delete_file(msg) - else - @log.warning("#{client.peeraddr[3]} is disconnected because unkwnow command") - send_msg = {action: 'unknown', - gpg_key: msg['gpg_key'], - error: 'server.error.client.unknown' - } - client.puts send_msg - close_connection(client) - end - end - end - end - - rescue Exception => e - puts "Impossible to start the server: #{e}" - @log.error("Impossible to start the server: #{e}") - exit 2 - end - - # Get a gpg file - # @args: msg -> message puts by the client - # @rtrn: json message - def get_file(msg) - gpg_key = msg['gpg_key'].sub('@', '_') - - if msg['suffix'].nil? or msg['suffix'].empty? - file_gpg = "#{@data_dir}/#{gpg_key}.yml" - else - file_gpg = "#{@data_dir}/#{gpg_key}-#{msg['suffix']}.yml" - end - - if File.exist?(file_gpg) - gpg_data = YAML::load_file(file_gpg) - salt = gpg_data['gpg']['salt'] - hash = gpg_data['gpg']['hash'] - data = gpg_data['gpg']['data'] - - if is_authorized?(msg['password'], salt, hash) - send_msg = {action: 'get', - gpg_key: msg['gpg_key'], - data: data, - error: nil - } - else - send_msg = {action: 'get', - gpg_key: msg['gpg_key'], - error: 'server.error.client.no_authorized' - } - end - else - send_msg = {action: 'get', - gpg_key: msg['gpg_key'], - data: '', - error: nil - } - end - - return send_msg.to_json - end - - # Update a file - # @args: msg -> message puts by the client - # @rtrn: json message - def update_file(msg) - gpg_key = msg['gpg_key'].sub('@', '_') - data = msg['data'] - - if data.nil? or data.empty? - send_msg = {action: 'update', - gpg_key: msg['gpg_key'], - error: 'server.error.client.no_data' - } - - return send_msg.to_json - end - - if msg['suffix'].nil? or msg['suffix'].empty? - file_gpg = "#{@data_dir}/#{gpg_key}.yml" - else - file_gpg = "#{@data_dir}/#{gpg_key}-#{msg['suffix']}.yml" - end - - if File.exist?(file_gpg) - gpg_data = YAML::load_file(file_gpg) - salt = gpg_data['gpg']['salt'] - hash = gpg_data['gpg']['hash'] - - else - salt = generate_salt - hash = Digest::SHA256.hexdigest(salt + msg['password']) - end - - if is_authorized?(msg['password'], salt, hash) - begin - config = {'gpg' => {'salt' => salt, - 'hash' => hash, - 'data' => data - } - } - - File.open(file_gpg, 'w+') do |file| - file << config.to_yaml - end - - send_msg = {action: 'update', - gpg_key: msg['gpg_key'], - error: nil - } - rescue Exception => e - send_msg = {action: 'update', - gpg_key: msg['gpg_key'], - error: 'server.error.client.unknown' - } - end - else - send_msg = {action: 'update', - gpg_key: msg['gpg_key'], - error: 'server.error.client.no_authorized' - } - end - - return send_msg.to_json - end - - # Remove a gpg file - # @args: msg -> message puts by the client - # @rtrn: json message - def delete_file(msg) - gpg_key = msg['gpg_key'].sub('@', '_') - - if msg['suffix'].nil? or msg['suffix'].empty? - file_gpg = "#{@data_dir}/#{gpg_key}.yml" - else - file_gpg = "#{@data_dir}/#{gpg_key}-#{msg['suffix']}.yml" - end - - if not File.exist?(file_gpg) - send_msg = {:action => 'delete', - :gpg_key => msg['gpg_key'], - :error => nil - } - - return send_msg.to_json - end - - gpg_data = YAML::load_file(file_gpg) - salt = gpg_data['gpg']['salt'] - hash = gpg_data['gpg']['hash'] - - if is_authorized?(msg['password'], salt, hash) - begin - File.unlink(file_gpg) - - send_msg = {action: 'delete', - gpg_key: msg['gpg_key'], - error: nil - } - rescue Exception => e - send_msg = {action: 'delete', - gpg_key: msg['gpg_key'], - error: 'server.error.client.unknown' - } - end - else - send_msg = {action: 'delete', - gpg_key: msg['gpg_key'], - error: 'server.error.client.no_authorized' - } - end - - return send_msg.to_json - end - - # Check is the hash equal the password with the salt - # @args: password -> the user password - # salt -> the salt - # hash -> the hash of the password with the salt - # @rtrn: true is is good, else false - def is_authorized?(password, salt, hash) - if hash == Digest::SHA256.hexdigest(salt + password) - return true - else - return false - end - end - - # Get message to client - # @args: client -> client connection - # @rtrn: array of the json string, or false if isn't json message - def get_client_msg(client) - msg = client.gets - return JSON.parse(msg) - rescue - closeConnection(client) - return false - end - - # Close the client connection - # @args: client -> client connection - def close_connection(client) - client.puts "Closing the connection. Bye!" - client.close - end - - # Check the config file - # @args: file_config -> the configuration file - # @rtrn: true if the config file is correct - def checkconfig(file_config) - config = YAML::load_file(file_config) - @host = config['config']['host'] - @port = config['config']['port'].to_i - @data_dir = config['config']['data_dir'] - @log_file = config['config']['log_file'] - @timeout = config['config']['timeout'].to_i - - if @host.empty? or @port <= 0 or @data_dir.empty? - puts I18n.t('checkconfig.fail') - puts I18n.t('checkconfig.empty') - return false - end - - if not Dir.exist?(@data_dir) - puts I18n.t('checkconfig.fail') - puts I18n.t('checkconfig.datadir') - return false - end - - if @log_file.nil? or @log_file.empty? - puts I18n.t('checkconfig.fail') - puts I18n.t('checkconfig.log_file_empty') - return false - else - begin - @log = Logger.new(@log_file) - rescue - puts I18n.t('checkconfig.fail') - puts I18n.t('checkconfig.log_file_create') - return false - end - end - - return true - rescue Exception => e - puts "#{I18n.t('checkconfig.fail')}\n#{e}" - return false - end - - # Create a new config file - # @args: file_config -> the configuration file - # @rtrn: true if le config file is create - def setup(file_config) - puts I18n.t('form.setup.title') - puts '--------------------' - host = ask(I18n.t('form.setup.host')).to_s - port = ask(I18n.t('form.setup.port')).to_s - data_dir = ask(I18n.t('form.setup.data_dir')).to_s - log_file = ask(I18n.t('form.setup.log_file')).to_s - timeout = ask(I18n.t('form.setup.timeout')).to_s - - config = {'config' => {'host' => host, - 'port' => port, - 'data_dir' => data_dir, - 'log_file' => log_file, - 'timeout' => timeout - } - } - - File.open(file_config, 'w') do |file| - file << config.to_yaml - end - - return true - rescue Exception => e - puts "#{I18n.t('form.setup.not_valid')}\n#{e}" - return false - end - - # Generate a random salt - # @args: length -> the length salt - # @rtrn: a random string - def generate_salt(length=4) - if length.to_i <= 0 or length.to_i > 16 - length = 4 - else - length = length.to_i - end - - return ([*('A'..'Z'),*('a'..'z'),*('0'..'9')]).sample(length).join - end - - end - -end diff --git a/lib/mpw/sync.rb b/lib/mpw/sync.rb deleted file mode 100644 index 3e18d32..0000000 --- a/lib/mpw/sync.rb +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/ruby -# author: nishiki -# mail: nishiki@yaegashi.fr -# info: a simple script who manage your passwords - -require 'rubygems' -require 'i18n' -require 'yaml' -require 'tempfile' -require 'mpw/mpw' -require 'mpw/item' - -module MPW - class Sync - - attr_accessor :error_msg - - # Constructor - # raise an exception if there is a bad parameter - def initialize(config, local, password=nil) - @error_msg = nil - @config = config - @local = local - @password = password - - raise I18n.t('error.class') if not @local.instance_of?(MPW) - end - - # Get the data on remote host - # @rtrn: true if get the date, else false - def get_remote - case @config.sync_type - when 'mpw' - require 'mpw/sync/mpw' - @sync = SyncMPW.new(@config.sync_host, @config.sync_user, @config.sync_pwd, @config.sync_path, @config.sync_port) - when 'sftp', 'scp', 'ssh' - require 'mpw/sync/ssh' - @sync = SyncSSH.new(@config.sync_host, @config.sync_user, @config.sync_pwd, @config.sync_path, @config.sync_port) - when 'ftp' - require 'mpw/sync/ftp' - @sync = SyncFTP.new(@config.sync_host, @config.sync_user, @config.sync_pwd, @config.sync_path, @config.sync_port) - else - @error_msg = I18n.t('error.unknown_type') - return false - end - - if not @sync.connect - @error_msg = @sync.error_msg - return false - end - - - file_tmp = Tempfile.new('mpw-') - raise @sync.error_msg if not @sync.get(file_tmp.path) - - @remote = MPW.new(file_tmp.path, @config.key) - raise @remote.error_msg if not @remote.decrypt(@password) - - file_tmp.close(true) - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.sync.download')} #{e}" - file_tmp.close(true) - return false - end - - # Sync remote data and local data - # raise an exception if there is a problem - def sync - - if not @remote.to_s.empty? - @local.list.each do |item| - update = false - @remote.list.each do |r| - - # Update item - if item.id == r.id - if item.last_edit < r.last_edit - raise item.error_msg if not item.update(name: r.name, - group: r.group, - host: r.host, - protocol: r.protocol, - user: r.user, - password: r.password, - port: r.port, - comment: r.comment - ) - end - - r.delete - update = true - - break - end - end - - # Remove an old item - if not update and item.last_sync.to_i < @config.last_sync and item.last_edit < @config.last_sync - item.delete - end - end - end - - # Add item - @remote.list.each do |r| - if r.last_edit > @config.last_sync - item = Item.new(id: r.id, - name: r.name, - group: r.group, - host: r.host, - protocol: r.protocol, - user: r.user, - password: r.password, - port: r.port, - comment: r.comment, - created: r.created, - last_edit: r.last_edit - ) - raise @local.error_msg if not @local.add(item) - end - end - - @local.list.each do |item| - item.set_last_sync - end - - raise @mpw.error_msg if not @local.encrypt - raise @sync.error_msg if not @sync.update(@config.file_gpg) - - @config.set_last_sync - - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.sync.unknown')} #{e}" - return false - end - end -end diff --git a/lib/mpw/sync/ftp.rb b/lib/mpw/sync/ftp.rb deleted file mode 100644 index 0d324e7..0000000 --- a/lib/mpw/sync/ftp.rb +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/ruby -# author: nishiki -# mail: nishiki@yaegashi.fr - -require 'rubygems' -require 'i18n' -require 'net/ftp' - -module MPW - class FTP - - attr_accessor :error_msg - attr_accessor :enable - - # Constructor - # @args: host -> the server host - # port -> ther connection port - # gpg_key -> the gpg key - # password -> the remote password - # suffix -> the suffix file - def initialize(host, user, password, path, port=nil) - @error_msg = nil - @enable = false - - @host = host - @user = user - @password = password - @path = path - @port = port.instance_of?(Integer) ? 21 : port - end - - # Connect to server - # @rtrn: false if the connection fail - def connect - Net::FTP.open(@host) do |ftp| - ftp.login(@user, @password) - @enable = true - end - rescue Exception => e - @error_msg = "#{I18n.t('error.sync.connection')}\n#{e}" - @enable = false - else - return @enable - end - - # Get data on server - # @args: gpg_password -> the gpg password - # @rtrn: nil if nothing data or error - def get(file_tmp) - return false if not @enable - - Net::FTP.open(@host) do |ftp| - ftp.login(@user, @password) - ftp.gettextfile(@path, file_tmp) - end - - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.sync.download')}\n#{e}" - return false - end - - # Update the remote data - # @args: data -> the data to send on server - # @rtrn: false if there is a problem - def update(file_gpg) - return true if not @enable - - Net::FTP.open(@host) do |ftp| - ftp.login(@user, @password) - ftp.puttextfile(file_gpg, @path) - end - - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.sync.upload')}\n#{e}" - return false - end - - end -end diff --git a/lib/mpw/sync/mpw.rb b/lib/mpw/sync/mpw.rb deleted file mode 100644 index 596414d..0000000 --- a/lib/mpw/sync/mpw.rb +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/ruby -# author: nishiki - -require 'rubygems' -require 'i18n' -require 'socket' -require 'json' -require 'timeout' - -module MPW - class SyncMPW - - attr_accessor :error_msg - attr_accessor :enable - - # Constructor - # @args: host -> the server host - # port -> ther connection port - # gpg_key -> the gpg key - # password -> the remote password - # suffix -> the suffix file - def initialize(host, user, password, suffix, port=nil) - @error_msg = nil - @enable = false - - @host = host - @port = !port.instance_of?(Integer) ? 2201 : port - @gpg_key = user - @password = password - @suffix = suffix - end - - # Connect to server - # @rtrn: false if the connection fail - def connect - Timeout.timeout(10) do - begin - TCPSocket.open(@host, @port) do - @enable = true - end - rescue Errno::ENETUNREACH - retry - end - end - rescue Timeout::Error - @error_msg = "#{I18n.t('error.timeout')}\n#{e}" - @enable = false - rescue Exception => e - @error_msg = "#{I18n.t('error.sync.connection')}\n#{e}" - @enable = false - else - return @enable - end - - # Get data on server - # @args: gpg_password -> the gpg password - # @rtrn: nil if nothing data or error - def get(file_tmp) - return false if not @enable - - msg = nil - TCPSocket.open(@host, @port) do |socket| - send_msg = {action: 'get', - gpg_key: @gpg_key, - password: @password, - suffix: @suffix - } - - socket.puts send_msg.to_json - msg = JSON.parse(socket.gets) - end - - if not defined?(msg['error']) - @error_msg = I18n.t('error.sync.communication') - return false - elsif not msg['error'].nil? - @error_msg = I18n.t(msg['error']) - return false - end - - File.open(file_tmp, 'w') do |file| - file << msg['data'] - end - - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.sync.download')}\n#{e}" - return false - end - - # Update the remote data - # @args: data -> the data to send on server - # @rtrn: false if there is a problem - def update(file_gpg) - return true if not @enable - - data = File.open(file_gpg, 'r').read - - msg = nil - TCPSocket.open(@host, @port) do |socket| - send_msg = {action: 'update', - gpg_key: @gpg_key, - password: @password, - suffix: @suffix, - data: data - } - - socket.puts send_msg.to_json - msg = JSON.parse(socket.gets) - end - - if not defined?(msg['error']) - @error_msg = I18n.t('error.sync.communication') - return false - elsif msg['error'].nil? - return true - else - @error_msg = I18n.t(msg['error']) - return false - end - end - - end -end diff --git a/lib/mpw/sync/ssh.rb b/lib/mpw/sync/ssh.rb deleted file mode 100644 index 632e292..0000000 --- a/lib/mpw/sync/ssh.rb +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/ruby -# author: nishiki -# mail: nishiki@yaegashi.fr - -require 'rubygems' -require 'i18n' -require 'net/ssh' -require 'net/sftp' - -module MPW - class SyncSSH - - attr_accessor :error_msg - attr_accessor :enable - - # Constructor - # @args: host -> the server host - # port -> ther connection port - # gpg_key -> the gpg key - # password -> the remote password - def initialize(host, user, password, path, port=nil) - @error_msg = nil - @enable = false - - @host = host - @user = user - @password = password - @path = path - @port = port.instance_of?(Integer) ? 22 : port - end - - # Connect to server - # @rtrn: false if the connection fail - def connect - Net::SSH.start(@host, @user, password: @password, port: @port) do |ssh| - @enable = true - end - rescue Exception => e - @error_msg = "#{I18n.t('error.sync.connection')}\n#{e}" - @enable = false - else - return @enable - end - - # Get data on server - # @args: gpg_password -> the gpg password - # @rtrn: nil if nothing data or error - def get(file_tmp) - return false if not @enable - - Net::SFTP.start(@host, @user, password: @password, port: @port) do |sftp| - sftp.lstat(@path) do |response| - sftp.download!(@path, file_tmp) if response.ok? - end - end - - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.sync.download')}\n#{e}" - return false - end - - # Update the remote data - # @args: file_gpg -> the data to send on server - # @rtrn: false if there is a problem - def update(file_gpg) - return true if not @enable - - Net::SFTP.start(@host, @user, password: @password, port: @port) do |sftp| - sftp.upload!(file_gpg, @path) - end - - return true - rescue Exception => e - @error_msg = "#{I18n.t('error.sync.upload')}\n#{e}" - return false - end - - end -end diff --git a/lib/mpw/ui/cli.rb b/lib/mpw/ui/cli.rb deleted file mode 100644 index 3875be5..0000000 --- a/lib/mpw/ui/cli.rb +++ /dev/null @@ -1,324 +0,0 @@ -#!/usr/bin/ruby -# author: nishiki -# mail: nishiki@yaegashi.fr -# info: a simple script who m your passwords - -require 'rubygems' -require 'highline/import' -require 'pathname' -require 'readline' -require 'i18n' -require 'colorize' -require 'mpw/sync' -require 'mpw/mpw' -require 'mpw/item' - -class Cli - - # Constructor - # @args: lang -> the operating system language - # config_file -> a specify config file - def initialize(config) - @config = config - end - - # Sync the data with the server - # @rtnr: true if the synchro is finish - def sync - @sync = MPW::Sync.new(@config, @mpw, @password) - - raise(@sync.error_msg) if not @sync.get_remote - raise(@sync.error_msg) if not @sync.sync - - return true - rescue Exception => e - puts "#{I18n.t('display.error')} #7: #{e}".red - return false - end - - # Create a new config file - # @args: lang -> the software language - def setup(lang) - puts I18n.t('form.setup.title') - puts '--------------------' - language = ask(I18n.t('form.setup.lang', lang: lang)).to_s - key = ask(I18n.t('form.setup.gpg_key')).to_s - share_keys = ask(I18n.t('form.setup.share_gpg_keys')).to_s - file_gpg = ask(I18n.t('form.setup.gpg_file', home: @conf.dir_config)).to_s - sync_type = ask(I18n.t('form.setup.sync_type')).to_s - - if ['ssh', 'ftp', 'mpw'].include?(sync_type) - sync_host = ask(I18n.t('form.setup.sync_host')).to_s - sync_port = ask(I18n.t('form.setup.sync_port')).to_s - sync_user = ask(I18n.t('form.setup.sync_user')).to_s - sync_pwd = ask(I18n.t('form.setup.sync_pwd')).to_s - sync_path = ask(I18n.t('form.setup.sync_path')).to_s - end - - if language.nil? or language.empty? - language = lang - end - I18n.locale = language.to_sym - - sync_type = sync_type.nil? or sync_type.empty? ? nil : sync_type - sync_host = sync_host.nil? or sync_host.empty? ? nil : sync_host - sync_port = sync_port.nil? or sync_port.empty? ? nil : sync_port.to_i - sync_user = sync_user.nil? or sync_user.empty? ? nil : sync_user - sync_pwd = sync_pwd.nil? or sync_pwd.empty? ? nil : sync_pwd - sync_path = sync_path.nil? or sync_path.empty? ? nil : sync_path - - if @config.setup(key, share_keys, language, file_gpg, sync_type, sync_host, sync_port, sync_user, sync_pwd, sync_path) - puts "#{I18n.t('form.setup.valid')}".green - else - puts "#{I18n.t('display.error')} #8: #{@config.error_msg}".red - exit 2 - end - - if not @config.checkconfig - puts "#{I18n.t('display.error')} #9: #{@config.error_msg}".red - exit 2 - end - end - - # Setup a new GPG key - def setup_gpg_key - puts I18n.t('form.setup_gpg_key.title') - puts '--------------------' - ask = ask(I18n.t('form.setup_gpg_key.ask')).to_s - - if not ['Y', 'y', 'O', 'o'].include?(ask) - puts I18n.t('form.setup_gpg_key.no_create') - exit 2 - end - - name = ask(I18n.t('form.setup_gpg_key.name')).to_s - password = ask(I18n.t('form.setup_gpg_key.password')) {|q| q.echo = false} - confirm = ask(I18n.t('form.setup_gpg_key.confirm_password')) {|q| q.echo = false} - - if password != confirm - puts I18n.t('form.setup_gpg_key.error_password') - exit 2 - end - - length = ask(I18n.t('form.setup_gpg_key.length')).to_s - expire = ask(I18n.t('form.setup_gpg_key.expire')).to_s - password = password.to_s - - length = length.nil? or length.empty? ? 2048 : length.to_i - expire = expire.nil? or expire.empty? ? 0 : expire.to_i - - puts I18n.t('form.setup_gpg_key.wait') - - if @config.setup_gpg_key(password, name, length, expire) - puts "#{I18n.t('form.setup_gpg_key.valid')}".green - else - puts "#{I18n.t('display.error')} #10: #{@config.error_msg}".red - exit 2 - end - end - - # Request the GPG password and decrypt the file - def decrypt - if not defined?(@mpw) - @mpw = MPW::MPW.new(@config.file_gpg, @config.key, @config.share_keys) - end - - @password = ask(I18n.t('display.gpg_password')) {|q| q.echo = false} - if not @mpw.decrypt(@password) - puts "#{I18n.t('display.error')} #11: #{@mpw.error_msg}".red - exit 2 - end - end - - # Display the query's result - # @args: search -> the string to search - # protocol -> search from a particular protocol - def display(options={}) - result = @mpw.list(options) - - case result.length - when 0 - puts I18n.t('display.nothing') - when 1 - display_item(result.first) - else - i = 1 - result.each do |item| - print "#{i}: ".cyan - print item.name - print " -> #{item.comment}".magenta if not item.comment.to_s.empty? - print "\n" - - i += 1 - end - choice = ask(I18n.t('form.select')).to_i - - if choice >= 1 and choice < i - display_item(result[choice-1]) - else - puts "#{I18n.t('display.warning')}: #{I18n.t('warning.select')}".yellow - end - end - end - - # Display an item in the default format - # @args: item -> an array with the item information - def display_item(item) - puts '--------------------'.cyan - print 'Id: '.cyan - puts item.id - print "#{I18n.t('display.name')}: ".cyan - puts item.name - print "#{I18n.t('display.group')}: ".cyan - puts item.group - print "#{I18n.t('display.server')}: ".cyan - puts item.host - print "#{I18n.t('display.protocol')}: ".cyan - puts item.protocol - print "#{I18n.t('display.login')}: ".cyan - puts item.user - print "#{I18n.t('display.password')}: ".cyan - puts item.password - print "#{I18n.t('display.port')}: ".cyan - puts item.port - print "#{I18n.t('display.comment')}: ".cyan - puts item.comment - end - - # Form to add a new item - def add - options = {} - - puts I18n.t('form.add.title') - puts '--------------------' - options[:name] = ask(I18n.t('form.add.name')).to_s - options[:group] = ask(I18n.t('form.add.group')).to_s - options[:host] = ask(I18n.t('form.add.server')).to_s - options[:protocol] = ask(I18n.t('form.add.protocol')).to_s - options[:user] = ask(I18n.t('form.add.login')).to_s - options[:password] = ask(I18n.t('form.add.password')).to_s - options[:port] = ask(I18n.t('form.add.port')).to_s - options[:comment] = ask(I18n.t('form.add.comment')).to_s - - item = MPW::Item.new(options) - if @mpw.add(item) - if @mpw.encrypt - sync - puts "#{I18n.t('form.add.valid')}".green - else - puts "#{I18n.t('display.error')} #12: #{@mpw.error_msg}".red - end - else - puts "#{I18n.t('display.error')} #13: #{item.error_msg}".red - end - end - - # Update an item - # @args: id -> the item's id - def update(id) - item = @mpw.search_by_id(id) - - if not item.nil? - options = {} - - puts I18n.t('form.update.title') - puts '--------------------' - options[:name] = ask(I18n.t('form.update.name' , name: item.name)).to_s - options[:group] = ask(I18n.t('form.update.group' , group: item.group)).to_s - options[:host] = ask(I18n.t('form.update.server' , server: item.host)).to_s - options[:protocol] = ask(I18n.t('form.update.protocol', protocol: item.protocol)).to_s - options[:user] = ask(I18n.t('form.update.login' , login: item.user)).to_s - options[:password] = ask(I18n.t('form.update.password')).to_s - options[:port] = ask(I18n.t('form.update.port' , port: item.port)).to_s - options[:comment] = ask(I18n.t('form.update.comment' , comment: item.comment)).to_s - - options.delete_if { |k,v| v.empty? } - - if item.update(options) - if @mpw.encrypt - sync - puts "#{I18n.t('form.update.valid')}".green - else - puts "#{I18n.t('display.error')} #14: #{@mpw.error_msg}".red - end - else - puts "#{I18n.t('display.error')} #15: #{item.error_msg}".red - end - else - puts I18n.t('display.nothing') - end - end - - # Remove an item - # @args: id -> the item's id - # force -> no resquest a validation - def delete(id, force=false) - if not force - item = @mpw.search_by_id(id) - - if not item.nil? - display_item(item) - - confirm = ask("#{I18n.t('form.delete.ask', id: id)} (y/N) ").to_s - if confirm =~ /^(y|yes|YES|Yes|Y)$/ - force = true - end - else - puts I18n.t('display.nothing') - end - end - - if force - item.delete - - if @mpw.encrypt - sync - puts "#{I18n.t('form.delete.valid', id: id)}".green - else - puts "#{I18n.t('display.error')} #16: #{@mpw.error_msg}".red - end - end - end - - # Export the items in a CSV file - # @args: file -> the destination file - def export(file, type=:yaml) - if @mpw.export(file, type) - puts "#{I18n.t('export.valid', file)}".green - else - puts "#{I18n.t('display.error')} #17: #{@mpw.error_msg}".red - end - end - - # Import items from a CSV file - # @args: file -> the import file - # force -> no resquest a validation - def import(file, type=:yaml, force=false) - - if not force - result = @mpw.import_preview(file, type) - if result.is_a?(Array) and not result.empty? - result.each do |r| - display_item(r) - end - - confirm = ask("#{I18n.t('form.import.ask', file: file)} (y/N) ").to_s - if confirm =~ /^(y|yes|YES|Yes|Y)$/ - force = true - end - else - puts I18n.t('form.import.not_valid') - end - end - - if force - if @mpw.import(file, type) and @mpw.encrypt - sync - puts "#{I18n.t('form.import.valid')}".green - else - puts "#{I18n.t('display.error')} #18: #{@mpw.error_msg}".red - end - end - end - -end diff --git a/lib/mpw/ui/clissh.rb b/lib/mpw/ui/clissh.rb deleted file mode 100644 index 0bf7bb0..0000000 --- a/lib/mpw/ui/clissh.rb +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/ruby -# author: nishiki -# mail: nishiki@yaegashi.fr -# info: a simple script who manage your passwords - -require 'mpw/ui/cli' - -class CliSSH < Cli - - attr_accessor :server, :port, :login - - # Connect to SSH - # args: search -> string to search - def ssh(search) - result = @mpw.list(search: search, protocol: 'ssh') - - if result.length > 0 - result.each do |item| - server = @server.nil? ? item.host : @server - port = @port.nil? ? item.port : @port - login = @login.nil? ? item.user : @login - - passwd = item.password - - if port.nil? and port.empty? - port = 22 - end - - puts "#{I18n.t('ssh.display.connect')} ssh #{login}@#{server} -p #{port}" - if passwd.empty? - system("ssh #{login}@#{server} -p #{port}") - else - system("sshpass -p '#{passwd}' ssh #{login}@#{server} -p #{port}") - end - end - - else - puts I18n.t('ssh.display.nothing') - end - end -end - diff --git a/mpw.gemspec b/mpw.gemspec index aaf249a..abcb375 100644 --- a/mpw.gemspec +++ b/mpw.gemspec @@ -1,19 +1,28 @@ -# coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) Gem::Specification.new do |spec| spec.name = 'mpw' spec.version = File.open('VERSION').read - spec.authors = ['nishiki'] - spec.email = ['gems@yae.im'] - spec.summary = 'Manage your password' - spec.description = 'Save and read your password with gpg' + spec.authors = ['Adrien Waksberg'] + spec.email = ['mpw@yae.im'] + spec.summary = 'MPW is a software to crypt and manage your passwords' + spec.description = 'Manage your passwords in all security with MPW, we use GPG to encrypt your passwords' spec.homepage = 'https://github.com/nishiki/manage-password' - spec.license = 'GPL' + spec.license = 'GPL-2.0' - spec.files = `git ls-files -z`.split("\x0") - spec.executables = ['mpw', 'mpw-server', 'mpw-ssh'] + spec.files = %x(git ls-files -z).split("\x0") + spec.executables = ['mpw'] spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ['lib'] + + spec.required_ruby_version = '>= 2.1' + + spec.add_dependency 'i18n', '~> 0.9', '>= 0.9.1' + spec.add_dependency 'gpgme', '~> 2.0', '>= 2.0.14' + spec.add_dependency 'highline', '~> 1.7', '>= 1.7.8' + spec.add_dependency 'locale', '~> 2.1', '>= 2.1.2' + spec.add_dependency 'colorize', '~> 0.8', '>= 0.8.1' + spec.add_dependency 'clipboard', '~> 1.1', '>= 1.1.1' + spec.add_dependency 'rotp', '~> 3.3', '>= 3.3.0' end diff --git a/templates/add_form.erb b/templates/add_form.erb new file mode 100644 index 0000000..e2aea20 --- /dev/null +++ b/templates/add_form.erb @@ -0,0 +1,13 @@ +--- +# <%= I18n.t('form.add_item.url') %> +url: <%= options[:url] %> +# <%= I18n.t('form.add_item.login') %> +user: <%= options[:user] %> +# <%= I18n.t('form.add_item.group') %> +group: <%= options[:group] %><% unless password %> +# <%= I18n.t('form.add_item.password') %> +password:<% end %> +# <%= I18n.t('form.add_item.comment') %> +comment: <%= options[:comment] %> +# <%= I18n.t('form.add_item.otp_key') %> +otp_key: <%= options[:otp] %> diff --git a/templates/update_form.erb b/templates/update_form.erb new file mode 100644 index 0000000..eafeb1b --- /dev/null +++ b/templates/update_form.erb @@ -0,0 +1,13 @@ +--- +# <%= I18n.t('form.update_item.url') %> +host: <% if options[:url] %><%= options[:url] %><% else %><%= item.url %><% end %> +# <%= I18n.t('form.update_item.login') %> +user: <% if options[:user] %><%= options[:user] %><% else %><%= item.user %><% end %><% unless password %> +# <%= I18n.t('form.update_item.password') %> +password: <% end %> +# <%= I18n.t('form.update_item.group') %> +group: <% if options[:group] %><%= options[:group] %><% else %><%= item.group %><% end %> +# <%= I18n.t('form.update_item.otp_key') %> +otp_key: <% if options[:otp_key] %><%= options[:otp_key] %><% end %> +# <%= I18n.t('form.update_item.comment') %> +comment: <% if options[:comment] %><%= options[:comment] %><% else %><%= item.comment %><% end %> diff --git a/test/files/fixtures-import.yml b/test/files/fixtures-import.yml new file mode 100644 index 0000000..8cb1ff4 --- /dev/null +++ b/test/files/fixtures-import.yml @@ -0,0 +1,16 @@ +--- +1: + url: https://fric.com + user: 230403 + group: Bank + password: 5XdiTQOubRDw9B0aJoMlcEyL + otp_key: 330223432 + comment: I love my bank +2: + url: https://assurance.com:443 + user: user_2132 + host: assurance.com + group: Assurance + password: DMyK6B3v4bWO52VzU7aTHIem + otp_key: + comment: diff --git a/test/files/fixtures.yml b/test/files/fixtures.yml index 73eacf7..d8927cb 100644 --- a/test/files/fixtures.yml +++ b/test/files/fixtures.yml @@ -1,32 +1,31 @@ -add_new: - name: 'test_name' - group: 'test_group' - host: 'test_host' - protocol: 'test_protocol' - user: 'test_user' - password: 'test_password' - port: '42' - comment: 'test_comment' +add: + url: 'https://example.com:8080' + group: 'Bank' + host: 'example.com' + protocol: 'https' + user: 'admin' + password: 'VmfnCN6pPIqgRIbc' + port: '8080' + comment: 'the website' -add_existing: +import: id: 'TEST-ID-XXXXX' - name: 'test_name_existing' - group: 'test_group_existing' - host: 'test_host_existing' - protocol: 'test_protocol_existing' - user: 'test_user_existing' - password: 'test_password_existing' - port: '44' - comment: 'test_comment_existing' + url: 'https://gogole.com:8081/toto' + group: 'Cloud' + host: 'gogole.com' + protocol: 'https' + user: 'gg-2304' + password: 'TITl0kV9CDDa9sVK' + port: '8081' + comment: 'My little servers' created: 1386752948 update: - name: 'test_name_update' - group: 'test_group_update' - host: 'test_host_update' - protocol: 'test_protocol_update' - user: 'test_user_update' - password: 'test_password_update' - port: '43' - comment: 'test_comment_update' - + url: 'ssh://example2.com:2222' + group: 'Assurance' + host: 'example2.com' + protocol: 'ssh' + user: 'root' + password: 'kbSrbv4WlMaVxaZ7' + port: '2222' + comment: 'i love ssh' diff --git a/test/files/import-gorilla.txt b/test/files/import-gorilla.txt new file mode 100644 index 0000000..a5fc604 --- /dev/null +++ b/test/files/import-gorilla.txt @@ -0,0 +1,4 @@ +uuid,group,title,url,user,password,notes +49627979-e393-48c4-49ca-1cf66603238e,Bank,Fric,http://fric.com,12345,secret,money money +49627979-e393-48c4-49ca-1cf66603238f,,My little server,server.com,secret2, +49627979-e393-48c4-49ca-1cf66603238g,Cloud,,ssh://fric.com:4333,username,secret,bastion diff --git a/test/files/import-keepass.txt b/test/files/import-keepass.txt new file mode 100644 index 0000000..61ce7ad --- /dev/null +++ b/test/files/import-keepass.txt @@ -0,0 +1,3 @@ +"Group","Title","Username","Password","URL","Notes" +"Racine","Bank","123456","ywcExJW8qmBVTSyi","http://bank.com/login","My little bank" +"Racine/Cloud","GAFAM","wesh","superpassword","localhost.local","" diff --git a/test/files/import-mpw_old.txt b/test/files/import-mpw_old.txt new file mode 100644 index 0000000..fd162aa --- /dev/null +++ b/test/files/import-mpw_old.txt @@ -0,0 +1,35 @@ +--- +1: + host: fric.com + user: 12345 + group: Bank + password: secret + protocol: http + port: + otp_key: + comment: Fric money money + last_edit: 1487623641 + created: 1485729356 +2: + host: server.com + user: sercret2 + group: + password: + protocol: + port: 4222 + otp_key: + comment: My little server + last_edit: 1487623641 + created: 1485729356 +3: + host: fric.com + user: username + group: Cloud + password: + protocol: ssh + port: 4333 + otp_key: + comment: bastion + last_edit: 1487623641 + created: 1485729356 + diff --git a/test/files/test_import.csv b/test/files/test_import.csv deleted file mode 100644 index 74c1844..0000000 --- a/test/files/test_import.csv +++ /dev/null @@ -1,3 +0,0 @@ -name,group,protocol,host,user,password,port,comment -test_name,test_group,test_protocol,test_host,test_user,test_password,42,test_comment -test_name_update,test_group_update,test_protocol_update,test_host_update,test_user_update,test_password_update,43,test_comment_update diff --git a/test/files/test_import.yml b/test/files/test_import.yml deleted file mode 100644 index 889a264..0000000 --- a/test/files/test_import.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -XWas7vpy0HerhOYd: - id: XWas7vpy0HerhOYd - name: test_name - group: test_group - host: test_host - protocol: test_protocol - user: test_user - password: test_password - port: 42 - comment: test_comment - date: 1419858983 -D7URyJENLa91jt0b: - id: D7URyJENLa91jt0b - name: test_name_update - group: test_group_update - host: test_host_update - protocol: test_protocol_update - user: test_user_update - password: test_password_update - port: 43 - comment: test_comment_update - date: 1419858983 diff --git a/test/init.rb b/test/init.rb new file mode 100644 index 0000000..08ebbb8 --- /dev/null +++ b/test/init.rb @@ -0,0 +1,21 @@ +require 'fileutils' +require 'gpgme' + +FileUtils.rm_rf("#{Dir.home}/.config/mpw") +FileUtils.rm_rf("#{Dir.home}/.gnupg") + +param = '' +param << '<GnupgKeyParms format="internal">' + "\n" +param << "Key-Type: RSA\n" +param << "Key-Length: 512\n" +param << "Subkey-Type: ELG-E\n" +param << "Subkey-Length: 512\n" +param << "Name-Real: test\n" +param << "Name-Comment: test\n" +param << "Name-Email: test2@example.com\n" +param << "Expire-Date: 0\n" +param << "Passphrase: password\n" +param << "</GnupgKeyParms>\n" + +ctx = GPGME::Ctx.new +ctx.genkey(param, nil, nil) diff --git a/test/test_cli.rb b/test/test_cli.rb new file mode 100644 index 0000000..8bae4cc --- /dev/null +++ b/test/test_cli.rb @@ -0,0 +1,256 @@ +require 'i18n' +require 'test/unit' + +class TestConfig < Test::Unit::TestCase + def setup + if defined?(I18n.enforce_available_locales) + I18n.enforce_available_locales = true + end + + I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks) + I18n.load_path = ["#{File.expand_path('../../i18n', __FILE__)}/en.yml"] + I18n.locale = :en + + @password = 'password' + @fixtures = YAML.load_file('./test/files/fixtures.yml') + @gpg_key = 'test@example.com' + end + + def test_00_init_config + output = %x( + echo "#{@password}\n#{@password}" | mpw config \ + --init #{@gpg_key} \ + 2>/dev/null + ) + assert_match(I18n.t('form.setup_config.valid'), output) + assert_match(I18n.t('form.setup_gpg_key.valid'), output) + end + + def test_01_add_item + data = @fixtures['add'] + + output = %x( + echo #{@password} | mpw add \ + --url #{data['url']} \ + --user #{data['user']} \ + --comment '#{data['comment']}' \ + --group #{data['group']} \ + --random \ + 2>/dev/null + ) + assert_match(I18n.t('form.add_item.valid'), output) + + output = %x(echo #{@password} | mpw list 2>/dev/null) + assert_match(%r{#{data['protocol']}://.+#{data['host']}.+:#{data['port']}}, output) + assert_match(data['user'], output) + assert_match(data['comment'], output) + assert_match(data['group'], output) + end + + def test_02_search + data = @fixtures['add'] + + output = %x(echo #{@password} | mpw list --group #{data['group']} 2>/dev/null) + assert_match(%r{#{data['protocol']}://.+#{data['host']}.+:#{data['port']}}, output) + + output = %x(echo #{@password} | mpw list --pattern #{data['host']} 2>/dev/null) + assert_match(%r{#{data['protocol']}://.+#{data['host']}.+:#{data['port']}}, output) + + output = %x(echo #{@password} | mpw list --pattern #{data['comment']} 2>/dev/null) + assert_match(%r{#{data['protocol']}://.+#{data['host']}.+:#{data['port']}}, output) + + output = %x(echo #{@password} | mpw list --group R1Pmfbp626TFpjlr 2>/dev/null) + assert_match(I18n.t('display.nothing'), output) + + output = %x(echo #{@password} | mpw list --pattern h1IfnKqamaGM9oEX 2>/dev/null) + assert_match(I18n.t('display.nothing'), output) + end + + def test_03_update_item + data = @fixtures['update'] + + output = %x( + echo #{@password} | mpw update \ + -p #{@fixtures['add']['host']} \ + --url #{data['url']} \ + --user #{data['user']} \ + --comment '#{data['comment']}' \ + --new-group #{data['group']} \ + 2>/dev/null + ) + assert_match(I18n.t('form.update_item.valid'), output) + + output = %x(echo #{@password} | mpw list 2>/dev/null) + assert_match(%r{#{data['protocol']}://.+#{data['host']}.+:#{data['port']}}, output) + assert_match(data['user'], output) + assert_match(data['comment'], output) + assert_match(data['group'], output) + end + + def test_04_delete_item + output = %x( + echo "#{@password}\ny" | mpw delete \ + -p #{@fixtures['update']['host']} \ + 2>/dev/null + ) + assert_match(I18n.t('form.delete_item.valid'), output) + + output = %x(echo #{@password} | mpw list 2>/dev/null) + assert_match(I18n.t('display.nothing'), output) + end + + def test_05_import_export + file_import = './test/files/fixtures-import.yml' + file_export = '/tmp/test-mpw.yml' + + output = %x(echo #{@password} | mpw import --file #{file_import} 2>/dev/null) + assert_match(I18n.t('form.import.valid', file: file_import), output) + + output = %x(echo #{@password} | mpw export --file #{file_export} 2>/dev/null) + assert_match(I18n.t('form.export.valid', file: file_export), output) + assert(File.exist?(file_export)) + assert_equal(YAML.load_file(file_export).length, 2) + + YAML.load_file(file_import).each_value do |import| + error = true + + YAML.load_file(file_export).each_value do |export| + next if import['url'] != export['url'] + + %w[user group password protocol port otp_key comment].each do |key| + assert_equal(import[key].to_s, export[key].to_s) + end + + error = false + break + end + + assert(!error) + end + end + + def test_06_copy + data = YAML.load_file('./test/files/fixtures-import.yml')[2] + + output = %x( + echo "#{@password}\np\nq" | mpw copy \ + --disable-clipboard \ + -p #{data['host']} \ + 2>/dev/null + ) + assert_match(data['password'], output) + end + + def test_07_setup_wallet + gpg_key = 'test2@example.com' + + output = %x(echo #{@password} | mpw wallet --add-gpg-key #{gpg_key} 2>/dev/null) + assert_match(I18n.t('form.add_key.valid'), output) + + output = %x(echo #{@password} | mpw wallet --list-keys 2>/dev/null) + assert_match("| #{@gpg_key}", output) + assert_match("| #{gpg_key}", output) + + output = %x(echo #{@password} | mpw wallet --delete-gpg-key #{gpg_key} 2>/dev/null) + assert_match(I18n.t('form.delete_key.valid'), output) + + output = %x(echo #{@password} | mpw wallet --list-keys 2>/dev/null) + assert_match("| #{@gpg_key}", output) + assert_no_match(/\| #{gpg_key}/, output) + + output = %x(mpw wallet) + assert_match('| default', output) + + output = %x(mpw wallet --path '.') + assert_match(I18n.t('form.set_wallet_path.valid'), output) + + output = %x(mpw config) + assert_match(%r{path_wallet_default.+\| #{Dir.pwd}/default.mpw}, output) + assert(File.exist?("#{Dir.pwd}/default.mpw")) + + output = %x(mpw wallet) + assert_match('default', output) + + output = %x(mpw wallet --default-path) + assert_match(I18n.t('form.set_wallet_path.valid'), output) + + output = %x(mpw config) + assert_no_match(/path_wallet_default/, output) + end + + def test_08_setup_config + gpg_key = 'test2@example.com' + gpg_exe = '/usr/bin/gpg2' + wallet_dir = '/tmp' + length = 24 + wallet = 'work' + + output = %x( + mpw config \ + --gpg-exe #{gpg_exe} \ + --key #{gpg_key} \ + --enable-pinmode \ + --disable-alpha \ + --disable-special-chars \ + --disable-numeric \ + --length #{length} \ + --wallet-dir #{wallet_dir} \ + --default-wallet #{wallet} + ) + assert_match(I18n.t('form.set_config.valid'), output) + + output = %x(mpw config) + assert_match(/gpg_key.+\| #{gpg_key}/, output) + assert_match(/gpg_exe.+\| #{gpg_exe}/, output) + assert_match(/pinmode.+\| true/, output) + assert_match(/default_wallet.+\| #{wallet}/, output) + assert_match(/wallet_dir.+\| #{wallet_dir}/, output) + assert_match(/password_length.+\| #{length}/, output) + %w[numeric alpha special].each do |k| + assert_match(/password_#{k}.+\| false/, output) + end + + output = %x( + mpw config \ + --gpg-exe '' \ + --key #{@gpg_key} \ + --alpha \ + --special-chars \ + --numeric \ + --disable-pinmode + ) + assert_match(I18n.t('form.set_config.valid'), output) + + output = %x(mpw config) + assert_match(/gpg_key.+\| #{@gpg_key}/, output) + assert_match(/pinmode.+\| false/, output) + %w[numeric alpha special].each do |k| + assert_match(/password_#{k}.+\| true/, output) + end + end + + def test_09_generate_password + length = 24 + + output = %x( + mpw genpwd \ + --length #{length} \ + --alpha + ) + assert_match(/[a-zA-Z]{#{length}}/, output) + + output = %x( + mpw genpwd \ + --length #{length} \ + --numeric + ) + assert_match(/[0-9]{#{length}}/, output) + + output = %x( + mpw genpwd \ + --length #{length} \ + --special-chars + ) + assert_no_match(/[a-zA-Z0-9]/, output) + end +end diff --git a/test/test_config.rb b/test/test_config.rb new file mode 100644 index 0000000..2b88b54 --- /dev/null +++ b/test/test_config.rb @@ -0,0 +1,79 @@ +require 'mpw/config' +require 'test/unit' +require 'locale' +require 'i18n' + +class TestConfig < Test::Unit::TestCase + def setup + lang = Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1] + + if defined?(I18n.enforce_available_locales) + I18n.enforce_available_locales = true + end + + I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks) + I18n.load_path = Dir["#{File.expand_path('../../i18n', __FILE__)}/*.yml"] + I18n.default_locale = :en + I18n.locale = lang.to_sym + end + + def test_00_config + data = { + gpg_key: 'test@example.com', + lang: 'en', + wallet_dir: '/tmp/test', + gpg_exe: '' + } + + @config = MPW::Config.new + @config.setup(data) + @config.load_config + + data.each do |k, v| + assert_equal(v, @config.send(k)) + end + + @config.setup_gpg_key('password', 'test@example.com', 2048) + assert(@config.check_gpg_key?) + end + + def test_01_password + data = { + pwd_alpha: false, + pwd_numeric: false, + pwd_special: true, + pwd_length: 32 + } + + @config = MPW::Config.new + @config.load_config + + assert_equal(@config.password[:length], 16) + assert(@config.password[:alpha]) + assert(@config.password[:numeric]) + assert(!@config.password[:special]) + + @config.setup(data) + @config.load_config + + assert_equal(@config.password[:length], data[:pwd_length]) + assert(!@config.password[:alpha]) + assert(!@config.password[:numeric]) + assert(@config.password[:special]) + end + + def test_02_wallet_paths + new_path = '/tmp/mpw-test' + + @config = MPW::Config.new + @config.load_config + + assert(!@config.wallet_paths['default']) + + @config.set_wallet_path(new_path, 'default') + assert_equal(@config.wallet_paths['default'], new_path) + + @config.set_wallet_path('default', 'default') + assert(!@config.wallet_paths['default']) + end +end diff --git a/test/test_import.rb b/test/test_import.rb new file mode 100644 index 0000000..f1a8727 --- /dev/null +++ b/test/test_import.rb @@ -0,0 +1,79 @@ +require 'i18n' +require 'test/unit' + +class TestImport < Test::Unit::TestCase + def setup + if defined?(I18n.enforce_available_locales) + I18n.enforce_available_locales = true + end + + I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks) + I18n.load_path = ["#{File.expand_path('../../i18n', __FILE__)}/en.yml"] + I18n.locale = :en + + @password = 'password' + end + + def test_00_import_mpw_old + file = './test/files/import-mpw_old.txt' + format = 'mpw_old' + + output = %x( + mpw import \ + --file #{file} \ + --format #{format} \ + --wallet #{format} + ) + assert_match(I18n.t('form.import.valid'), output) + + output = %x(echo #{@password} | mpw list --group Bank --wallet #{format}) + assert_match(%r{http://.*fric\.com.*12345.*Fric money money}, output) + + output = %x(echo #{@password} | mpw list --group Cloud --wallet #{format}) + assert_match(%r{ssh://.*fric\.com.*:4333.*username.*bastion}, output) + + output = %x(echo #{@password} | mpw list --wallet #{format}) + assert_match(/server\.com.*My little server/, output) + end + + def test_01_import_gorilla + file = './test/files/import-gorilla.txt' + format = 'gorilla' + + output = %x( + mpw import \ + --file #{file} \ + --format #{format} \ + --wallet #{format} + ) + assert_match(I18n.t('form.import.valid'), output) + + output = %x(echo #{@password} | mpw list --group Bank --wallet #{format}) + assert_match(%r{http://.*fric\.com.*12345.*Fric money money}, output) + + output = %x(echo #{@password} | mpw list --group Cloud --wallet #{format}) + assert_match(%r{ssh://.*fric\.com.*:4333.*username.*bastion}, output) + + output = %x(echo #{@password} | mpw list --wallet #{format}) + assert_match(/server\.com.*My little server/, output) + end + + def test_02_import_keepass + file = './test/files/import-keepass.txt' + format = 'keepass' + + output = %x( + mpw import \ + --file #{file} \ + --format #{format} \ + --wallet #{format} + ) + assert_match(I18n.t('form.import.valid'), output) + + output = %x(echo #{@password} | mpw list --group 'Racine/Cloud' --wallet #{format}) + assert_match(/localhost\.local.*wesh.*GAFAM/, output) + + output = %x(echo #{@password} | mpw list --wallet #{format}) + assert_match(%r{http://.*bank\.com.*123456.*Bank My little bank}, output) + end +end diff --git a/test/test_item.rb b/test/test_item.rb index 923d7af..97503c0 100644 --- a/test/test_item.rb +++ b/test/test_item.rb @@ -1,218 +1,165 @@ -#!/usr/bin/ruby - -require_relative '../lib/Item' +require 'mpw/item' require 'test/unit' require 'yaml' - + class TestItem < Test::Unit::TestCase - def setup - @fixture_file = 'files/fixtures.yml' - @fixtures = YAML.load_file(@fixture_file) - - if defined?(I18n.enforce_available_locales) - I18n.enforce_available_locales = false - end + def setup + if defined?(I18n.enforce_available_locales) + I18n.enforce_available_locales = false + end - I18n.load_path = Dir['../i18n/cli/*.yml'] - I18n.default_locale = :en + I18n.load_path = Dir['./i18n/cli/*.yml'] + I18n.default_locale = :en + @fixtures = YAML.load_file('./test/files/fixtures.yml') + end - puts - end + def test_00_add_without_name + assert_raise(RuntimeError) { MPW::Item.new } + end - def test_00_add_without_name - assert_raise(RuntimeError){MPW::Item.new} - end + def test_01_add + data = { + group: @fixtures['add']['group'], + user: @fixtures['add']['user'], + url: @fixtures['add']['url'], + comment: @fixtures['add']['comment'] + } - def test_01_add_new - data = {name: @fixtures['add_new']['name'], - group: @fixtures['add_new']['group'], - host: @fixtures['add_new']['host'], - protocol: @fixtures['add_new']['protocol'], - user: @fixtures['add_new']['user'], - password: @fixtures['add_new']['password'], - port: @fixtures['add_new']['port'], - comment: @fixtures['add_new']['comment'], - } - - item = MPW::Item.new(data) + item = MPW::Item.new(data) - assert(!item.nil?) - assert(!item.empty?) + assert(!item.nil?) + assert(!item.empty?) - assert_equal(@fixtures['add_new']['name'], item.name) - assert_equal(@fixtures['add_new']['group'], item.group) - assert_equal(@fixtures['add_new']['host'], item.host) - assert_equal(@fixtures['add_new']['protocol'], item.protocol) - assert_equal(@fixtures['add_new']['user'], item.user) - assert_equal(@fixtures['add_new']['password'], item.password) - assert_equal(@fixtures['add_new']['port'].to_i, item.port) - assert_equal(@fixtures['add_new']['comment'], item.comment) - end + assert_equal(@fixtures['add']['url'], item.url) + assert_equal(@fixtures['add']['group'], item.group) + assert_equal(@fixtures['add']['host'], item.host) + assert_equal(@fixtures['add']['protocol'], item.protocol) + assert_equal(@fixtures['add']['user'], item.user) + assert_equal(@fixtures['add']['port'].to_i, item.port) + assert_equal(@fixtures['add']['comment'], item.comment) + end - def test_02_add_existing - data = {id: @fixtures['add_existing']['id'], - name: @fixtures['add_existing']['name'], - group: @fixtures['add_existing']['group'], - host: @fixtures['add_existing']['host'], - protocol: @fixtures['add_existing']['protocol'], - user: @fixtures['add_existing']['user'], - password: @fixtures['add_existing']['password'], - port: @fixtures['add_existing']['port'], - comment: @fixtures['add_existing']['comment'], - created: @fixtures['add_existing']['created'], - } + def test_02_import + data = { + id: @fixtures['import']['id'], + group: @fixtures['import']['group'], + user: @fixtures['import']['user'], + url: @fixtures['import']['url'], + comment: @fixtures['import']['comment'], + created: @fixtures['import']['created'] + } - item = MPW::Item.new(data) + item = MPW::Item.new(data) - assert(!item.nil?) - assert(!item.empty?) + assert(!item.nil?) + assert(!item.empty?) - assert_equal(@fixtures['add_existing']['id'], item.id) - assert_equal(@fixtures['add_existing']['name'], item.name) - assert_equal(@fixtures['add_existing']['group'], item.group) - assert_equal(@fixtures['add_existing']['host'], item.host) - assert_equal(@fixtures['add_existing']['protocol'], item.protocol) - assert_equal(@fixtures['add_existing']['user'], item.user) - assert_equal(@fixtures['add_existing']['password'], item.password) - assert_equal(@fixtures['add_existing']['port'].to_i, item.port) - assert_equal(@fixtures['add_existing']['comment'], item.comment) - assert_equal(@fixtures['add_existing']['created'], item.created) - end + assert_equal(@fixtures['import']['id'], item.id) + assert_equal(@fixtures['import']['url'], item.url) + assert_equal(@fixtures['import']['group'], item.group) + assert_equal(@fixtures['import']['host'], item.host) + assert_equal(@fixtures['import']['protocol'], item.protocol) + assert_equal(@fixtures['import']['user'], item.user) + assert_equal(@fixtures['import']['port'].to_i, item.port) + assert_equal(@fixtures['import']['comment'], item.comment) + assert_equal(@fixtures['import']['created'], item.created) + end - def test_03_update - data = {name: @fixtures['add_new']['name'], - group: @fixtures['add_new']['group'], - host: @fixtures['add_new']['host'], - protocol: @fixtures['add_new']['protocol'], - user: @fixtures['add_new']['user'], - password: @fixtures['add_new']['password'], - port: @fixtures['add_new']['port'], - comment: @fixtures['add_new']['comment'], - } - - item = MPW::Item.new(data) + def test_03_update + data = { + group: @fixtures['add']['group'], + user: @fixtures['add']['user'], + url: @fixtures['add']['url'], + comment: @fixtures['add']['comment'] + } - assert(!item.nil?) - assert(!item.empty?) + item = MPW::Item.new(data) - created = item.created - last_edit = item.last_edit + assert(!item.nil?) + assert(!item.empty?) - data = {name: @fixtures['update']['name'], - group: @fixtures['update']['group'], - host: @fixtures['update']['host'], - protocol: @fixtures['update']['protocol'], - user: @fixtures['update']['user'], - password: @fixtures['update']['password'], - port: @fixtures['update']['port'], - comment: @fixtures['update']['comment'], - } - - sleep(1) - assert(item.update(data)) + created = item.created + last_edit = item.last_edit - assert(!item.empty?) + data = { + group: @fixtures['update']['group'], + user: @fixtures['update']['user'], + url: @fixtures['update']['url'], + comment: @fixtures['update']['comment'] + } - assert_equal(@fixtures['update']['name'], item.name) - assert_equal(@fixtures['update']['group'], item.group) - assert_equal(@fixtures['update']['host'], item.host) - assert_equal(@fixtures['update']['protocol'], item.protocol) - assert_equal(@fixtures['update']['user'], item.user) - assert_equal(@fixtures['update']['password'], item.password) - assert_equal(@fixtures['update']['port'].to_i, item.port) - assert_equal(@fixtures['update']['comment'], item.comment) + sleep(1) + assert(item.update(data)) - assert_equal(created, item.created) - assert_not_equal(last_edit, item.last_edit) - end + assert(!item.empty?) - def test_04_update_with_empty_name - data = {name: @fixtures['add_new']['name'], - group: @fixtures['add_new']['group'], - host: @fixtures['add_new']['host'], - protocol: @fixtures['add_new']['protocol'], - user: @fixtures['add_new']['user'], - password: @fixtures['add_new']['password'], - port: @fixtures['add_new']['port'], - comment: @fixtures['add_new']['comment'], - } - - item = MPW::Item.new(data) + assert_equal(@fixtures['update']['url'], item.url) + assert_equal(@fixtures['update']['group'], item.group) + assert_equal(@fixtures['update']['host'], item.host) + assert_equal(@fixtures['update']['protocol'], item.protocol) + assert_equal(@fixtures['update']['user'], item.user) + assert_equal(@fixtures['update']['port'].to_i, item.port) + assert_equal(@fixtures['update']['comment'], item.comment) - assert(!item.nil?) - assert(!item.empty?) + assert_equal(created, item.created) + assert_not_equal(last_edit, item.last_edit) + end - last_edit = item.last_edit + def test_05_update_one_element + data = { + group: @fixtures['add']['group'], + user: @fixtures['add']['user'], + url: @fixtures['add']['url'], + comment: @fixtures['add']['comment'] + } - sleep(1) - assert(!item.update({name: ''})) + item = MPW::Item.new(data) - assert_equal(last_edit, item.last_edit) - end + assert(!item.nil?) + assert(!item.empty?) - def test_05_update_one_element - data = {name: @fixtures['add_new']['name'], - group: @fixtures['add_new']['group'], - host: @fixtures['add_new']['host'], - protocol: @fixtures['add_new']['protocol'], - user: @fixtures['add_new']['user'], - password: @fixtures['add_new']['password'], - port: @fixtures['add_new']['port'], - comment: @fixtures['add_new']['comment'], - } - - item = MPW::Item.new(data) + last_edit = item.last_edit - assert(!item.nil?) - assert(!item.empty?) + sleep(1) + item.update(comment: @fixtures['update']['comment']) - last_edit = item.last_edit + assert_equal(@fixtures['add']['url'], item.url) + assert_equal(@fixtures['add']['group'], item.group) + assert_equal(@fixtures['add']['host'], item.host) + assert_equal(@fixtures['add']['protocol'], item.protocol) + assert_equal(@fixtures['add']['user'], item.user) + assert_equal(@fixtures['add']['port'].to_i, item.port) + assert_equal(@fixtures['update']['comment'], item.comment) - sleep(1) - assert(item.update({comment: @fixtures['update']['comment']})) + assert_not_equal(last_edit, item.last_edit) + end - assert_equal(@fixtures['add_new']['name'], item.name) - assert_equal(@fixtures['add_new']['group'], item.group) - assert_equal(@fixtures['add_new']['host'], item.host) - assert_equal(@fixtures['add_new']['protocol'], item.protocol) - assert_equal(@fixtures['add_new']['user'], item.user) - assert_equal(@fixtures['add_new']['password'], item.password) - assert_equal(@fixtures['add_new']['port'].to_i, item.port) - assert_equal(@fixtures['update']['comment'], item.comment) - - assert_not_equal(last_edit, item.last_edit) - end + def test_05_delete + data = { + group: @fixtures['add']['group'], + user: @fixtures['add']['user'], + url: @fixtures['add']['url'], + comment: @fixtures['add']['comment'] + } - def test_05_delete - data = {name: @fixtures['add_new']['name'], - group: @fixtures['add_new']['group'], - host: @fixtures['add_new']['host'], - protocol: @fixtures['add_new']['protocol'], - user: @fixtures['add_new']['user'], - password: @fixtures['add_new']['password'], - port: @fixtures['add_new']['port'], - comment: @fixtures['add_new']['comment'], - } - - item = MPW::Item.new(data) + item = MPW::Item.new(data) - assert(!item.nil?) - assert(!item.empty?) + assert(!item.nil?) + assert(!item.empty?) - assert(item.delete) - assert(!item.nil?) - assert(item.empty?) + item.delete + assert(!item.nil?) + assert(item.empty?) - assert_equal(nil, item.id) - assert_equal(nil, item.name) - assert_equal(nil, item.group) - assert_equal(nil, item.host) - assert_equal(nil, item.protocol) - assert_equal(nil, item.user) - assert_equal(nil, item.password) - assert_equal(nil, item.port) - assert_equal(nil, item.comment) - assert_equal(nil, item.created) - end -end + assert_equal(nil, item.id) + assert_equal(nil, item.url) + assert_equal(nil, item.group) + assert_equal(nil, item.host) + assert_equal(nil, item.protocol) + assert_equal(nil, item.user) + assert_equal(nil, item.port) + assert_equal(nil, item.comment) + assert_equal(nil, item.created) + end +end diff --git a/test/test_mpw.rb b/test/test_mpw.rb index 899b366..8fc544e 100644 --- a/test/test_mpw.rb +++ b/test/test_mpw.rb @@ -1,191 +1,130 @@ -#!/usr/bin/ruby - -require_relative '../lib/MPW' -require_relative '../lib/Item' +require 'mpw/mpw' +require 'mpw/item' require 'test/unit' require 'yaml' require 'csv' - + class TestMPW < Test::Unit::TestCase - def setup - fixture_file = 'files/fixtures.yml' + def setup + wallet_file = 'default.gpg' + key = 'test@example.com' + password = 'password' - file_gpg = 'test.gpg' - key = ENV['MPW_TEST_KEY'] + if defined?(I18n.enforce_available_locales) + I18n.enforce_available_locales = false + end - puts + @mpw = MPW::MPW.new(key, wallet_file, password) + @fixtures = YAML.load_file('./test/files/fixtures.yml') + end - if defined?(I18n.enforce_available_locales) - I18n.enforce_available_locales = false - end + def test_00_decrypt_empty_file + @mpw.read_data + assert_equal(0, @mpw.list.length) + end - File.delete(file_gpg) if File.exist?(file_gpg) + def test_01_encrypt_empty_file + @mpw.read_data + @mpw.write_data + end - @mpw = MPW::MPW.new(file_gpg, key) - @fixtures = YAML.load_file(fixture_file) - end - - def test_01_import_yaml - import_file = 'files/test_import.yml' + def test_02_add_item + data = { + group: @fixtures['add']['group'], + user: @fixtures['add']['user'], + url: @fixtures['add']['url'], + comment: @fixtures['add']['comment'] + } - assert(@mpw.import(import_file, :yaml)) - assert_equal(2, @mpw.list.length) + item = MPW::Item.new(data) - item = @mpw.list[0] - assert_equal(@fixtures['add_new']['name'], item.name) - assert_equal(@fixtures['add_new']['group'], item.group) - assert_equal(@fixtures['add_new']['host'], item.host) - assert_equal(@fixtures['add_new']['protocol'], item.protocol) - assert_equal(@fixtures['add_new']['user'], item.user) - assert_equal(@fixtures['add_new']['password'], item.password) - assert_equal(@fixtures['add_new']['port'].to_i, item.port) - assert_equal(@fixtures['add_new']['comment'], item.comment) - end + assert(!item.nil?) + assert(!item.empty?) - def test_02_export_yaml - import_file = 'files/test_import.yml' - export_file = 'test_export.yml' + @mpw.read_data + @mpw.add(item) + @mpw.set_password(item.id, @fixtures['add']['password']) - assert(@mpw.import(import_file)) - assert_equal(2, @mpw.list.length) - assert(@mpw.export(export_file, :yaml)) - export = YAML::load_file(export_file) - assert_equal(2, export.length) + assert_equal(1, @mpw.list.length) - result = export.values[0] - assert_equal(@fixtures['add_new']['name'], result['name']) - assert_equal(@fixtures['add_new']['group'], result['group']) - assert_equal(@fixtures['add_new']['host'], result['host']) - assert_equal(@fixtures['add_new']['protocol'], result['protocol']) - assert_equal(@fixtures['add_new']['user'], result['user']) - assert_equal(@fixtures['add_new']['password'], result['password']) - assert_equal(@fixtures['add_new']['port'].to_i, result['port']) - assert_equal(@fixtures['add_new']['comment'], result['comment']) + item = @mpw.list[0] + @fixtures['add'].each do |k, v| + if k == 'password' + assert_equal(v, @mpw.get_password(item.id)) + else + assert_equal(v, item.send(k).to_s) + end + end - File.unlink(export_file) - end + @mpw.write_data + end - def test_03_import_csv - import_file = 'files/test_import.csv' + def test_03_decrypt_file + @mpw.read_data + assert_equal(1, @mpw.list.length) - assert(@mpw.import(import_file, :csv)) - assert_equal(2, @mpw.list.length) + item = @mpw.list[0] + @fixtures['add'].each do |k, v| + if k == 'password' + assert_equal(v, @mpw.get_password(item.id)) + else + assert_equal(v, item.send(k).to_s) + end + end + end - import = CSV.parse(File.read(import_file), headers: true) + def test_04_delete_item + @mpw.read_data + assert_equal(1, @mpw.list.length) - item = @mpw.list[0] - assert_equal(import[0]['name'], item.name) - assert_equal(import[0]['group'], item.group) - assert_equal(import[0]['host'], item.host) - assert_equal(import[0]['protocol'], item.protocol) - assert_equal(import[0]['user'], item.user) - assert_equal(import[0]['password'], item.password) - assert_equal(import[0]['port'].to_i, item.port) - assert_equal(import[0]['comment'], item.comment) - end + @mpw.list.each(&:delete) + assert_equal(0, @mpw.list.length) - def test_04_export_csv - import_file = 'files/test_import.csv' - export_file = 'test_export.csv' + @mpw.write_data + end - assert(@mpw.import(import_file, :csv)) - assert_equal(2, @mpw.list.length) - assert(@mpw.export(export_file, :csv)) - export = CSV.parse(File.read(export_file), headers: true) - assert_equal(2, export.length) + def test_05_search + @mpw.read_data - result = export[0] - assert_equal(@fixtures['add_new']['name'], result['name']) - assert_equal(@fixtures['add_new']['group'], result['group']) - assert_equal(@fixtures['add_new']['host'], result['host']) - assert_equal(@fixtures['add_new']['protocol'], result['protocol']) - assert_equal(@fixtures['add_new']['user'], result['user']) - assert_equal(@fixtures['add_new']['password'], result['password']) - assert_equal(@fixtures['add_new']['port'], result['port']) - assert_equal(@fixtures['add_new']['comment'], result['comment']) + @fixtures.each_value do |v| + data = { + group: v['group'], + user: v['user'], + url: v['url'], + comment: v['comment'] + } - File.unlink(export_file) - end + item = MPW::Item.new(data) - def test_05_add_item - data = {name: @fixtures['add_new']['name'], - group: @fixtures['add_new']['group'], - host: @fixtures['add_new']['host'], - protocol: @fixtures['add_new']['protocol'], - user: @fixtures['add_new']['user'], - password: @fixtures['add_new']['password'], - port: @fixtures['add_new']['port'], - comment: @fixtures['add_new']['comment'], - } - - item = MPW::Item.new(data) + assert(!item.nil?) + assert(!item.empty?) - assert(!item.nil?) - assert(!item.empty?) + @mpw.add(item) + @mpw.set_password(item.id, v['password']) + end - assert(@mpw.add(item)) + assert_equal(3, @mpw.list.length) + assert_equal(1, @mpw.list(group: @fixtures['add']['group']).length) + assert_equal(1, @mpw.list(pattern: 'gogole').length) + assert_equal(2, @mpw.list(pattern: 'example[2\.]').length) + end - assert_equal(1, @mpw.list.length) + def test_06_add_gpg_key + @mpw.read_data - item = @mpw.list[0] - assert_equal(@fixtures['add_new']['name'], item.name) - assert_equal(@fixtures['add_new']['group'], item.group) - assert_equal(@fixtures['add_new']['host'], item.host) - assert_equal(@fixtures['add_new']['protocol'], item.protocol) - assert_equal(@fixtures['add_new']['user'], item.user) - assert_equal(@fixtures['add_new']['password'], item.password) - assert_equal(@fixtures['add_new']['port'].to_i, item.port) - assert_equal(@fixtures['add_new']['comment'], item.comment) - end + @mpw.add_key('test2@example.com') + assert_equal(2, @mpw.list_keys.length) - def test_11_encrypt_empty_file - assert(@mpw.encrypt) - end + @mpw.write_data + end - def test_12_encrypt - import_file = 'files/test_import.yml' + def test_07_delete_gpg_key + @mpw.read_data + assert_equal(2, @mpw.list_keys.length) - assert(@mpw.import(import_file, :yaml)) - assert_equal(2, @mpw.list.length) + @mpw.delete_key('test2@example.com') + assert_equal(1, @mpw.list_keys.length) - assert(@mpw.encrypt) - end - - def test_13_decrypt_empty_file - assert(@mpw.decrypt) - assert_equal(0, @mpw.list.length) - end - - def test_14_decrypt - import_file = 'files/test_import.yml' - - assert(@mpw.import(import_file, :yaml)) - assert_equal(2, @mpw.list.length) - - assert(@mpw.encrypt) - - assert(@mpw.decrypt) - assert_equal(2, @mpw.list.length) - - item = @mpw.list[0] - assert_equal(@fixtures['add_new']['name'], item.name) - assert_equal(@fixtures['add_new']['group'], item.group) - assert_equal(@fixtures['add_new']['host'], item.host) - assert_equal(@fixtures['add_new']['protocol'], item.protocol) - assert_equal(@fixtures['add_new']['user'], item.user) - assert_equal(@fixtures['add_new']['password'], item.password) - assert_equal(@fixtures['add_new']['port'].to_i, item.port) - assert_equal(@fixtures['add_new']['comment'], item.comment) - end - - def test_15_search - import_file = 'files/test_import.yml' - - assert(@mpw.import(import_file, :yaml)) - assert_equal(2, @mpw.list.length) - - assert_equal(1, @mpw.list(group: @fixtures['add_new']['group']).length) - assert_equal(1, @mpw.list(protocol: @fixtures['add_new']['protocol']).length) - assert_equal(2, @mpw.list(search: @fixtures['add_new']['name'][0..-2]).length) - end + @mpw.write_data + end end diff --git a/test/test_translate.rb b/test/test_translate.rb new file mode 100644 index 0000000..d7cbceb --- /dev/null +++ b/test/test_translate.rb @@ -0,0 +1,29 @@ +require 'yaml' +require 'test/unit' + +class TestTranslate < Test::Unit::TestCase + def test_00_check_translate + missing = 0 + + Dir.glob('i18n/*.yml').each do |yaml| + lang = File.basename(yaml, '.yml') + translate = YAML.load_file(yaml) + + %x(grep -r -o "I18n.t('.*)" bin/ lib/ | cut -d"'" -f2).each_line do |line| + begin + t = translate[lang] + line.strip.split('.').each do |v| + t = t[v] + end + + assert(!t.to_s.empty?) + rescue + puts "#{lang}.#{line}" + missing = 1 + end + end + end + + assert_equal(0, missing) + end +end diff --git a/test/tests.rb b/test/tests.rb deleted file mode 100644 index d3735f5..0000000 --- a/test/tests.rb +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/ruby - -require_relative 'test_mpw.rb' -require_relative 'test_item.rb'