imp 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +202 -0
- data/README.md +12 -0
- data/bin/imp +4 -0
- data/imp.gemspec +26 -0
- data/lib/imp.rb +13 -0
- data/lib/imp/commands.rb +262 -0
- data/lib/imp/crypto.rb +71 -0
- data/lib/imp/encrypted_file.rb +90 -0
- data/lib/imp/encrypted_tree.rb +133 -0
- data/lib/imp/tree.rb +105 -0
- data/lib/imp/ui.rb +191 -0
- data/lib/imp/util.rb +19 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ca718bcafa6983935ec3f5cebab249382b78e08d
|
4
|
+
data.tar.gz: da7918ac1a7f95ce76da11cf7cbf100c6ac399d3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 468ba338782bf8215381da5d0a9fdc15fb8f95f489613367dbc208753f33de81ddb4440203a509249af390533f8956af810b306a1bb9a43ff7a30e76951f0859
|
7
|
+
data.tar.gz: fce1f953626dae4b430864589a80c1c2c6633e0e43859d3f92adf2ab0bc090caa7fe193da64f2955e28735bc64fc78343517aff5ee778f2e8cdc275ff5a0784f
|
data/LICENSE
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
Apache License
|
2
|
+
Version 2.0, January 2004
|
3
|
+
http://www.apache.org/licenses/
|
4
|
+
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
6
|
+
|
7
|
+
1. Definitions.
|
8
|
+
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
11
|
+
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
13
|
+
the copyright owner that is granting the License.
|
14
|
+
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
16
|
+
other entities that control, are controlled by, or are under common
|
17
|
+
control with that entity. For the purposes of this definition,
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
19
|
+
direction or management of such entity, whether by contract or
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
22
|
+
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
24
|
+
exercising permissions granted by this License.
|
25
|
+
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
27
|
+
including but not limited to software source code, documentation
|
28
|
+
source, and configuration files.
|
29
|
+
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
31
|
+
transformation or translation of a Source form, including but
|
32
|
+
not limited to compiled object code, generated documentation,
|
33
|
+
and conversions to other media types.
|
34
|
+
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
36
|
+
Object form, made available under the License, as indicated by a
|
37
|
+
copyright notice that is included in or attached to the work
|
38
|
+
(an example is provided in the Appendix below).
|
39
|
+
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
46
|
+
the Work and Derivative Works thereof.
|
47
|
+
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
49
|
+
the original version of the Work and any modifications or additions
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
61
|
+
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
64
|
+
subsequently incorporated within the Work.
|
65
|
+
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
72
|
+
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
78
|
+
where such license applies only to those patent claims licensable
|
79
|
+
by such Contributor that are necessarily infringed by their
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
82
|
+
institute patent litigation against any entity (including a
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
85
|
+
or contributory patent infringement, then any patent licenses
|
86
|
+
granted to You under this License for that Work shall terminate
|
87
|
+
as of the date such litigation is filed.
|
88
|
+
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
91
|
+
modifications, and in Source or Object form, provided that You
|
92
|
+
meet the following conditions:
|
93
|
+
|
94
|
+
(a) You must give any other recipients of the Work or
|
95
|
+
Derivative Works a copy of this License; and
|
96
|
+
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
98
|
+
stating that You changed the files; and
|
99
|
+
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
102
|
+
attribution notices from the Source form of the Work,
|
103
|
+
excluding those notices that do not pertain to any part of
|
104
|
+
the Derivative Works; and
|
105
|
+
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
108
|
+
include a readable copy of the attribution notices contained
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
111
|
+
of the following places: within a NOTICE text file distributed
|
112
|
+
as part of the Derivative Works; within the Source form or
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
114
|
+
within a display generated by the Derivative Works, if and
|
115
|
+
wherever such third-party notices normally appear. The contents
|
116
|
+
of the NOTICE file are for informational purposes only and
|
117
|
+
do not modify the License. You may add Your own attribution
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
120
|
+
that such additional attribution notices cannot be construed
|
121
|
+
as modifying the License.
|
122
|
+
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
124
|
+
may provide additional or different license terms and conditions
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
128
|
+
the conditions stated in this License.
|
129
|
+
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
133
|
+
this License, without any additional terms or conditions.
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
135
|
+
the terms of any separate license agreement you may have executed
|
136
|
+
with Licensor regarding such Contributions.
|
137
|
+
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
140
|
+
except as required for reasonable and customary use in describing the
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
142
|
+
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
152
|
+
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
158
|
+
incidental, or consequential damages of any character arising as a
|
159
|
+
result of this License or out of the use or inability to use the
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
162
|
+
other commercial damages or losses), even if such Contributor
|
163
|
+
has been advised of the possibility of such damages.
|
164
|
+
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
168
|
+
or other liability obligations and/or rights consistent with this
|
169
|
+
License. However, in accepting such obligations, You may act only
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
174
|
+
of your accepting any such warranty or additional liability.
|
175
|
+
|
176
|
+
END OF TERMS AND CONDITIONS
|
177
|
+
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
179
|
+
|
180
|
+
To apply the Apache License to your work, attach the following
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "{}"
|
182
|
+
replaced with your own identifying information. (Don't include
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
184
|
+
comment syntax for the file format. We also recommend that a
|
185
|
+
file or class name and description of purpose be included on the
|
186
|
+
same "printed page" as the copyright notice for easier
|
187
|
+
identification within third-party archives.
|
188
|
+
|
189
|
+
Copyright {yyyy} {name of copyright owner}
|
190
|
+
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
192
|
+
you may not use this file except in compliance with the License.
|
193
|
+
You may obtain a copy of the License at
|
194
|
+
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
196
|
+
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
200
|
+
See the License for the specific language governing permissions and
|
201
|
+
limitations under the License.
|
202
|
+
|
data/README.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
IMP (IMP Manager for Passwords)
|
2
|
+
===============================
|
3
|
+
|
4
|
+
A small and simple console based password manager.
|
5
|
+
|
6
|
+
Uses 256-bit AES encryption to encrypt a tree struction of saved passwords
|
7
|
+
with a master password. Provides a basic interactive environment to print
|
8
|
+
and copy these passwords.
|
9
|
+
|
10
|
+
Allows working with encrypted passwords without them ever appearing on-screen
|
11
|
+
(due to the copy functionality) as they would if using a simple encrypted
|
12
|
+
password file, but without the bloat of larger password managers.
|
data/bin/imp
ADDED
data/imp.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'imp'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'imp'
|
7
|
+
spec.summary = 'A lightweight console based password mangement system.'
|
8
|
+
spec.description = "
|
9
|
+
IMP is a simple console password manager. Passwords are stored in an AES
|
10
|
+
encrypted filesystem-like tree. The main functionality includes printing,
|
11
|
+
setting and copying files, allowing the handling of passwords without them
|
12
|
+
being shown on screen."
|
13
|
+
spec.description
|
14
|
+
spec.version = Imp::VERSION
|
15
|
+
spec.date = Time.now.strftime('%Y-%m-%d')
|
16
|
+
spec.author = 'Thomas Kerber'
|
17
|
+
spec.email = 't.kerber@online.de'
|
18
|
+
spec.homepage = 'https://github.com/tkerber/imp'
|
19
|
+
spec.files = Dir.glob("{docs,bin,lib}/**/*") + ['LICENSE', 'README.md',
|
20
|
+
__FILE__]
|
21
|
+
spec.executables = ['imp']
|
22
|
+
spec.license = "Apache-2.0"
|
23
|
+
|
24
|
+
spec.add_runtime_dependency 'highline'
|
25
|
+
spec.add_runtime_dependency 'clipboard'
|
26
|
+
end
|
data/lib/imp.rb
ADDED
data/lib/imp/commands.rb
ADDED
@@ -0,0 +1,262 @@
|
|
1
|
+
require 'clipboard'
|
2
|
+
require 'highline/import'
|
3
|
+
|
4
|
+
require_relative 'ui'
|
5
|
+
|
6
|
+
module Imp
|
7
|
+
|
8
|
+
# Contains the methods for all commands issued by the user.
|
9
|
+
# All commands are executed with Commands#send
|
10
|
+
module Commands
|
11
|
+
|
12
|
+
# The signals which should be sent to this module.
|
13
|
+
METHODS = [
|
14
|
+
"help",
|
15
|
+
"set",
|
16
|
+
"change_passwd",
|
17
|
+
"paste",
|
18
|
+
"print",
|
19
|
+
"copy",
|
20
|
+
"copy_raw",
|
21
|
+
"copyc",
|
22
|
+
"del",
|
23
|
+
"exit"]
|
24
|
+
|
25
|
+
# Deletes a key. If the key has no children, it is removed from the tree.
|
26
|
+
# If it has children, it is removed from the tree if and only if it's
|
27
|
+
# value was previously nil. Otherwise it's value is set to nil.
|
28
|
+
#
|
29
|
+
# @param key [String] The key to delete.
|
30
|
+
# @param force [Boolean] Doesn't require confirmation from the user if
|
31
|
+
# it is true.
|
32
|
+
def self.del(key, force = false)
|
33
|
+
unless force ||
|
34
|
+
agree("Are you sure you want to delete the key '#{key}'? ")
|
35
|
+
return
|
36
|
+
end
|
37
|
+
node = $tree.cont.descendant(key)
|
38
|
+
fail "Key does not exist." if node == nil
|
39
|
+
|
40
|
+
if node.val == nil
|
41
|
+
$tree.delete key
|
42
|
+
else
|
43
|
+
node.val = nil
|
44
|
+
end
|
45
|
+
# Remove any nil-leaves. (This may remove key IF it is a leaf)
|
46
|
+
$tree.prune
|
47
|
+
# Write out the tree.
|
48
|
+
$tree.flush
|
49
|
+
end
|
50
|
+
|
51
|
+
# Prints help text.
|
52
|
+
#
|
53
|
+
# @param args [Array] Ignored.
|
54
|
+
def self.help(*args)
|
55
|
+
puts ("
|
56
|
+
help - Prints this help text
|
57
|
+
set KEY - Sets the value of the key to a value entered by the user.
|
58
|
+
change_passwd - Changes the password of the current file.
|
59
|
+
paste KEY - Sets the value of the key from the system clipboard.
|
60
|
+
print - Prints a representation of the tree, without values.
|
61
|
+
print KEY - Prints the value of the key.
|
62
|
+
copy KEY - Copies the value of the key, auto clears clipboard afterward.
|
63
|
+
copy_raw - Clears the clipboard.
|
64
|
+
copy_raw KEY - Copies the value of a key, without clearing the clipboard.
|
65
|
+
Useful for moving values around between keys.
|
66
|
+
copyc INT KEY - Copies the (1-indexed) character from the value of the key.
|
67
|
+
del KEY - Deletes the key from the tree. If it has subtrees, the
|
68
|
+
subtrees get deleted if and only if the key had no value.
|
69
|
+
exit - Exit.
|
70
|
+
|
71
|
+
Keys are sorted in forward-slash seperated tree structure (slightly
|
72
|
+
remenicient of urls).")
|
73
|
+
end
|
74
|
+
|
75
|
+
# Prints either the tree if no argument is provided, or prints the value
|
76
|
+
# of a certain key.
|
77
|
+
#
|
78
|
+
# @param key [String, nil] The key if provided, or nil to print the tree.
|
79
|
+
def self.print(key = nil)
|
80
|
+
if key
|
81
|
+
print_val(key)
|
82
|
+
else
|
83
|
+
print_tree
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Changes the encryption password.
|
88
|
+
#
|
89
|
+
# @param args [Array] Ignored.
|
90
|
+
def self.change_passwd(*args)
|
91
|
+
pass = read_passwd
|
92
|
+
return unless pass
|
93
|
+
$tree.password = pass
|
94
|
+
$tree.flush
|
95
|
+
end
|
96
|
+
|
97
|
+
# Set a value. Require entering the value to set it to twice until they
|
98
|
+
# match. An empty value will cancel setting.
|
99
|
+
#
|
100
|
+
# @param key [String] The key to set the value for.
|
101
|
+
def self.set(key)
|
102
|
+
fail "Key must be supplied." unless key
|
103
|
+
pass = read_passwd
|
104
|
+
return unless pass
|
105
|
+
$tree[key] = pass
|
106
|
+
# We save the tree whenever it is modified.
|
107
|
+
$tree.flush
|
108
|
+
end
|
109
|
+
|
110
|
+
# Sets a value from the system clipboard.
|
111
|
+
# Fails if the clipboard is empty.
|
112
|
+
#
|
113
|
+
# @param key [String] The key to set.
|
114
|
+
def self.paste(key)
|
115
|
+
fail "Key must be supplied." unless key
|
116
|
+
pass = Clipboard.paste
|
117
|
+
fail "Clipboard empty, could not paste." if pass == ''
|
118
|
+
$tree[key] = pass
|
119
|
+
$tree.flush
|
120
|
+
end
|
121
|
+
|
122
|
+
# Copys the value of a key onto the system clipboard.
|
123
|
+
#
|
124
|
+
# @param key [String, nil] The key of the value to copy. If nil, clears
|
125
|
+
# the clipboard instead.
|
126
|
+
def self.copy_raw(key = nil)
|
127
|
+
begin
|
128
|
+
if key
|
129
|
+
Clipboard.copy($tree[key])
|
130
|
+
else
|
131
|
+
Clipboard.clear
|
132
|
+
end
|
133
|
+
# No method error arises from trying to work on a nil tree (or trying to
|
134
|
+
# decrypt a nil value).
|
135
|
+
rescue NoMethodError
|
136
|
+
fail "No value entered for key '#{key}'."
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Copys the value of a key onto the system clipboard. And auto-clears it
|
141
|
+
# afterwards.
|
142
|
+
#
|
143
|
+
# @param key [String] The key of the value to copy.
|
144
|
+
def self.copy(key)
|
145
|
+
fail "Key must be supplied." unless key
|
146
|
+
begin
|
147
|
+
UI.timeout do
|
148
|
+
copy_raw key
|
149
|
+
$stdout.print "Value copied. Press enter to wipe..."
|
150
|
+
gets
|
151
|
+
end
|
152
|
+
ensure
|
153
|
+
Clipboard.clear
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Copies the value of a single 1-indexed character of the value of a key
|
158
|
+
# to the system clipboard.
|
159
|
+
#
|
160
|
+
# @param argstr [String] The index to copy followed by the key, seperated
|
161
|
+
# by whitespace.
|
162
|
+
def self.copyc(argstr)
|
163
|
+
pos, key = argstr.split(2)
|
164
|
+
pos = pos.to_i
|
165
|
+
copyc_expanded(char, key)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Quits.
|
169
|
+
#
|
170
|
+
# @param args [Array] Ignored.
|
171
|
+
def self.quit(*args)
|
172
|
+
exit
|
173
|
+
end
|
174
|
+
|
175
|
+
# This private is purely symbolic as classmethods have to be explicitly
|
176
|
+
# defined as private.
|
177
|
+
private
|
178
|
+
|
179
|
+
# Reads a password from the user.
|
180
|
+
#
|
181
|
+
# @return [String, nil] The password enetered, or nil if aborted.
|
182
|
+
def self.read_passwd
|
183
|
+
first_pass = true
|
184
|
+
pass1 = pass2 = nil
|
185
|
+
until pass1 == pass2 && !first_pass
|
186
|
+
unless first_pass
|
187
|
+
puts "The pass did not match. Please try again."
|
188
|
+
end
|
189
|
+
pass1 = ask "Please enter the pass (leave blank to cancel): " do |q|
|
190
|
+
q.echo = false
|
191
|
+
end
|
192
|
+
return if pass1 == ''
|
193
|
+
pass2 = ask "Re-enter the pass to confirm: " do |q|
|
194
|
+
q.echo = false
|
195
|
+
end
|
196
|
+
first_pass = false
|
197
|
+
end
|
198
|
+
return pass1
|
199
|
+
end
|
200
|
+
private_class_method :read_passwd
|
201
|
+
|
202
|
+
# Copies the value of a single 1-indexed character of the value of a key
|
203
|
+
# to the system clipboard.
|
204
|
+
#
|
205
|
+
# @param pos [Int] The index to copy. IMPORTANT: The string starts at
|
206
|
+
# index 1!
|
207
|
+
# @param key [String] The key of the value to copy.
|
208
|
+
def self.copyc_expanded(pos, key)
|
209
|
+
begin
|
210
|
+
UI.timeout do
|
211
|
+
Clipboard.copy($tree[key][pos - 1])
|
212
|
+
end
|
213
|
+
# No method error arises from trying to work on a nil tree (or trying to
|
214
|
+
# decrypt a nil value).
|
215
|
+
rescue NoMethodError
|
216
|
+
fail "No value entered for key '#{key}'."
|
217
|
+
ensure
|
218
|
+
Clipboard.clear
|
219
|
+
end
|
220
|
+
end
|
221
|
+
private_class_method :copyc_expanded
|
222
|
+
|
223
|
+
# Prints strings, waits for enter then replaces them. Also adds color
|
224
|
+
# for fancyness.
|
225
|
+
def self.tmp_print(str)
|
226
|
+
HighLine::SystemExtensions.raw_no_echo_mode
|
227
|
+
$stdout.print HighLine.color(str, :bold, :green)
|
228
|
+
begin
|
229
|
+
UI.timeout do
|
230
|
+
HighLine::SystemExtensions.get_character
|
231
|
+
end
|
232
|
+
ensure
|
233
|
+
HighLine::SystemExtensions.restore_mode
|
234
|
+
hidden_text = "\r<hidden>" << ' ' * [str.length - 8, 0].max
|
235
|
+
puts HighLine.color(hidden_text, :bold, :green)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
private_class_method :tmp_print
|
239
|
+
|
240
|
+
# Prints a value
|
241
|
+
#
|
242
|
+
# @param key [String] The key for which to retrieve a value.
|
243
|
+
def self.print_val(key)
|
244
|
+
begin
|
245
|
+
tmp_print $tree[key]
|
246
|
+
# No method error arises from trying to work on a nil tree (or trying to
|
247
|
+
# decrypt a nil value).
|
248
|
+
rescue NoMethodError
|
249
|
+
fail "No value entered for key '#{key}'."
|
250
|
+
end
|
251
|
+
end
|
252
|
+
private_class_method :print_val
|
253
|
+
|
254
|
+
# Prints the currently loaded tree (without values).
|
255
|
+
def self.print_tree
|
256
|
+
puts $tree
|
257
|
+
end
|
258
|
+
private_class_method :print_tree
|
259
|
+
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
data/lib/imp/crypto.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Imp
|
4
|
+
|
5
|
+
# Contains methods for easily interfacing with ruby's encryption algorithms.
|
6
|
+
# Uses 256 bit AES in CBC mode with keys generated by PBKDF2 using SHA1
|
7
|
+
# and 10 000 iterations.
|
8
|
+
module Crypto
|
9
|
+
|
10
|
+
# The length of the key to use.
|
11
|
+
# This module used AES-256, which has a key length of 32 bytes.
|
12
|
+
KEYLEN = 32
|
13
|
+
# The block size of the cipher.
|
14
|
+
# As this module uses AES, this is 16 bytes.
|
15
|
+
BLOCK_SIZE = 16
|
16
|
+
# The length of the salts to generate. The length is used as that of the
|
17
|
+
# key.
|
18
|
+
SALTLEN = KEYLEN
|
19
|
+
# The iteration of the PBKDF2 algorim to go through.
|
20
|
+
ITER = 10_000
|
21
|
+
# The mode of AES to use.
|
22
|
+
MODE = :CBC
|
23
|
+
|
24
|
+
# Delegates key generation to PBKDF2
|
25
|
+
#
|
26
|
+
# @param passwd [String] The password.
|
27
|
+
# @param salt [String] The salt.
|
28
|
+
# @return [String] The key.
|
29
|
+
def self.get_key(passwd, salt)
|
30
|
+
OpenSSL::PKCS5.pbkdf2_hmac_sha1(passwd, salt, ITER, KEYLEN)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Gets a random salt.
|
34
|
+
#
|
35
|
+
# @return [String] A salt.
|
36
|
+
def self.rand_salt
|
37
|
+
OpenSSL::Random.random_bytes SALTLEN
|
38
|
+
end
|
39
|
+
|
40
|
+
# Encrypts a string. The result is the IV, followed by the actual
|
41
|
+
# encrypted string.
|
42
|
+
#
|
43
|
+
# @param key [String] The key.
|
44
|
+
# @param data [String] The unencrypted data.
|
45
|
+
# @return [String] The encrypted data.
|
46
|
+
def self.encrypt(key, data)
|
47
|
+
cipher = OpenSSL::Cipher::AES.new(KEYLEN * 8, MODE)
|
48
|
+
cipher.encrypt
|
49
|
+
iv = cipher.random_iv
|
50
|
+
cipher.key = key
|
51
|
+
|
52
|
+
iv + cipher.update(data) + cipher.final
|
53
|
+
end
|
54
|
+
|
55
|
+
# Decrypts a string encrypted by ::encrypt
|
56
|
+
#
|
57
|
+
# @param key [String] The key.
|
58
|
+
# @param data [String] The encrypted data.
|
59
|
+
# @return [String] The unencrypted data.
|
60
|
+
def self.decrypt(key, data)
|
61
|
+
cipher = OpenSSL::Cipher::AES.new(KEYLEN * 8, MODE)
|
62
|
+
cipher.decrypt
|
63
|
+
cipher.iv = data.byteslice 0...BLOCK_SIZE
|
64
|
+
cipher.key = key
|
65
|
+
|
66
|
+
cipher.update(data.byteslice BLOCK_SIZE..-1) + cipher.final
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require_relative 'crypto'
|
2
|
+
require_relative 'util'
|
3
|
+
|
4
|
+
module Imp
|
5
|
+
|
6
|
+
# A rudimentary wrapper to interface with encrypted files.
|
7
|
+
#
|
8
|
+
# Files are saved as a concatination of the password's salt and a string
|
9
|
+
# encrypted with Crypto#encrypt. The string may be marshalled content, or
|
10
|
+
# it may be the content itself.
|
11
|
+
#
|
12
|
+
# @note This is NOT a file object. The file's content is loaded entirely
|
13
|
+
# into memory.
|
14
|
+
class EncryptedFile
|
15
|
+
|
16
|
+
# The plaintext content of the encrypted file.
|
17
|
+
attr_accessor :cont
|
18
|
+
|
19
|
+
# If the file exists, load the content from it. Otherwise load the
|
20
|
+
# content as nil, generate a salt and key to prepare for writing.
|
21
|
+
#
|
22
|
+
# @param passwd [String] The password.
|
23
|
+
# @param file [String] The location of the file.
|
24
|
+
# @param marshal [Boolean] Whether or not the content is marshalled.
|
25
|
+
def initialize(passwd, file, marshal = true)
|
26
|
+
@file = File.expand_path(file)
|
27
|
+
@marshal = marshal
|
28
|
+
if File.exists? @file
|
29
|
+
init_with_file(passwd)
|
30
|
+
else
|
31
|
+
first_time_init(passwd)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Writes the content to the file.
|
36
|
+
def flush
|
37
|
+
f = File.new(@file, "w")
|
38
|
+
f << @salt
|
39
|
+
if @marshal
|
40
|
+
cont = Marshal.dump @cont
|
41
|
+
else
|
42
|
+
cont = @cont
|
43
|
+
end
|
44
|
+
f << Crypto.encrypt(@key, cont)
|
45
|
+
f.flush
|
46
|
+
# Encrypted files should only be readable by their owner. Doesn't really
|
47
|
+
# add much security but hey.
|
48
|
+
f.chmod(0600)
|
49
|
+
f.close
|
50
|
+
end
|
51
|
+
|
52
|
+
# Nulls the key. (It may still be in memory!)
|
53
|
+
def close
|
54
|
+
@cont = nil
|
55
|
+
@key = nil
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def password=(passwd)
|
61
|
+
@salt = Crypto.rand_salt
|
62
|
+
@key = Crypto.get_key(passwd, @salt)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Loads the content from the file.
|
66
|
+
#
|
67
|
+
# @param passwd [String] The password.
|
68
|
+
def init_with_file(passwd)
|
69
|
+
f = File.new(@file)
|
70
|
+
@cont = f.read
|
71
|
+
f.close
|
72
|
+
@salt = @cont.byteslice 0...Crypto::SALTLEN
|
73
|
+
@cont = @cont.byteslice Crypto::SALTLEN..-1
|
74
|
+
@key = Crypto.get_key(passwd, @salt)
|
75
|
+
@cont = Crypto.decrypt(@key, @cont)
|
76
|
+
@cont = Marshal.load(@cont) if @marshal
|
77
|
+
end
|
78
|
+
|
79
|
+
# Initializes the encrypted file.
|
80
|
+
#
|
81
|
+
# @param passwd [String] The password.
|
82
|
+
def first_time_init(passwd)
|
83
|
+
Util.mkdirs(File.dirname(@file))
|
84
|
+
self.password = passwd
|
85
|
+
@cont = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require_relative 'tree'
|
2
|
+
require_relative 'encrypted_file'
|
3
|
+
require_relative 'crypto'
|
4
|
+
|
5
|
+
|
6
|
+
module Imp
|
7
|
+
|
8
|
+
# A tree loaded from an encrypted file.
|
9
|
+
#
|
10
|
+
# All values are themselves encrypted with the key again. This doesn't
|
11
|
+
# add additional security, but prevents them from appearing in memory in
|
12
|
+
# plaintext if avoidable. Note that any program designed specifically
|
13
|
+
# to tap into this programs memory will have no problem with this.
|
14
|
+
class EncryptedTree < EncryptedFile
|
15
|
+
|
16
|
+
include Enumerable
|
17
|
+
|
18
|
+
|
19
|
+
# Creates a new tree / load an existing one from a file.
|
20
|
+
#
|
21
|
+
# @param passwd [String] The password to decrypt the file. Discarded
|
22
|
+
# after this call (although the key is kept in memory)
|
23
|
+
# @param file [String] The location of the file to decrypt. If this does
|
24
|
+
# not exist, a new tree is made.
|
25
|
+
def initialize(passwd, file)
|
26
|
+
super(passwd, file)
|
27
|
+
if @cont == nil
|
28
|
+
@cont = Tree.new
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Retrieves a node label following a forward-slash seperated list of
|
33
|
+
# edge labels.
|
34
|
+
#
|
35
|
+
# @param key [String] The forward-slash seperated list of edge labels.
|
36
|
+
# @return [String] The decrypted value of the corresponding node label.
|
37
|
+
def [](key)
|
38
|
+
Crypto.decrypt(@key, @cont.descendant(key).val)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Sets a node label corresponding to a forward-slash seperated list of
|
42
|
+
# edge labels.
|
43
|
+
#
|
44
|
+
# @param key [String] The list of edge labels.
|
45
|
+
# @param val [String] The value to set the node label to. This will be
|
46
|
+
# encrypted.
|
47
|
+
def []=(key, val)
|
48
|
+
@cont.descendant(key, true).val = Crypto.encrypt(@key, val)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Iterates over key/value pairs.
|
52
|
+
#
|
53
|
+
# @param keys [Array<String>] A list of the keys followed to reach the
|
54
|
+
# current subtree.
|
55
|
+
# @param subtree [Tree] The tree currently iterating over.
|
56
|
+
# @yield [String, String] Key, value pairs where the key is a forward
|
57
|
+
# slash seperated string of edge labels. Values are not decrypted or
|
58
|
+
# processed.
|
59
|
+
def each(keys = [], subtree = @cont, &block)
|
60
|
+
# Yield the subtree's value unless it is the root.
|
61
|
+
yield [keys.join('/'), subtree.val] unless keys == []
|
62
|
+
subtree.each do |key, tree|
|
63
|
+
each(keys + [key], tree, &block)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Checks whether a tree contains a key.
|
68
|
+
#
|
69
|
+
# @param item [String] The forward slash seperated string of edge labels.
|
70
|
+
def include?(item)
|
71
|
+
@cont.descendant(key) != nil
|
72
|
+
end
|
73
|
+
|
74
|
+
# Deletes a node corresponding to a forward-slash seperated list of edge
|
75
|
+
# labels.
|
76
|
+
#
|
77
|
+
# @param key [String] The list of edge labels. Must be a valid key.
|
78
|
+
def delete(key)
|
79
|
+
# We seperate the last key from the first keys.
|
80
|
+
key = key.split('/')
|
81
|
+
finalkey = key[-1]
|
82
|
+
key = key[0...-1]
|
83
|
+
|
84
|
+
# Instead of using descendant we reduce over the root. This also handels
|
85
|
+
# the root being the parent node well.
|
86
|
+
node = key.reduce(@cont, :[])
|
87
|
+
node.delete finalkey
|
88
|
+
end
|
89
|
+
|
90
|
+
# Iteratively removes any leaves with a nil value.
|
91
|
+
# Not terribly efficient but there is no need to be.
|
92
|
+
def prune
|
93
|
+
pruned = true
|
94
|
+
while pruned
|
95
|
+
pruned = false
|
96
|
+
self.each do |key, value|
|
97
|
+
if value == nil && @cont.descendant(key).leaf?
|
98
|
+
delete(key)
|
99
|
+
pruned = true
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Sets a new password for the file.
|
106
|
+
#
|
107
|
+
# @param [String] The new password to generate a key from.
|
108
|
+
def password=(passwd)
|
109
|
+
key = @key
|
110
|
+
# Super call.
|
111
|
+
EncryptedFile.instance_method(:password=).bind(self).call(passwd)
|
112
|
+
# If the file is still being initialized, @cont may be nil. In this case
|
113
|
+
# return.
|
114
|
+
return unless @cont
|
115
|
+
each do |k, v|
|
116
|
+
# Don't change nil values.
|
117
|
+
next unless v
|
118
|
+
# Otherwise decrypt with the old key and encrypt with the new.
|
119
|
+
# (Encryption is done automatically by #[]=)
|
120
|
+
self[k] = Crypto.decrypt(key, v)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Delegates to the tree for string representation.
|
125
|
+
#
|
126
|
+
# @return [String] The string representation of the tree.
|
127
|
+
def to_s
|
128
|
+
@cont.to_s
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
data/lib/imp/tree.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
|
2
|
+
module Imp
|
3
|
+
|
4
|
+
# A directory-esque tree with labeled nodes and edges.
|
5
|
+
class Tree
|
6
|
+
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
|
10
|
+
# The value of the current node in the tree.
|
11
|
+
attr_accessor :val
|
12
|
+
|
13
|
+
# Creates a new Tree.
|
14
|
+
#
|
15
|
+
# @param val [Object] The node label of the tree.
|
16
|
+
def initialize(val = nil)
|
17
|
+
@val = val
|
18
|
+
@succ = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Gets a subtree by the label of the edge leading to it.
|
22
|
+
#
|
23
|
+
# @param key [String] The edge label.
|
24
|
+
# @param create [Boolean] Whether or not the create a new Node if there
|
25
|
+
# is no edge with the label given.
|
26
|
+
# @return [Tree, nil] The tree at the edge, or nil if it didn't exist
|
27
|
+
# annd create was false.
|
28
|
+
def [](key, create = false)
|
29
|
+
if create and not @succ.include? key
|
30
|
+
@succ[key] = Tree.new
|
31
|
+
else
|
32
|
+
@succ[key]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Removes a subtree by the label of the edge leading to it.
|
37
|
+
#
|
38
|
+
# @param key [String] The edge label.s
|
39
|
+
def delete(key)
|
40
|
+
@succ.delete key
|
41
|
+
end
|
42
|
+
|
43
|
+
# Checks if this is a leaf node.
|
44
|
+
#
|
45
|
+
# @return [Boolean] Wheter or not the node is a leaf.
|
46
|
+
def leaf?
|
47
|
+
@succ.length == 0
|
48
|
+
end
|
49
|
+
|
50
|
+
# Iterates over (edge, node) pairs.
|
51
|
+
#
|
52
|
+
# @yield [String, Tree] Edge, node pairs of connected nodes.
|
53
|
+
def each(&block)
|
54
|
+
@succ.each(&block)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Checks if an edge is included.
|
58
|
+
#
|
59
|
+
# @param item [String] The string to check.
|
60
|
+
# @return [Boolean] Whether or not the string is an edge label going out
|
61
|
+
# from this node.
|
62
|
+
def include? item
|
63
|
+
@succ.include? item
|
64
|
+
end
|
65
|
+
|
66
|
+
# Gets a (more distant descendant of the current node.
|
67
|
+
#
|
68
|
+
# @param key [String] A forward-slash seperated list of the edge labels to
|
69
|
+
# follow.
|
70
|
+
# @param create [Boolean] Whether or not to create nodes if the edge
|
71
|
+
# labels aren't used yet.
|
72
|
+
# @return [Tree, nil] The node connected through the edge labels, or nil
|
73
|
+
# if there is no such node and create was false.
|
74
|
+
def descendant(key, create = false)
|
75
|
+
if key.include? '/'
|
76
|
+
key, keys = key.split('/', 2)
|
77
|
+
child = self[key, create]
|
78
|
+
if child
|
79
|
+
child.descendant(keys, create)
|
80
|
+
end
|
81
|
+
else
|
82
|
+
self[key, create]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Prints the skeleton of the tree. Node labels are NOT printed.
|
87
|
+
#
|
88
|
+
# @param indent [Int] By how many stages to indent the tree.
|
89
|
+
# @return [String] The skeleton of the tree.
|
90
|
+
def to_s(indent = 0)
|
91
|
+
s = ""
|
92
|
+
each do |k, v|
|
93
|
+
s += ' ' * indent
|
94
|
+
s += k
|
95
|
+
s += '/' unless v.leaf?
|
96
|
+
s += '*' if v.val
|
97
|
+
s += "\n"
|
98
|
+
s += v.to_s(indent + 1)
|
99
|
+
end
|
100
|
+
return s
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
data/lib/imp/ui.rb
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
require 'highline/import'
|
2
|
+
require 'readline'
|
3
|
+
require 'timeout'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
require_relative 'encrypted_tree'
|
7
|
+
require_relative 'util'
|
8
|
+
require_relative 'commands'
|
9
|
+
require_relative '../imp'
|
10
|
+
|
11
|
+
# A small and simple password manager.
|
12
|
+
module Imp
|
13
|
+
|
14
|
+
# Module handling user I/O.
|
15
|
+
module UI
|
16
|
+
|
17
|
+
# The default file to save encrypted passwords in.
|
18
|
+
DEFAULT_FILE = '~/.imp/default.enc'
|
19
|
+
# The file of the history of the prompt.
|
20
|
+
HISTFILE = '~/.imp/hist'
|
21
|
+
# The string precending user input in the prompt.
|
22
|
+
PROMPT = 'imp> '
|
23
|
+
# The time in seconds, after which the program exits if it recieves no
|
24
|
+
# input from the user.
|
25
|
+
TIMEOUT = 300
|
26
|
+
|
27
|
+
# Loads and decrypts a file. The password is asked for interactively.
|
28
|
+
def self.load_file
|
29
|
+
until $tree
|
30
|
+
begin
|
31
|
+
passwd = ask("Password for file #{$file} (leave blank to cancel): ")\
|
32
|
+
do |q|
|
33
|
+
q.echo = false
|
34
|
+
end
|
35
|
+
if passwd == ''
|
36
|
+
break
|
37
|
+
end
|
38
|
+
$tree = EncryptedTree.new(passwd, $file)
|
39
|
+
rescue OpenSSL::Cipher::CipherError
|
40
|
+
$stderr.puts "Decryption failed. Corrupt file or wrong password."
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Closes the tree. This generally only happens right before ruby is about
|
46
|
+
# to exit so it isn't that important but hey.
|
47
|
+
def self.close_file
|
48
|
+
$tree.close
|
49
|
+
$tree = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
# Runs the program.
|
53
|
+
def self.main
|
54
|
+
load_options
|
55
|
+
if $opts[:file]
|
56
|
+
$file = $opts[:file]
|
57
|
+
else
|
58
|
+
$file = DEFAULT_FILE
|
59
|
+
end
|
60
|
+
load_file
|
61
|
+
# If no password was entered, quit.
|
62
|
+
exit unless $tree
|
63
|
+
welcome
|
64
|
+
init_readline
|
65
|
+
begin
|
66
|
+
prompt
|
67
|
+
ensure
|
68
|
+
close_file
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Times out execution of a block and exits printing an appropriate message
|
73
|
+
# if the block doesn't time out in time.
|
74
|
+
#
|
75
|
+
# The name is due to a conflict with Timeout's own.
|
76
|
+
def self.timeout(&block)
|
77
|
+
begin
|
78
|
+
Timeout::timeout(TIMEOUT, &block)
|
79
|
+
rescue Timeout::Error
|
80
|
+
$stderr.puts "\nUser input timeout. Closing..."
|
81
|
+
exit
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
|
89
|
+
# Load program options.
|
90
|
+
def self.load_options
|
91
|
+
$opts = {}
|
92
|
+
OptionParser.new do |opts|
|
93
|
+
opts.banner = "Usage: imp [options]"
|
94
|
+
opts.on('-v', '--[no-]verbose', 'Print exception stacks.') do |v|
|
95
|
+
$opts[:verbose] = v
|
96
|
+
end
|
97
|
+
opts.on('-f', '--file [FILE]', 'Load from the given file') do |f|
|
98
|
+
$opts[:file] = f
|
99
|
+
end
|
100
|
+
end.parse!
|
101
|
+
end
|
102
|
+
private_class_method :load_options
|
103
|
+
|
104
|
+
# Displays welcome text.
|
105
|
+
def self.welcome
|
106
|
+
puts "imp version #{VERSION}"
|
107
|
+
puts "Using password file #{$file}."
|
108
|
+
puts "Welcome to imp! Type 'help' for a list of commands."
|
109
|
+
end
|
110
|
+
private_class_method :welcome
|
111
|
+
|
112
|
+
# Runs a single command by the user. Also catches most errors and prints
|
113
|
+
# them.
|
114
|
+
def self.run(command)
|
115
|
+
# Ctrl-D will return nil; this should be a quit signal.
|
116
|
+
exit unless command
|
117
|
+
# Ignore empty commands
|
118
|
+
return if command == ''
|
119
|
+
command, args = command.strip.split(nil, 2)
|
120
|
+
command.downcase!
|
121
|
+
if Commands::METHODS.include? command
|
122
|
+
begin
|
123
|
+
Commands.send(command.to_sym, args)
|
124
|
+
rescue
|
125
|
+
$stderr.puts $!
|
126
|
+
if $opts[:verbose]
|
127
|
+
$!.backtrace.each do |t|
|
128
|
+
puts "\tfrom #{t}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
else
|
133
|
+
$stderr.puts "Command '#{command}' undefined. Type 'help' for a list "\
|
134
|
+
"of commands."
|
135
|
+
end
|
136
|
+
end
|
137
|
+
private_class_method :run
|
138
|
+
|
139
|
+
# Runs a basic prompt for the user to interface with the program.
|
140
|
+
def self.prompt
|
141
|
+
load_prompt_hist
|
142
|
+
quit = false
|
143
|
+
begin
|
144
|
+
until quit
|
145
|
+
timeout do
|
146
|
+
input = Readline.readline(PROMPT, true)
|
147
|
+
quit = run(input) == :quit
|
148
|
+
end
|
149
|
+
end
|
150
|
+
ensure
|
151
|
+
save_prompt_hist
|
152
|
+
end
|
153
|
+
end
|
154
|
+
private_class_method :prompt
|
155
|
+
|
156
|
+
# Loads the prompt history.
|
157
|
+
def self.load_prompt_hist
|
158
|
+
f = File.expand_path(HISTFILE)
|
159
|
+
return unless File.exists? f
|
160
|
+
f = File.new f
|
161
|
+
cont = f.read
|
162
|
+
f.close
|
163
|
+
Marshal.load(cont).each do |h|
|
164
|
+
Readline::HISTORY << h
|
165
|
+
end
|
166
|
+
end
|
167
|
+
private_class_method :load_prompt_hist
|
168
|
+
|
169
|
+
# Saves the prompt history.
|
170
|
+
def self.save_prompt_hist
|
171
|
+
f = File.expand_path(HISTFILE)
|
172
|
+
Util.mkdirs(File.dirname(f))
|
173
|
+
f = File.new(f, "w")
|
174
|
+
f.write(Marshal.dump(Readline::HISTORY.to_a))
|
175
|
+
f.close
|
176
|
+
end
|
177
|
+
private_class_method :save_prompt_hist
|
178
|
+
|
179
|
+
# Initializes autocompletion for readline.
|
180
|
+
def self.init_readline
|
181
|
+
Readline.completion_proc = proc do |s|
|
182
|
+
reg = /^#{Regexp.escape s}/
|
183
|
+
ret = Commands::METHODS.grep reg
|
184
|
+
ret + $tree.find_all{ |k, v| k =~ reg && v }.map{ |k, _| k }
|
185
|
+
end
|
186
|
+
end
|
187
|
+
private_class_method :init_readline
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
data/lib/imp/util.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
module Imp
|
3
|
+
|
4
|
+
# Contains misc. utility methods.
|
5
|
+
module Util
|
6
|
+
|
7
|
+
# Creates as many directories as needed.
|
8
|
+
#
|
9
|
+
# @param dir [String] The directory to create.
|
10
|
+
def self.mkdirs(dir)
|
11
|
+
return if Dir.exists? dir
|
12
|
+
parent = File.dirname(dir)
|
13
|
+
mkdirs(parent)
|
14
|
+
Dir.mkdir(dir)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: imp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Thomas Kerber
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: highline
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: clipboard
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: |2-
|
42
|
+
|
43
|
+
IMP is a simple console password manager. Passwords are stored in an AES
|
44
|
+
encrypted filesystem-like tree. The main functionality includes printing,
|
45
|
+
setting and copying files, allowing the handling of passwords without them
|
46
|
+
being shown on screen.
|
47
|
+
email: t.kerber@online.de
|
48
|
+
executables:
|
49
|
+
- imp
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- bin/imp
|
54
|
+
- lib/imp/commands.rb
|
55
|
+
- lib/imp/crypto.rb
|
56
|
+
- lib/imp/encrypted_tree.rb
|
57
|
+
- lib/imp/tree.rb
|
58
|
+
- lib/imp/util.rb
|
59
|
+
- lib/imp/ui.rb
|
60
|
+
- lib/imp/encrypted_file.rb
|
61
|
+
- lib/imp.rb
|
62
|
+
- LICENSE
|
63
|
+
- README.md
|
64
|
+
- imp.gemspec
|
65
|
+
homepage: https://github.com/tkerber/imp
|
66
|
+
licenses:
|
67
|
+
- Apache-2.0
|
68
|
+
metadata: {}
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
requirements: []
|
84
|
+
rubyforge_project:
|
85
|
+
rubygems_version: 2.0.3
|
86
|
+
signing_key:
|
87
|
+
specification_version: 4
|
88
|
+
summary: A lightweight console based password mangement system.
|
89
|
+
test_files: []
|
90
|
+
has_rdoc:
|