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,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