passvault 0.1.2

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,146 @@
1
+ require_relative 'bytes_manip'
2
+ require 'openssl'
3
+
4
+ # This class contains the implementation of cryptographics utilities and functions.
5
+ #
6
+ # All keys used to communicate with the tag (Triple DES keys) are always
7
+ # represented as 16 bytes long byte-strings. Byte-strings is what the OpenSSL
8
+ # library use, so it makes sense to use that. But often, it is useful to
9
+ # manipulate those as array of bytes. The functions String.unpack() and
10
+ # Array.pack() ((un)pack into(from) a byte string) are very useful for this,
11
+ # especially with 'C*' as argument .
12
+ #
13
+ # The card is only able to perform TDES encryption (not decryption) with 2 keys
14
+ # (keying option 2, aka 2TDEA). When the two keys are identical, the result is
15
+ # the same as plain DES. We elected to use only TDES on the user end (which is
16
+ # presumably also the case on the tag's end). Therefore all TDES and DES keys
17
+ # are 16-byte key. A 16 byte DES key is formed by concatening the 8 byte DES key
18
+ # to itself.
19
+ #
20
+ # AES encryption is used to encrypt the credentials stored on the tag.
21
+ #
22
+ # OpenSSL modes:
23
+ #
24
+ # <tt>DES-EDE-CBC</tt> = Triple DES (Encrypt Decrypt Encrypt) in the regular CBC
25
+ # mode (send for encryption, receive for decryption). The IV is zeroed by
26
+ # default.
27
+ #
28
+ # <tt>AES-256-CBC</tt> = AES with 256 bit keys in the regular CBC mode. The IV
29
+ # is zeroed by default.
30
+
31
+ module Crypto
32
+ include BytesManipulation
33
+
34
+ # Block length for DES.
35
+ DES_BLEN = 8
36
+
37
+ # An IV of +DES_BLEN+ zeroes.
38
+ ZERO_IV = Array.new(DES_BLEN, 0).pack("C*")
39
+
40
+ # Size of the HMAC (SHA-256) digest.
41
+ HMAC_LEN = 32
42
+
43
+ # Deciphers a byte string using 3DES in CBC receive mode. Returns a byte
44
+ # string.
45
+ def decipher_receive(bytes, key)
46
+ des = OpenSSL::Cipher.new('DES-EDE-CBC')
47
+ des.decrypt
48
+ des.key = key
49
+ des.padding = 0 # no padding
50
+ return des.update(bytes) + des.final
51
+ end
52
+
53
+ # Deciphers a byte string using 3DES in CBC send mode.
54
+ def decipher_send(bytes, key)
55
+ mix = ZERO_IV
56
+ partial_block_count = (bytes.bytesize % DES_BLEN > 0) ? 1 : 0
57
+ nblocks = bytes.length / DES_BLEN + partial_block_count
58
+ out = Array.new(nblocks)
59
+
60
+ out.each_index do |i|
61
+ block = bytes.byteslice(i*DES_BLEN, DES_BLEN)
62
+ block = bs_xor(block, mix)
63
+ block = decipher_receive(block, key)
64
+ out[i] = block
65
+ mix = block
66
+ end
67
+ return out.reduce("", :+)
68
+ end
69
+
70
+ # The following enciphering functions are not needed by the passworld vault,
71
+ # but are always nice to have to perform some verifications.
72
+
73
+ # Enciphers a byte string using 3DES in CBC send mode. Returns a byte
74
+ # string. Unused.
75
+ def encipher_send(bytes, key)
76
+ des = OpenSSL::Cipher.new('DES-EDE-CBC')
77
+ des.encrypt
78
+ des.key = key
79
+ des.padding = 0 # no padding
80
+ return des.update(bytes) + des.final
81
+ end
82
+
83
+ # Enciphers a byte string using 3DES in CBC receive mode. Returns a byte
84
+ # string. Unused.
85
+ def encipher_receive(bytes, key)
86
+ mix = ZERO_IV
87
+ partial_block_count = (bytes.bytesize % DES_BLEN > 0) ? 1 : 0
88
+ nblocks = bytes.length / DES_BLEN + partial_block_count
89
+ out = Array.new(nblocks)
90
+
91
+ out.each_index do |i|
92
+ block1 = bytes.byteslice(i*DES_BLEN, DES_BLEN)
93
+ block = encipher_send(block1, key)
94
+ block = bs_xor(block, mix)
95
+ out[i] = block
96
+ mix = block1
97
+ end
98
+ return out.reduce("", :+)
99
+ end
100
+
101
+ # Derive a key of the given size using the PBKDF2 derivation function, from
102
+ # given pass and salt. 50k iterations of SHA-256 are used.
103
+ def derive_key(pass, salt, key_size)
104
+ sha256_d = OpenSSL::Digest::SHA256.new
105
+ # (pass, salt, iter, keylength, digest)
106
+ return OpenSSL::PKCS5::pbkdf2_hmac(pass, salt, 50000, key_size, sha256_d)
107
+ end
108
+
109
+ # Derive a DESFire Triple DES key. This is similar to <tt>derive_key()</tt>
110
+ # with +key_size+ = 16, excepted that we overwrite the parity bits of the
111
+ # key. Anyone can ask to get those bits from the tag ("key versionning"
112
+ # feature). As such, keeping the orignal parity bits makes the key less
113
+ # secure.
114
+ def derive_desfire_key(pass, salt)
115
+ key = derive_key(pass, salt, 16)
116
+ array = key.unpack('C*')
117
+ array.map! { |e| e & 0xFE }
118
+ return array.pack('C*')
119
+ end
120
+
121
+ # Enciphers using AES with 256 bits key in CBC (send) mode.
122
+ def aes_encipher(bytes, key)
123
+ aes = OpenSSL::Cipher.new('AES-256-CBC')
124
+ aes.encrypt
125
+ aes.key = key
126
+ aes.padding = 0 # no padding
127
+ return aes.update(bytes) + aes.final
128
+ end
129
+
130
+ # Deciphers using AES with 256 bits key in CBC (receive) mode.
131
+ def aes_decipher(bytes, key)
132
+ aes = OpenSSL::Cipher.new('AES-256-CBC')
133
+ aes.decrypt
134
+ aes.key = key
135
+ aes.padding = 0 # no padding
136
+ return aes.update(bytes) + aes.final
137
+ end
138
+
139
+ # Computes a HMAC digest on given data using given key. Uses SHA-1 as
140
+ # digest.
141
+ def hmac(bytes, key)
142
+ sha256_d = sha256_d = OpenSSL::Digest::SHA256.new
143
+ return OpenSSL::HMAC.digest(sha256_d, key, bytes)
144
+ end
145
+
146
+ end
@@ -0,0 +1,103 @@
1
+ module Rights
2
+
3
+ # Communication setting: plain (unencrypted, unauthenticated) communication.
4
+ CS_PLAIN = 0x0
5
+
6
+ # Communication setting: unencrypted, authenticated communication.
7
+ CS_PLAIN_MAC = 0x1
8
+
9
+ # Communication setting: encrypted, authenticated communication.
10
+ CS_CIPHERED = 0x3
11
+
12
+ # Used in file access rights in place of a key number, to allow unauthenticated
13
+ # access.
14
+ FREE_ACCESS = 0xE
15
+
16
+ # Used in file access rights in place of a key number, to indicate that no key
17
+ # is authorized.
18
+ DENY_ACCESS = 0xF
19
+
20
+ # The file access settings determine the keys needed to authenticate in order to
21
+ # perform some operations on files (inside an application), and how the file
22
+ # transfer are performed. Those two separate concepts are called in the DESFire
23
+ # documentation "file access rights" and "communication settings".
24
+ #
25
+ # The keys can be a key number from the application (from 0x0 to 0xD) or the
26
+ # constants +FREE_ACCESS+ (no authentication needed) or +DENY_ACCESS+ (the operation
27
+ # cannot be performed) (constants defined in the +Rights+ module).
28
+ class FileAccess
29
+
30
+ # Key to be able to read.
31
+ attr_reader :read
32
+
33
+ # Key to be able to write.
34
+ attr_reader :write
35
+
36
+ # Key to be able to read and write.
37
+ attr_reader :rw
38
+
39
+ # Key to change the file access rights.
40
+ attr_reader :change
41
+
42
+ # Do the data transfer to/from the file need to authenticated or ciphered? The
43
+ # valid values are the <tt>CS_...</tt> constants defined in the +Rights+
44
+ # module. +com_set+ stands for "communication settings".
45
+ attr_reader :com_set
46
+
47
+ # The constructor takes a hash of parameters, which correspond to the class
48
+ # attributes. See the class documentation for an explanation of valid key
49
+ # values.
50
+ def initialize(hash={})
51
+ @read = hash[:read] || DENY_ACCESS
52
+ @write = hash[:write] || DENY_ACCESS
53
+ @rw = hash[:rw] || DENY_ACCESS
54
+ @change = hash[:change] || DENY_ACCESS
55
+ @com_set = hash[:com_set] || CS_CIPHERED
56
+ end
57
+
58
+ # Builds and returns a +FileAccess+ object from the byte array returned by the
59
+ # +GET_FILE_SETTINGS+ command.
60
+ def self.from_a(ba)
61
+ FileAccess.new(
62
+ :read => ba[3] >> 4,
63
+ :write => ba[3] % 16,
64
+ :rw => ba[2] >> 4,
65
+ :change => ba[2] % 16,
66
+ :com_set => ba[1])
67
+ end
68
+
69
+ # Returns a two-byte little-endian view of the access rights (not the
70
+ # communication settings), fit to be used as a command parameter.
71
+ def to_a
72
+ [rw*16 + change, read*16 + write]
73
+ end
74
+
75
+ # Can the file be read when authenticated with the given key, and how? Returns
76
+ # either +false+ or a <tt>CS_...</tt> constant (see +Rights+ module
77
+ # documentation).
78
+ def read_type(key_no)
79
+ if @read == key_no or @rw == key_no
80
+ return @com_set
81
+ elsif @read == FREE_ACCESS or @rw == FREE_ACCESS
82
+ return CS_PLAIN
83
+ else
84
+ return false
85
+ end
86
+ end
87
+
88
+ # Can the file be written when authenticated with the given key, and how?
89
+ # Returns either +false+ or a <tt>CS_...</tt> constant (see +Rights+ module
90
+ # documentation).
91
+ def write_type(key_no)
92
+ if @write == key_no or @rw == key_no
93
+ return @com_set
94
+ elsif @write == FREE_ACCESS or @rw == FREE_ACCESS
95
+ return CS_PLAIN
96
+ else
97
+ return false
98
+ end
99
+ end
100
+
101
+ end
102
+
103
+ end
@@ -0,0 +1,270 @@
1
+ require "wx"
2
+ require_relative 'vault'
3
+ include Wx
4
+
5
+ module VaultGUI
6
+ TIMEOUT = 300
7
+
8
+ # Initialize the vault
9
+ @@vault = Vault.new
10
+
11
+ # Return the vault associated to the GUI.
12
+ def self.vault
13
+ return @@vault
14
+ end
15
+
16
+ class GUILauncher < App
17
+ def on_init
18
+ GUI.new.show
19
+ end
20
+ end
21
+
22
+ class GUI < Frame
23
+ def initialize
24
+ super(nil, 0, "Password Vault GUI",
25
+ :style => DEFAULT_FRAME_STYLE ^ RESIZE_BORDER ^ MAXIMIZE_BOX)
26
+
27
+ ask_password()
28
+
29
+ vbox = BoxSizer.new(VERTICAL)
30
+ pl = PasswordList.new(self)
31
+ evt_list_item_activated(pl) { |event|
32
+ item_activated(pl.get_item(event.get_index, 0).get_text())
33
+ }
34
+ panel = Panel.new(self)
35
+
36
+ hbox = BoxSizer.new(HORIZONTAL)
37
+ add_button = Button.new(panel, :label=>"ADD", :size => [80, 20])
38
+ evt_button(add_button) {
39
+ check_and_reset_timer()
40
+ add_pushed(pl)
41
+ }
42
+ remove_button = Button.new(panel, :label=>"REMOVE", :size => [80, 20])
43
+ evt_button(remove_button) {
44
+ check_and_reset_timer()
45
+ remove_pushed(pl)
46
+ }
47
+ edit_button = Button.new(panel, :label=>"EDIT", :size => [80, 20])
48
+ evt_button(edit_button) {
49
+ check_and_reset_timer()
50
+ edit_pushed(pl)
51
+ }
52
+ hbox.add(add_button)
53
+ hbox.add(remove_button)
54
+ hbox.add(edit_button)
55
+ panel.set_sizer(hbox)
56
+
57
+ vbox.add(pl)
58
+ vbox.add(panel)
59
+ self.set_sizer(vbox)
60
+ vbox.set_size_hints( self )
61
+ self.centre
62
+
63
+ evt_close() { |event|
64
+ VaultGUI.vault.destroy
65
+ self.destroy
66
+ }
67
+
68
+ @timer = Time.now.to_i
69
+ end
70
+
71
+ def ask_password()
72
+ password_popup = PasswordPopup.new(self)
73
+ if password_popup.show_modal == ID_OK
74
+ pass_candidate = password_popup.get_value()
75
+ begin
76
+ VaultGUI.vault.authenticate(pass_candidate)
77
+ rescue => e
78
+ Kernel.raise e unless e.message == Vault::AUTHENTICATION_ERROR
79
+ ErrorPopup.new("Error: wrong password!", "Error: wrong password!", true)
80
+ end
81
+ else
82
+ Kernel.exit
83
+ end
84
+ end
85
+
86
+ def check_and_reset_timer()
87
+ now = Time.now.to_i
88
+ if now - @timer > TIMEOUT
89
+ VaultGUI.vault.deauthenticate()
90
+ ask_password()
91
+ end
92
+ @timer = now
93
+ end
94
+
95
+ def item_activated(name)
96
+ info_popup = ViewPopup.new(self)
97
+ loginPassword = VaultGUI.vault.credential(name)
98
+ info_popup.set_name_value(name)
99
+ info_popup.set_login_value(loginPassword[0])
100
+ info_popup.set_password_value(loginPassword[1])
101
+ info_popup.show
102
+ evt_button(info_popup) {
103
+ info_popup.destroy
104
+ }
105
+ end
106
+
107
+ def add_pushed(pl)
108
+ add_popup = AddPopup.new(self)
109
+ add_popup.show
110
+ evt_button(add_popup) {
111
+ new_name = add_popup.get_name_value
112
+ new_login = add_popup.get_login_value
113
+ new_password = add_popup.get_password_value
114
+ begin
115
+ VaultGUI.vault.add(new_name, new_login, new_password)
116
+ pl.insert_item(ListItem.new)
117
+ add_popup.destroy
118
+ rescue => e
119
+ display_error_if_needed(e)
120
+ add_popup.destroy
121
+ end
122
+ }
123
+ end
124
+
125
+ def remove_pushed(pl)
126
+ selection = pl.get_selections
127
+ if selection.length > 0
128
+ index_deleted = selection[0]
129
+ deleted = pl.get_item(index_deleted, 0).get_text
130
+ pl.delete_item(index_deleted)
131
+ VaultGUI.vault.remove(deleted)
132
+ end
133
+ end
134
+
135
+ def edit_pushed(pl)
136
+ selection = pl.get_selections
137
+ if selection.length > 0
138
+ index_edited = selection[0]
139
+ edit_popup = EditPopup.new(self)
140
+ edit_popup.show
141
+ evt_button(edit_popup) {
142
+ name = pl.get_item(index_edited, 0).get_text
143
+ new_login = edit_popup.get_login_value
144
+ new_password = edit_popup.get_password_value
145
+ begin
146
+ VaultGUI.vault.edit(name, new_login, new_password)
147
+ edit_popup.destroy
148
+ rescue => e
149
+ display_error_if_needed(e)
150
+ end
151
+ }
152
+ end
153
+ end
154
+
155
+ def display_error_if_needed(e)
156
+ if e.class == Vault::CredentialError
157
+ ErrorPopup.new(e.message, "Credential error", false)
158
+ else
159
+ Kernel.raise e
160
+ end
161
+ end
162
+ end
163
+
164
+ class PasswordList < ListCtrl
165
+ def initialize(parent)
166
+ super(parent,
167
+ :style=>LC_REPORT | LC_VIRTUAL | LC_SINGLE_SEL,
168
+ :size=>[240, 240])
169
+ insert_column(0, "Name")
170
+ set_column_width(0, 240)
171
+ set_item_count(VaultGUI.vault.credentials_names.count)
172
+ end
173
+
174
+ def on_get_item_text(item, column)
175
+ return VaultGUI.vault.credentials_names.to_a[item]
176
+ end
177
+ end
178
+
179
+ class ErrorPopup < MessageDialog
180
+ def initialize (message, caption, kill)
181
+ super(nil,
182
+ :message => message,
183
+ :caption => caption,
184
+ :style => OK | ICON_ERROR)
185
+ if self.show_modal == ID_OK && kill
186
+ VaultGUI.vault.destroy
187
+ Kernel.exit
188
+ end
189
+ end
190
+ end
191
+
192
+ class PasswordPopup < TextEntryDialog
193
+ def initialize(parent)
194
+ super(parent,
195
+ :message => "Enter your password:",
196
+ :caption => "Enter your password",
197
+ :style => OK | TE_PASSWORD)
198
+ end
199
+ end
200
+
201
+ class InfoPopup < Frame
202
+ def initialize(parent, title, edit_name_tf, edit_login_tf, edit_password_tf)
203
+ super(parent, 0, title,
204
+ :style => DEFAULT_FRAME_STYLE ^ RESIZE_BORDER ^ MAXIMIZE_BOX)
205
+ panel = Panel.new(self)
206
+ namel = StaticText.new(panel, 0, "Name", :style => ALIGN_CENTER)
207
+ @name_tf = TextCtrl.new(panel, 0)
208
+ @name_tf.set_editable(edit_name_tf)
209
+ loginl = StaticText.new(panel, 0, "Login", :style => ALIGN_CENTER)
210
+ @login_tf = TextCtrl.new(panel, 0)
211
+ @login_tf.set_editable(edit_login_tf)
212
+ password_l = StaticText.new(panel, 0, "Password", :style => ALIGN_CENTER)
213
+ @password_tf = TextCtrl.new(panel, 0, :style => TE_PASSWORD)
214
+ @password_tf.set_editable(edit_password_tf)
215
+ okb = Button.new(panel, 0, "OK")
216
+ vbox = BoxSizer.new(VERTICAL)
217
+ vbox.add(namel)
218
+ vbox.add(@name_tf)
219
+ vbox.add(loginl)
220
+ vbox.add(@login_tf)
221
+ vbox.add(password_l)
222
+ vbox.add(@password_tf)
223
+ vbox.add(okb)
224
+ panel.set_sizer(vbox)
225
+ self.centre
226
+ end
227
+
228
+ def get_name_value()
229
+ return @name_tf.get_value
230
+ end
231
+
232
+ def get_login_value()
233
+ return @login_tf.get_value
234
+ end
235
+
236
+ def get_password_value()
237
+ return @password_tf.get_value
238
+ end
239
+
240
+ def set_name_value(name)
241
+ @name_tf.set_value(name)
242
+ end
243
+
244
+ def set_login_value(login)
245
+ @login_tf.set_value(login)
246
+ end
247
+
248
+ def set_password_value(password)
249
+ @password_tf.set_value(password)
250
+ end
251
+ end
252
+
253
+ class EditPopup < InfoPopup
254
+ def initialize(parent)
255
+ super(parent, "Edit entry", false, true, true)
256
+ end
257
+ end
258
+
259
+ class AddPopup < InfoPopup
260
+ def initialize(parent)
261
+ super(parent, "Add new entry", true, true, true)
262
+ end
263
+ end
264
+
265
+ class ViewPopup < InfoPopup
266
+ def initialize(parent)
267
+ super(parent, "View entry", false, false, false)
268
+ end
269
+ end
270
+ end