ctapi 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/ext/extconf.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'mkmf'
2
+ require 'fileutils'
3
+
4
+ libraryname = ARGV.shift or fail "A ctapi library name is required!"
5
+ if have_library(libraryname)
6
+ create_makefile('ctapicore')
7
+ end
8
+ # vim: set et sw=2 ts=2:
data/install.rb ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rbconfig'
4
+ require 'fileutils'
5
+ include FileUtils::Verbose
6
+
7
+ include Config
8
+
9
+ libraryname = ARGV.shift or fail "A ctapi library name is required!"
10
+ Dir.chdir('ext') do
11
+ File.exist?('Makefile') and system("make clean")
12
+ system("ruby extconf.rb #{libraryname}") or
13
+ fail "Could not make makefile for library!"
14
+ system("make") or fail "Could not make library"
15
+ end
16
+
17
+ libdir = CONFIG["sitelibdir"]
18
+ for file in %w[ext/ctapicore.so lib/ctapi.rb]
19
+ install(file, libdir)
20
+ end
21
+ # vim: set et sw=2 ts=2:
data/lib/ctapi.rb ADDED
@@ -0,0 +1,563 @@
1
+ # There are two possible ways to use this library:
2
+ #
3
+ # 1. You can require 'ctapicore' and directly use the ct_init, ct_data and
4
+ # ct_close methods . That will give you access to the very simple
5
+ # C-Interface in Ruby, but you have to send APDU commands and receive and
6
+ # interpret APDU Responses yourself:
7
+ #
8
+ # require 'ctapicore'
9
+ # include CTAPICore
10
+ # ct_init(PORT_COM1, 0)
11
+ # response = ct_data(0, CT, HOST, "\x12\x34\x56")
12
+ # p response
13
+ # ct_close(0)
14
+ #
15
+ # 2. You can require 'ctapi' and the module CTAPI has some nice abstractions to
16
+ # deal with cardterminals, cards and APDUs, which are much more the Ruby
17
+ # Way:
18
+ #
19
+ # require 'ctapi'
20
+ # include CTAPI
21
+ # Cardterminal.open(PORT_COM1) do |ct|
22
+ # puts "Cardterminal Manufacturer: " + ct.manufacturer.to_s
23
+ # if ct.card_inserted?
24
+ # card = ct.card
25
+ # puts "My Cardterminal object: #{ct.inspect}"
26
+ # puts "Current card status is: #{ct.card_status}"
27
+ # puts "Answer to Reset is #{card.atr}."
28
+ # puts "ATR ok? #{card.atr_ok?}."
29
+ # puts "Memory size of this card is #{card.memory_size} bytes" +
30
+ # (#{card.memory_blocks} blocks x #{card.memory_bits} bit)."
31
+ # puts "Structure of this card is #{card.structure}."
32
+ # puts "Supported protocol type of this card is #{card.protocol}."
33
+ # puts "Trying to read(0, 16):"
34
+ # data = ct.read(0, 16)
35
+ # puts "Have read #{data.size} bytes:"
36
+ # p data
37
+ # else
38
+ # puts "Please insert a card into your cardterminal!"
39
+ # end
40
+ # end
41
+ #
42
+ module CTAPI
43
+
44
+ require 'ctapicore'
45
+ include CTAPICore
46
+
47
+ # Represents a cardterminal. Mixes in CTAPI methods and constants.
48
+ class Cardterminal
49
+
50
+ include CTAPI
51
+ @@cardterminals = {}
52
+
53
+ # Creates a new Cardterminal object for this interface and initializes
54
+ # the card terminal that is connected. If no cardterminal number is
55
+ # given, a number (that is not currently in use) is assigned to this
56
+ # object. Otherwise this object is forced to use the given card
57
+ # terminal number. BTW: number has to be >= 0.
58
+ def initialize(interface, number = nil)
59
+ Thread.critical = true
60
+ @interface = interface
61
+ @slot = ICC1
62
+ @chunk_size = 255
63
+ if number
64
+ @number = number
65
+ else
66
+ @number = 0
67
+ while @@cardterminals.key?(@number)
68
+ @number += 1
69
+ end
70
+ end
71
+ ct_init(@number, interface)
72
+ @@cardterminals[@number] = true
73
+ @manufacturer = get_manufacturer
74
+ reset
75
+ select_file # master file
76
+ ensure
77
+ Thread.critical = false
78
+ end
79
+
80
+ class << self
81
+ alias init new
82
+ end
83
+
84
+ # Returns a Manufacturer object with information about the manufacturer
85
+ # of this cardterminal. Could return nil if the response was not
86
+ # successful.
87
+ def get_manufacturer
88
+ get_manufacturer_status = [ CTBCS_CLA, CTBCS_INS_STATUS,
89
+ CTBCS_P1_CT_KERNEL, CTBCS_P2_STATUS_MANUFACTURER, 0 ]
90
+ response = data(CT, HOST, get_manufacturer_status)
91
+ return nil unless response.successful?
92
+ Manufacturer.new(response)
93
+ end
94
+ private :get_manufacturer
95
+
96
+ # The cardterminal number assigned to this object.
97
+ attr_reader :number
98
+
99
+ # The interface this cardterminal is connected to. This is
100
+ # a value of PORT_COM1, PORT_COM2, PORT_COM3, PORT_COM4, PORT_LPT1,
101
+ # PORT_LPT2, PORT_Modem or PORT_Printer.
102
+ attr_reader :interface
103
+
104
+ # The slot of the card terminal where the card is inserted. This
105
+ # is a value in the range of ICC1, ICC2,..., ICC14. Default is
106
+ # ICC1.
107
+ attr_accessor :slot
108
+
109
+ # The maximum size of the chunks of data that are read from/written to
110
+ # the card. Can be set by calling the chunk_size= method.
111
+ attr_reader :chunk_size
112
+
113
+ # Sets the size of the chunks of data that are read from/written to
114
+ # the card to size: (0 < size <= 255).
115
+ def chunk_size=(size)
116
+ raise ArgumentError.new("size must be > 0") unless size > 0
117
+ raise ArgumentError.new("size must be <= 255") unless size <= 255
118
+ @chunk_size = size
119
+ end
120
+
121
+ # The Manufacturer object for this cardterminal.
122
+ attr_reader :manufacturer
123
+
124
+ # Card object of the inserted chipcard.
125
+ attr_reader :card
126
+
127
+ # Takes a block and yields to an initialized Cardterminal object.
128
+ # After the block has been called, the cardterminal is closed
129
+ # again.
130
+ def self.open(interface, number = nil)
131
+ cardterminal = self.new(interface, cardterminal)
132
+ yield cardterminal
133
+ ensure
134
+ cardterminal.close if cardterminal
135
+ end
136
+
137
+ # Sends a sequence of commands to the card or the cardterminal
138
+ # (depending on destination/dad and source address/sad) and returns the
139
+ # response (or responses) to the calling program. A command can be
140
+ # given as an array of bytes [ 0x12, 0x23 ] or as a string of the form
141
+ # '12:23:a4' or '12 23 a4' or as a Command object.
142
+ def data(dad, sad, *commands)
143
+ responses = []
144
+ commands.each do |command|
145
+ command =
146
+ case command
147
+ when String then Command.from_string(command)
148
+ when Array then Command.from_array(command)
149
+ else command
150
+ end
151
+ $DEBUG and debug(2, command)
152
+ data = ct_data(@number, dad, sad, command.data)
153
+ response = Response.new(data)
154
+ $DEBUG and debug(1, response)
155
+ responses << response
156
+ end
157
+ return *responses
158
+ end
159
+
160
+ # Terminates the communication with the cardterminal associated with
161
+ # this object and releases the assigned cardterminal number.
162
+ def close
163
+ Thread.critical = true
164
+ ct_close(@number)
165
+ @@cardterminals.delete @number
166
+ ensure
167
+ Thread.critical = false
168
+ end
169
+
170
+ # Sends the select file byte sequence to the card with a default value
171
+ # for the master file. Returns true if the response was successful,
172
+ # false otherwise.
173
+ def select_file(fid = [ 0x3f, 0 ])
174
+ select_file = [ 0, 0xa4, 0, 0, 0x02, *fid ]
175
+ response = data(@slot, HOST, select_file)
176
+ response.successful?
177
+ end
178
+
179
+ def read_chunk(address, size)
180
+ unless address <= 65535
181
+ raise ArgumentError.new("address must be <= 65535")
182
+ end
183
+ unless size <= 255
184
+ raise ArgumentError.new("size must be <= 255")
185
+ end
186
+ read_file = [ 0, 0xb0, address >> 8, address & 0xff, size ]
187
+ response = data(@slot, HOST, read_file)
188
+ response.successful? or return
189
+ data = response.data
190
+ data.slice!(-2, 2)
191
+ data
192
+ end
193
+ private :read_chunk
194
+
195
+ # Attempts to read data of length size starting at address. If
196
+ # size is nil, an attempt is made to read the whole card memory.
197
+ def read(address = 0, size = nil)
198
+ if size == nil
199
+ if @card
200
+ size = @card.memory_size - address
201
+ else
202
+ size = chunk_size
203
+ end
204
+ elsif @card and address + size > @card.memory_size
205
+ size = @card.memory_size - address
206
+ end
207
+ return if size <= 0
208
+ data = ''
209
+ caught = catch(:break) do
210
+ while size >= chunk_size
211
+ d = read_chunk(address, chunk_size)
212
+ if d
213
+ data << d
214
+ address += chunk_size
215
+ size -= chunk_size
216
+ else
217
+ break :break
218
+ end
219
+ end
220
+ end
221
+ if caught != :break and size > 0
222
+ d = read_chunk(address, size) and data << d
223
+ end
224
+ data
225
+ end
226
+
227
+ def write_chunk(address, data)
228
+ unless address <= 65535
229
+ raise ArgumentError.new("address must be <= 65535")
230
+ end
231
+ unless data.size <= 255
232
+ raise ArgumentError.new("size of data must be <= 255")
233
+ end
234
+ data = data.unpack("C*") if data.is_a? String
235
+ write_file = [ 0, 0xd6, address >> 8, address & 0xff, data.size ] +
236
+ data
237
+ response = data(@slot, HOST, write_file)
238
+ response.successful? or return
239
+ true
240
+ end
241
+ private :write_chunk
242
+
243
+ # Attempts to write the string data to the card starting at address.
244
+ # On success returns a true value.
245
+ def write(data, address = 0)
246
+ size = data.size
247
+ if @card and address + size > @card.memory_size
248
+ size = @card.memory_size - address
249
+ end
250
+ return if size <= 0
251
+ offset = 0
252
+ caught = catch(:break) do
253
+ while size >= chunk_size
254
+ write_chunk(address, data[offset, chunk_size]) or break :break
255
+ address += chunk_size
256
+ offset += chunk_size
257
+ size -= chunk_size
258
+ end
259
+ end
260
+ if caught == :break
261
+ return false
262
+ elsif size > 0
263
+ write_chunk(address, data[offset, size])
264
+ end
265
+ true
266
+ end
267
+
268
+ # The pin is sent to the card. It can be given as a string or an array
269
+ # of characters. A true result is returned if the sending was
270
+ # successful.
271
+ def enter_pin(pin)
272
+ unless pin.size <= 255
273
+ raise ArgumentError.new("size of pin must be <= 255")
274
+ end
275
+ pin = pin.unpack("C*") if pin.is_a? String
276
+ enter_pin = [ 0, 0x20, 0, 0, pin.size ] + pin
277
+ response = data(@slot, HOST, enter_pin)
278
+ response.successful? or return
279
+ true
280
+ end
281
+
282
+ # The pin of this card is changed from old_pin to new_pin. They can be
283
+ # given as strings or arrays of characters. A true result is returned
284
+ # if the sending was successful.
285
+ def change_pin(old_pin, new_pin)
286
+ old_pin = old_pin.unpack("C*") if old_pin.is_a? String
287
+ new_pin = new_pin.unpack("C*") if new_pin.is_a? String
288
+ data = old_pin + new_pin
289
+ unless data.size <= 255
290
+ raise ArgumentError.new("size of old and new pin must be <= 255")
291
+ end
292
+ change_pin = [ 0, 0x24, 0, 0, data.size ] + data
293
+ response = data(@slot, HOST, change_pin)
294
+ response.successful? or return
295
+ true
296
+ end
297
+
298
+ # Requests the card status from the cardterminal. Returns the
299
+ # Response object or nil if the response wasn't successful.
300
+ # This method is called by the card_inserted? method to find
301
+ # out if a card is inserted into the terminal.
302
+ def request_card_status
303
+ get_card_status = [ CTBCS_CLA, CTBCS_INS_STATUS,
304
+ CTBCS_P1_CT_KERNEL, CTBCS_P2_STATUS_ICC, 0 ]
305
+ response = data(CT, HOST, get_card_status)
306
+ response.successful? ? response : nil
307
+ end
308
+
309
+ # Sends a byte sequence to the card to get the answer to reset
310
+ # Response object. This method is called by the Cardterminal#reset
311
+ # method.
312
+ def request_icc
313
+ get_atr = [ CTBCS_CLA, CTBCS_INS_REQUEST, CTBCS_P1_INTERFACE1,
314
+ CTBCS_P2_REQUEST_GET_ATR, 0 ]
315
+ @card_old = @card if @card
316
+ @card, atr = nil, nil
317
+ begin
318
+ if card_inserted?
319
+ atr = data(CT, HOST, get_atr)
320
+ if atr
321
+ if atr.not_changed?
322
+ @card = @card_old
323
+ return @card.atr
324
+ end
325
+ @card = Card.new(atr)
326
+ end
327
+ end
328
+ rescue CTAPIError => e
329
+ STDERR.puts "Caught: #{e}."
330
+ end
331
+ @card.atr
332
+ end
333
+
334
+ # Sends the eject card byte sequence to the card terminal and
335
+ # returns the Response object. This method is called by the
336
+ # Cardterminal#reset method.
337
+ def eject_icc
338
+ eject = [ 0x20, 0x15, 0x01, 0x00, 0x00 ]
339
+ data(CT, HOST, eject)
340
+ end
341
+
342
+ # The cardterminal is reset by first calling eject_icc and
343
+ # then request_icc. If the reset was successful the Response
344
+ # object of request_icc is returned, otherwise a nontrue value is
345
+ # returned.
346
+ def reset
347
+ eject_icc
348
+ response = request_icc or return
349
+ response.successful? or return
350
+ response
351
+ end
352
+
353
+ # Returns the card status as symbol: :no_card, :card, :card_connect.
354
+ def card_status
355
+ response = request_card_status
356
+ case response[0]
357
+ when CTBCS_DATA_STATUS_NOCARD
358
+ :no_card
359
+ when CTBCS_DATA_STATUS_CARD
360
+ :card
361
+ when CTBCS_DATA_STATUS_CARD_CONNECT
362
+ :card_connect
363
+ end
364
+ end
365
+
366
+ # Returns true if a card is inserted in the cardterminal at the moment,
367
+ # false if the cardterminal is empty.
368
+ def card_inserted?
369
+ cs = card_status
370
+ cs == :card || cs == :card_connect ? true : false
371
+ end
372
+
373
+ # Returns true if the card has been changed in the cardterminal,
374
+ # false it is still the same card. If an error occured nil
375
+ # is returned.
376
+ def card_changed?
377
+ response = reset or return
378
+ response.changed? and return true
379
+ response.not_changed? and return false
380
+ return
381
+ end
382
+ private :card_changed?
383
+
384
+ # A little structure for manufacturer information.
385
+ class Manufacturer
386
+
387
+ # Creates a Manufacturer object for this cardterminal from
388
+ # a manufacturer status response.
389
+ def initialize(response)
390
+ @manufacturer, @model, @revision =
391
+ response[0, 5], response[5, 3], response[10, 5]
392
+ end
393
+
394
+ # The manufacturer of this cardterminal.
395
+ attr_reader :manufacturer
396
+
397
+ # The model name of this cardterminal.
398
+ attr_reader :model
399
+
400
+ # The revision number of this cardterminal.
401
+ attr_reader :revision
402
+
403
+ # A string of the form 'manufacturer model revision' is returned.
404
+ def to_s
405
+ [manufacturer, model, revision].join(" ")
406
+ end
407
+
408
+ end
409
+
410
+ # Prints data with indication of direction to STDERR.
411
+ def debug(direction, data)
412
+ direction =
413
+ case direction
414
+ when 1 then '>>> '
415
+ when 2 then '<<< '
416
+ else ''
417
+ end
418
+ STDERR.puts direction + data.to_s
419
+ end
420
+ private :debug
421
+
422
+ end
423
+
424
+ # APDU (Application Protocol Data Unit) that is sent to/received from the
425
+ # cardterminal. This is the parent class of Command and Response.
426
+ class APDU
427
+
428
+ # An APDU object is generated from the String data.
429
+ def initialize(data)
430
+ @data = data
431
+ end
432
+
433
+ # This is the data string.
434
+ attr_reader :data
435
+
436
+ # Access data string by indices.
437
+ def [](*args) @data[*args] end
438
+
439
+ # We use Ruby's inspect representation to inspect the data.
440
+ def inspect
441
+ @data.inspect
442
+ end
443
+
444
+ # To display the data, a string of the form '12:23:a4' is built.
445
+ def to_s
446
+ @data.unpack('C*').map { |x| sprintf("%02x", x) }.join(':')
447
+ end
448
+
449
+ end
450
+
451
+ # A command sent to the cardterminal.
452
+ class Command < APDU
453
+
454
+ # A Command is generated from a string of the form '12:23:a4' or
455
+ # '12 23 a4'.
456
+ def self.from_string(command)
457
+ command = command.split(/[ :]/).map { |x| Integer '0x' + x }
458
+ from_array(command)
459
+ end
460
+
461
+ # A Command is generated from an array of bytes of the form
462
+ # [ 0x12, 0x23, 0xa4].
463
+ def self.from_array(command)
464
+ new(command.pack('C*'))
465
+ end
466
+
467
+ end
468
+
469
+ # A response received from the cardterminal.
470
+ class Response < APDU
471
+
472
+ # If this response was successful true is returned, false otherwise.
473
+ # A response is successful if the last two bytes are '90:00'.
474
+ def successful?
475
+ return false if @data.size < 2
476
+ return true if @data[-2, 2] == "\x90\x00"
477
+ false
478
+ end
479
+
480
+ def changed?
481
+ @data[-2, 2] == "\x62\x00" ? true : false
482
+ end
483
+
484
+ def not_changed?
485
+ @data[-2, 2] == "\x62\x01" ? true : false
486
+ end
487
+
488
+ end
489
+
490
+ class Card
491
+
492
+ def initialize(atr)
493
+ @atr = atr
494
+ end
495
+
496
+ # The answer to reset (ATR) of the inserted chipcard.
497
+ attr_accessor :atr
498
+
499
+ # Check if the ATR is correct. If false, the data of this Card
500
+ # object could be wrong and the calculations based on this data wrong.
501
+ def atr_ok?
502
+ @atr[4, 2] == "\x90\x00" && @atr[2] == 0x10
503
+ end
504
+
505
+ # The bit width of this chipcard.
506
+ def memory_bits
507
+ 1 << (@atr[1] & 0x07)
508
+ end
509
+
510
+ # Number of memory blocks on this chipcard.
511
+ def memory_blocks
512
+ 1 << (((@atr[1] & 0x78) >> 3) + 6)
513
+ end
514
+
515
+ # Computes the memory size of the inserted chipcard in bytes.
516
+ def memory_size
517
+ (memory_blocks * memory_bits >> 3)
518
+ end
519
+
520
+ # The structure of this chipcard.
521
+ def structure
522
+ if (@atr[0] & 0x3) == 0
523
+ 'ISO'
524
+ elsif (@atr[0] & 0x7) == 2
525
+ 'common use'
526
+ elsif (@atr[0] & 0x7) == 6
527
+ 'proprietary use'
528
+ else
529
+ 'special use'
530
+ end
531
+ end
532
+
533
+ # Supported protocol of this chipcard.
534
+ def protocol
535
+ if (@atr[0] & 0x80) == 0x00
536
+ 'ISO'
537
+ elsif (@atr[0] & 0xf0) == 0x80
538
+ 'I2C'
539
+ elsif (@atr[0] & 0xf0) == 0x90
540
+ '3W'
541
+ elsif (@atr[0] & 0xf0) == 0xa0
542
+ '2W'
543
+ else
544
+ 'unknown'
545
+ end
546
+ end
547
+
548
+ # Returns a short description of this card as a string.
549
+ def to_s
550
+ "<#{self.class}:Structure #{structure} Protocol #{protocol} " +
551
+ "(#{memory_blocks}x#{memory_bits}=#{memory_size}B)" + '>'
552
+ end
553
+
554
+ # Returns adescription of this card including the ATR as a string.
555
+ def inspect
556
+ string = to_s
557
+ string[-1] = " #{atr}>"
558
+ string
559
+ end
560
+
561
+ end
562
+
563
+ end