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.
@@ -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
+
@@ -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
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'imp'
3
+
4
+ Imp.main
@@ -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
@@ -0,0 +1,13 @@
1
+ require_relative 'imp/ui'
2
+
3
+ module Imp
4
+
5
+ # The current version number.
6
+ VERSION = "0.2.1"
7
+
8
+ # Run the program.
9
+ def self.main
10
+ UI.main
11
+ end
12
+
13
+ end
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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: