passvault 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,116 @@
1
+ # Uncommented constants are DESFire comments.
2
+ class Connection
3
+ # This file holds the constants used for data transmission and commands.
4
+ #
5
+ # In another language, we would have spread the constants accross multiple
6
+ # classes (for the card, chip and reader constants) and then imported those
7
+ # constants. But in Ruby doing so might cause the constants to silently
8
+ # collide. Putting them in the same class enables warnings in case of constant
9
+ # redefinition.
10
+
11
+ # ============================================================================
12
+ # DESFire (card) related constants
13
+ # ============================================================================
14
+
15
+ # fixed fields values
16
+
17
+ # CLA field for DESFire commands.
18
+ CARD_CLA = 0x90
19
+
20
+ # SW1 field for DESFire answers.
21
+ SW1 = 0x91
22
+
23
+ # Hash from DESFire response codes name (symbols) to the codes (integers).
24
+ STATUS = {
25
+ :OPERATION_OK => 0x00,
26
+ :ADDITIONAL_FRAME => 0xAF,
27
+ :NO_CHANGES => 0x0C,
28
+ :ILLEGAL_COMMAND_CODE => 0x1C,
29
+ :INTEGRITY_ERROR => 0x1E,
30
+ :NO_SUCH_KEY => 0x40,
31
+ :LENGTH_ERROR => 0x7E,
32
+ :PERMISSION_DENIED => 0x9D,
33
+ :PARAMETER_ERROR => 0x9E,
34
+ :APPLICATION_NOT_FOUND => 0xA0,
35
+ :APPLICATION_INTEGRITY_ERROR => 0xA1,
36
+ :AUTHENTICATION_ERROR => 0xAE,
37
+ :BOUNDARY_ERROR => 0xBE,
38
+ :COMMAND_ABORTED => 0xCA,
39
+ :DUPLICATE_ERROR => 0xDE,
40
+ :FILE_NOT_FOUND => 0xF0,
41
+ :OUT_OF_EEPROM_ERROR => 0x0E,
42
+ }
43
+
44
+ CHANGE_KEY = 0xC4
45
+ CREATE_APPLICATION = 0xCA
46
+ CREATE_STD_DATA_FILE = 0xCD
47
+ GET_CIPHERED_NONCE = 0x0A
48
+ SELECT_APPLICATION = 0x5A
49
+ SEND_MORE_DATA = 0xAF
50
+ FORMAT_PICC = 0xFC
51
+ GET_APP_IDS = 0x6A
52
+ GET_FILE_IDS = 0x6F
53
+ DELETE_APP = 0xDA
54
+ DELETE_FILE = 0xDF
55
+ READ_DATA = 0xBD
56
+ WRITE_DATA = 0x3D
57
+ GET_KEY_SETTINGS = 0x45
58
+ GET_FILE_SETTINGS = 0xF5
59
+ GET_VERSION = 0x60
60
+ CHANGE_KEY_SETTINGS = 0x54
61
+
62
+ # Padding beginning marker when reading a whole file with encrypted
63
+ # communication.
64
+ WHOLE_FILE_PAD_MARKER = "\x80"
65
+
66
+ # ============================================================================
67
+ # PN532 (chip) related constants
68
+ # ============================================================================
69
+
70
+ # Communication direction: send data to the chip.
71
+ TO_CHIP = 0xD4
72
+
73
+ # Communicaiton direction: receive data from the chip.
74
+ FROM_CHIP = 0xD5
75
+
76
+ # PN532 command: poll for tags.
77
+ LIST_PASSIVE_TARGET = 0x4A
78
+
79
+ # PN532 command: send data to/from the chip.
80
+ DATA_EXCHANGE = 0x40
81
+
82
+ # PN532 response code (prefix).
83
+ LIST_RESPONSE = 0x4B
84
+
85
+ # PN532 data response codes (array) (prefix).
86
+ DATA_RESPONSE = [0x41, 0x00]
87
+
88
+ # PN532 success response codes (array) (suffix).
89
+ SUCCESS_SUFFIX = [0x90, 0x00]
90
+
91
+ # ============================================================================
92
+ # ACR112 (reader) releated constants
93
+ # ============================================================================
94
+
95
+ # ACR112: class field for pseudo-APDU commands.
96
+ READER_CLA = 0xFF
97
+
98
+ # ACR112 pseudo-APDU commands: send data.
99
+ DIRECT_TRANSMIT = 0x00
100
+
101
+ # ACR112 pseudo-APDU commands: retrieve an answer.
102
+ GET_RESPONSE = 0xC0
103
+
104
+ # ACR112 response code (SW1 field).
105
+ SUCCESS = 0x61
106
+
107
+ # ACR112 response code (SW1 field).
108
+ ERROR = 0x63
109
+
110
+ # ACR112 response codes (SW2 field for SW1=ERROR).
111
+ OPERATION_FAILED = 0x00
112
+
113
+ # ACR112 response codes (SW2 field for SW1=ERROR).
114
+ NO_ANSWER = 0x01
115
+
116
+ end
@@ -0,0 +1,92 @@
1
+ class Connection
2
+ # This files contains logic related to the initialization and termination of the
3
+ # connection.
4
+
5
+ # Connect to a card trough a reader.
6
+ def initialize()
7
+ super({}) # no options
8
+
9
+ @context = PCSC::Context.new
10
+
11
+ # find readers
12
+ begin
13
+ reader_names = @context.readers
14
+ rescue Exception => e
15
+ puts "No smartcard readers were detected."
16
+ disconnect
17
+ exit
18
+ end
19
+
20
+ # select a single reader
21
+ if reader_names.length == 1
22
+ @reader_name = reader_names.first
23
+ else
24
+ puts "Multiple readers available, please select one by number."
25
+ @reader_name = nil
26
+ while @reader_name == nil
27
+ reader_names.each_with_index do |r,i|
28
+ puts "#{i}) #{r.strip}"
29
+ end
30
+ begin
31
+ @reader_name = reader_names[gets.strip.to_i]
32
+ rescue
33
+ puts "Invalid selection."
34
+ end
35
+ end
36
+ end
37
+
38
+ # The following two lines are lifted from the supermethod, since they were
39
+ # the only thing of interest there, due to idiosyncraties of the ACR122.
40
+
41
+ # The card object is used to transmit data, tough we don't need to use it
42
+ # directly, as data transmission is wrapped by the exchange_apdu(apdu)
43
+ # function.
44
+
45
+ @card = @context.card(@reader_name, :shared)
46
+
47
+ # the Answer To Reset for the card
48
+ @atr = @card.info[:atr]
49
+
50
+ # Wait until a card is inserted in the reader. Normally the super() call
51
+ # takes care of this, but the ACR122 is a broken beast which always
52
+ # indicates that a card is present, even if it isn't the case.
53
+
54
+ msg_displayed = false
55
+ while true
56
+ begin
57
+ poll
58
+ break
59
+ rescue Smartcard::PCSC::Exception => e
60
+ status = e.pcsc_status_code
61
+ if status == Smartcard::PCSC::FFILib::Status[:comm_data_lost]
62
+ puts "No cards detected, please insert your card." if !msg_displayed
63
+ msg_displayed = true
64
+ else
65
+ puts "error: #{e.message}"
66
+ disconnect
67
+ exit
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ # Search for a card. This succeeds if a card is detected, else it throws a
74
+ # <tt>Smartcard::PCSC::Exception</tt> with +pcsc_status_code+ field set to the
75
+ # <tt>:comm_data_lost</tt> status after some time.
76
+ def poll(max_tags = 1, baud = 0)
77
+ response = send_chip_cmd LIST_PASSIVE_TARGET, [max_tags, baud]
78
+ throw :protocol_error unless response[0] == LIST_RESPONSE
79
+ end
80
+
81
+ # Release all resources associated with the card.
82
+ def disconnect
83
+ # :unpower is necessary if we want to relaunch the program without unpluging
84
+ # the reader.
85
+ unless @card.nil?
86
+ @card.disconnect(:unpower)
87
+ @card = nil
88
+ end
89
+ super
90
+ end
91
+
92
+ end
@@ -0,0 +1,96 @@
1
+ require_relative 'crypto'
2
+
3
+ class Connection
4
+ # This file holds the implementation of the communication logic between the
5
+ # computer and the card. There are three "hardware" levels we need to go
6
+ # trought in order to send data to the card: the reader, the chip and the card
7
+ # itself. However the whole picture is a little more complex and looks like
8
+ # this:
9
+ #
10
+ # +---------------------------------------------------------------+
11
+ # | card_cmd -> card_apdu -> chip_cmd -> chip_apdu -> reader_apdu |
12
+ # +---------------------------------------------------------------+
13
+ #
14
+ # To each of the arrows in the diagram above correspond a method named
15
+ # send_<thing_before_the_arrow>. The function transforms the thing before the
16
+ # arrow into the thing after the arrow and passes it along the
17
+ # chain. Afterwards it gets a response for the thing after the arrow and
18
+ # transforms it into a response for the thing before the arrow, which is then
19
+ # returned.
20
+ #
21
+ # Transforming the responses usually consists of stripping some response codes
22
+ # from the output.
23
+
24
+ include Crypto
25
+
26
+ # Thrown when the contact with the card is lost.
27
+ CARD_CONTACT_LOST_ERROR =
28
+ 'Contact with the tag was lost, please verify that the card is in the ' \
29
+ 'reader, then relaunch the application.'
30
+
31
+ # Send a chip APDU trough the reader (by wrapping the chip APDU inside a
32
+ # reader pseudo-APDU) and return the chip response. This takes care of the
33
+ # reader response codes.
34
+ def send_chip_apdu(chip_apdu)
35
+ nbytes = chip_apdu.length
36
+ pseudo_apdu = [READER_CLA, DIRECT_TRANSMIT, 0x00, 0x00, nbytes, *chip_apdu]
37
+ reader_response = exchange_apdu(pseudo_apdu)
38
+
39
+ # check the reader response
40
+ case reader_response[0]
41
+ when ERROR
42
+ throw :operation_failed if reader_response[1] == OPERATION_FAILED
43
+ throw :no_answer if reader_response[1] == NO_ANSWER
44
+ throw :unknown_error
45
+ when SUCCESS
46
+ # fetch the actual card/chip response
47
+ fetch_len = reader_response[1]
48
+ pseudo_apdu = [READER_CLA, GET_RESPONSE, 0x00, 0x00, fetch_len]
49
+ exchange_apdu(pseudo_apdu)
50
+ else
51
+ throw :protocol_error
52
+ end
53
+ end
54
+
55
+ # Send a chip command and return the command response (not comprising the chip
56
+ # response codes).
57
+ #
58
+ # There are two useful commands: +DATA_EXCHANGE+ (used in send_car_apdu()) and
59
+ # +LIST_PASSIVE_TARGET+ (used by <tt>poll()</tt>).
60
+ def send_chip_cmd(cmd, args)
61
+ chip_apdu = [TO_CHIP, cmd, *args]
62
+ response = send_chip_apdu(chip_apdu)
63
+ len = response.length
64
+ throw :protocol_error unless
65
+ response[0] == FROM_CHIP &&
66
+ response[len-2..len-1] == SUCCESS_SUFFIX
67
+ return response[1..len-3]
68
+ end
69
+
70
+ # Send a card APDU to the card and return the card's answer. This takes care
71
+ # of chip command DATA_EXCHANGE return codes.
72
+ def send_card_apdu(card_apdu, tag_no=1)
73
+ response = send_chip_cmd(DATA_EXCHANGE, [tag_no, *card_apdu])
74
+ raise CARD_CONTACT_LOST_ERROR unless response[0..1] == DATA_RESPONSE
75
+ return response[2..response.length-1]
76
+ end
77
+
78
+ # Send a command to the card and return an array whose first element is the
79
+ # command response (not comprising the card response codes) and the second is
80
+ # a successful status (needed to know if additional frames are available).
81
+ def send_card_cmd(cmd, args=[])
82
+ apdu = args.length == 0 \
83
+ ? [CARD_CLA, cmd, 0x00, 0x00, 0x00]
84
+ : [CARD_CLA, cmd, 0x00, 0x00, args.length, *args, 0x00]
85
+ response = send_card_apdu(apdu)
86
+ len = response.length
87
+ throw :protocol_error unless response[len-2] == SW1
88
+ status = response[len-1]
89
+ throw STATUS.key(status), true unless
90
+ status == STATUS[:OPERATION_OK] ||
91
+ status == STATUS[:ADDITIONAL_FRAME]
92
+
93
+ return [len < 3 ? [] : response[0..len-3], status]
94
+ end
95
+
96
+ end
@@ -0,0 +1,28 @@
1
+ require 'smartcard'
2
+
3
+ # An instance of the class Connection represents a connection to a DESFire tag
4
+ # trough an ACR122 reader outfitted with PN532 chip. In our passworld vault
5
+ # program there is only one instance of this class.
6
+ #
7
+ # The class extends the class Smartcard::Iso::PcscTransport from which it
8
+ # inherits most notably the method <tt>exchange_apdu(apdu)</tt> which is used to
9
+ # send APDUs (Application Protocol Data Unit) to the reader.
10
+ #
11
+ # We found out that object-oriented decomposition didn't work well to model the
12
+ # mechanisms of data transmission to the card, as it is more functional in
13
+ # nature. We elected to model inside this single class. To improve readability,
14
+ # the class is split inside multiple files that cover different concerns. The
15
+ # files are described below in the <tt>connection.rb</tt> file.
16
+ class Connection < Smartcard::Iso::PcscTransport ; end
17
+
18
+ # Initialization and termination of the connection.
19
+ require_relative 'conn_init'
20
+
21
+ # Constants used for data transmission and commands.
22
+ require_relative 'conn_constants'
23
+
24
+ # Tranmission of APDUs at various levels (reader, chip, card).
25
+ require_relative 'conn_transmit'
26
+
27
+ # Functions wrapping commands to be sent to the card.
28
+ require_relative 'conn_cmds'
@@ -0,0 +1,243 @@
1
+ require 'clipboard'
2
+ require 'highline/import'
3
+ require 'smartcard'
4
+
5
+ require_relative 'vault'
6
+
7
+ class ConsoleUI
8
+
9
+ # Displayed when running the console program.
10
+ HEADER = "
11
+ _____ _
12
+ | _ |___ ___ ___ _ _ _ ___ ___ _| |
13
+ | __| .'|_ -|_ -| | | | . | _| . |
14
+ |__| |__,|___|___|_____|___|_| |___|
15
+
16
+
17
+ _____ _ _
18
+ | | |___ _ _| | |_
19
+ | | | .'| | | | _|
20
+ \\___/|__,|___|_|_|
21
+
22
+
23
+ (Type \"help\" to display available commands.)"
24
+
25
+ # Time after which the the console program forgets the vault master password
26
+ # and the associated key.
27
+ TIMEOUT = 300
28
+
29
+ # The console program entry point.
30
+ def self.run
31
+ ui = ConsoleUI.new()
32
+ ui.loop()
33
+ rescue Smartcard::PCSC::Exception => e
34
+ case e.pcsc_status
35
+ when :reader_unavailable
36
+ puts "\nThe reader was disconnected or is otherwise unavailable. " \
37
+ "You can't disconnect the card during the execution of the program, " \
38
+ "even if you reconnect it before executing a command."
39
+ else
40
+ puts "\n#{e.message()}"
41
+ end
42
+ rescue Exception => e
43
+ puts "\n#{e.message()}"
44
+ ensure
45
+ ui.leave() if ui
46
+ end
47
+
48
+ # Loop on user input (commands).
49
+ def loop
50
+ puts HEADER
51
+ @continue = true
52
+ while @continue
53
+ if catch :timeout do
54
+ command = request('> ')
55
+ execute(command.strip)
56
+ false
57
+ end then
58
+ @vault.deauthenticate()
59
+ puts 'The prompt timed out, please re-do the last command.'
60
+ end
61
+ end
62
+ end
63
+
64
+ # Exit the console program.
65
+ def leave
66
+ @vault.destroy()
67
+ end
68
+
69
+ private ######################################################################
70
+
71
+ # Initialize the UI by creating a new +Vault+ object.
72
+ def initialize
73
+ @vault = Vault.new()
74
+ end
75
+
76
+ # Display the help of the console program.
77
+ def help()
78
+ puts ''
79
+ puts '-- user commands --'
80
+ puts 'list : list the names of registered credentials'
81
+ puts 'display <name> : display a registered credential'
82
+ puts 'clip <name> : same as "display", but copies the password'
83
+ puts ' to the clipboard instead of displaying it'
84
+ puts 'add <name> : register a new credential'
85
+ puts 'remove <name> : remove a registered credential'
86
+ puts 'edit <name> : change a registered credential'
87
+ puts 'quit : exit the program'
88
+ puts 'help : display this help message'
89
+ puts ''
90
+ puts '-- administration commands --'
91
+ puts 'reset : reinitialize the vault'
92
+ puts 'erase : erase the vault from the tag'
93
+ puts ''
94
+ end
95
+
96
+ # Ask the user for input, and throws <tt>:timeout</tt> (with value +true+) if
97
+ # the user is inactive for more than +TIMEOUT+ seconds.
98
+ def request(prompt, &block)
99
+ time = Time.now.to_i
100
+ answer = ask("\n#{prompt}", &block)
101
+ time = Time.now.to_i - time
102
+ throw :timeout, true if time > TIMEOUT
103
+ answer
104
+ end
105
+
106
+ # Prompt the user for a password. The password won't show on the screen (like
107
+ # in the Unix login prompt).
108
+ def enter_pass(prompt)
109
+ request(prompt) do |p| p.echo = false end
110
+ end
111
+
112
+ # Prompt for the tag's master password. Allows for a special case if the
113
+ # password is left empty.
114
+ def enter_tag_pass
115
+ master_prompt = 'Enter the tag master password (leave empty for default): '
116
+ pass = enter_pass(master_prompt)
117
+ pass = :default if pass == ""
118
+ return pass
119
+ end
120
+
121
+ # If not authenticated with the vault, prompt for the vault pass and perform
122
+ # the authentication.
123
+ def check_vault_pass
124
+ return true if @vault.authenticated?()
125
+ pass = enter_pass(
126
+ "Enter the vault master password \n" \
127
+ "(you will need to re-enter it after 5 minutes of inactivity): ")
128
+ @vault.authenticate(pass)
129
+ return true
130
+ rescue => e
131
+ return handle_auth_error(e)
132
+ end
133
+
134
+ # Re-raise the exception if it is not <tt>Vault::AUTHENTICATION_ERROR</tt>,
135
+ # else display the approrpiate message and return false.
136
+ def handle_auth_error(exception)
137
+ raise exception unless exception.message == Vault::AUTHENTICATION_ERROR
138
+ puts "\n#{exception.message}"
139
+ return false
140
+ end
141
+
142
+ # Execute a command entered by the user.
143
+ def execute(command)
144
+ # The regex will set $1 to the credential name.
145
+ name_regex = '(?<name>[[:graph:]]*)'
146
+ case command
147
+ when 'erase' ; erase()
148
+ when 'create', 'reset' ; reset()
149
+ when 'help' ; help()
150
+ when 'quit' ; @continue = false
151
+ when 'list' ; list()
152
+ when /display #{name_regex}/ ; display($1)
153
+ when /clip #{name_regex}/ ; clip($1)
154
+ when /add #{name_regex}/ ; add($1)
155
+ when /remove #{name_regex}/ ; remove($1)
156
+ when /edit #{name_regex}/ ; edit($1)
157
+ else ; unknown(command)
158
+ end
159
+ rescue Exception => e
160
+ raise e unless e.message == Vault::UNKNOWN_NAME_ERROR
161
+ puts "\n#{e.message}"
162
+ end
163
+
164
+ # See <tt>help()</tt> for a description.
165
+ def erase()
166
+ tag_pass = enter_tag_pass()
167
+ @vault.erase(tag_pass)
168
+ rescue => e
169
+ return handle_auth_error(e)
170
+ end
171
+
172
+ # See <tt>help()</tt> for a description.
173
+ def reset()
174
+ tag_pass = enter_tag_pass()
175
+ return unless tag_pass
176
+ vault_pass = enter_pass('Enter the new vault pass: ')
177
+ @vault.reset(tag_pass, vault_pass)
178
+ rescue => e
179
+ return handle_auth_error(e)
180
+ end
181
+
182
+ # See <tt>help()</tt> for a description.
183
+ def list()
184
+ return unless check_vault_pass()
185
+ puts "\nList of available credentials:"
186
+ @vault.credentials_names().each { |name| puts "- #{name}" }
187
+ end
188
+
189
+ # See <tt>help()</tt> for a description.
190
+ def display(name)
191
+ return unless check_vault_pass()
192
+ login, pass = @vault.credential(name)
193
+ puts ""
194
+ puts "login: #{login}"
195
+ puts "pass: #{pass}"
196
+ end
197
+
198
+ # See <tt>help()</tt> for a description.
199
+ def clip(name)
200
+ return unless check_vault_pass()
201
+ login, pass = @vault.credential(name)
202
+ puts ""
203
+ puts "login name: #{login}"
204
+ Clipboard.copy(pass)
205
+ end
206
+
207
+ # See <tt>help()</tt> for a description.
208
+ def add(name)
209
+ return unless check_vault_pass()
210
+ login = request('Enter a login: ')
211
+ pass = enter_pass('Enter a password: ')
212
+ @vault.add(name, login, pass)
213
+ rescue Vault::CredentialError => e
214
+ puts "\n#{e.message}"
215
+ end
216
+
217
+ # See <tt>help()</tt> for a description.
218
+ def remove(name)
219
+ return unless check_vault_pass()
220
+ @vault.remove(name)
221
+ end
222
+
223
+ # See <tt>help()</tt> for a description.
224
+ def edit(name)
225
+ return unless check_vault_pass()
226
+ login = request('Enter a login: ')
227
+ pass = enter_pass('Enter a new password: ')
228
+ @vault.edit(name, login, pass)
229
+ rescue Vault::CredentialError => e
230
+ puts "\n#{e.message}"
231
+ end
232
+
233
+ # Called when an unknown command is entered.
234
+ def unknown(command)
235
+ puts "\nUnknown command: #{command}"
236
+ puts 'Type "help" to display available commands.'
237
+ end
238
+ end
239
+
240
+ # # entry point
241
+ # if __FILE__ == $PROGRAM_NAME
242
+ # ConsoleUI.run
243
+ # end