ctapi 0.2.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.
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