passvault 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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