ruby-nfc 1.0.0
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.
- checksums.yaml +15 -0
- data/lib/ruby-nfc.rb +4 -0
- data/lib/ruby-nfc/libnfc.rb +292 -0
- data/lib/ruby-nfc/nfc.rb +27 -0
- data/lib/ruby-nfc/reader.rb +85 -0
- data/lib/ruby-nfc/tags/isodep.rb +71 -0
- data/lib/ruby-nfc/tags/mifare/classic.rb +170 -0
- data/lib/ruby-nfc/tags/mifare/tag.rb +68 -0
- data/lib/ruby-nfc/tags/mifare/ultralight.rb +63 -0
- data/lib/ruby-nfc/tags/tag.rb +46 -0
- metadata +72 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
!binary "U0hBMQ==":
|
|
3
|
+
metadata.gz: !binary |-
|
|
4
|
+
YmQ5NDY5MzE2MTUwMDJlMjEwYjNkMjU0ZDkxZTU1YmRjYTAyODUyOA==
|
|
5
|
+
data.tar.gz: !binary |-
|
|
6
|
+
M2U3Mjc5ZTY3YWI0MTg3ZTYwZTZmYzExOThhYjNkM2Y5NzVhMWQzZQ==
|
|
7
|
+
SHA512:
|
|
8
|
+
metadata.gz: !binary |-
|
|
9
|
+
NzMwMTFlZWVlZmUwNDIzOWEzYWQ5NzIwMDY5N2UzNTViNTYwYTQwYzI1OTQ1
|
|
10
|
+
ZmFkNzE4NjAzZDk4M2UxMzFjMTFlODliZmVkNjdhN2Y2NDk2ZWJhOWY4ZGE4
|
|
11
|
+
NDhlYTI2NjY2NGE1NzVhZDRmMTg4NTk2ZDNkMzQwY2UxOWYzZjE=
|
|
12
|
+
data.tar.gz: !binary |-
|
|
13
|
+
ZWNjMzUxZGQ2OGEyNDc5OGNlNzgwNzBhMTI5MWQ3MmU1NWRmZTc4ZjA2NWRl
|
|
14
|
+
M2YzMTc0NGZkMTM1ZDliNDVlNmRlZmU4NjA5NzY1ZmZkYTEzMjRjYmY4NGIy
|
|
15
|
+
ZWY5ZTc4NjJjYTZlN2QyYzJjMGRmODM2MWFlZDJmNjFhZmMyY2Q=
|
data/lib/ruby-nfc.rb
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
require 'ffi'
|
|
2
|
+
|
|
3
|
+
module LibNFC
|
|
4
|
+
extend FFI::Library
|
|
5
|
+
ffi_lib ['libnfc', 'libnfc.so', 'libnfc.so.5', '/usr/local/lib/libnfc.so']
|
|
6
|
+
|
|
7
|
+
PROPERTIES = [ # please refer to nfc-types.h for description
|
|
8
|
+
:NP_TIMEOUT_COMMAND,
|
|
9
|
+
:NP_TIMEOUT_ATR,
|
|
10
|
+
:NP_TIMEOUT_COM,
|
|
11
|
+
:NP_HANDLE_CRC,
|
|
12
|
+
:NP_HANDLE_PARITY,
|
|
13
|
+
:NP_ACTIVATE_FIELD,
|
|
14
|
+
:NP_ACTIVATE_CRYPTO1,
|
|
15
|
+
:NP_INFINITE_SELECT,
|
|
16
|
+
:NP_ACCEPT_INVALID_FRAMES,
|
|
17
|
+
:NP_ACCEPT_MULTIPLE_FRAMES,
|
|
18
|
+
:NP_AUTO_ISO14443_4,
|
|
19
|
+
:NP_EASY_FRAMING,
|
|
20
|
+
:NP_FORCE_ISO14443_A,
|
|
21
|
+
:NP_FORCE_ISO14443_B,
|
|
22
|
+
:NP_FORCE_SPEED_106
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
DEP_MODE = [
|
|
26
|
+
:NDM_UNDEFINED,
|
|
27
|
+
:NDM_PASSIVE,
|
|
28
|
+
:NDM_ACTIVE
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
BAUD_RATE = [
|
|
32
|
+
:NBR_UNDEFINED,
|
|
33
|
+
:NBR_106,
|
|
34
|
+
:NBR_212,
|
|
35
|
+
:NBR_424,
|
|
36
|
+
:NBR_847
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
MODULATION_TYPE = [
|
|
40
|
+
:NMT_ISO14443A, 1,
|
|
41
|
+
:NMT_JEWEL,
|
|
42
|
+
:NMT_ISO14443B,
|
|
43
|
+
:NMT_ISO14443BI, # pre-ISO14443B aka ISO/IEC 14443 B' or Type B'
|
|
44
|
+
:NMT_ISO14443B2SR, # ISO14443-2B ST SRx
|
|
45
|
+
:NMT_ISO14443B2CT, # ISO14443-2B ASK CTx
|
|
46
|
+
:NMT_FELICA,
|
|
47
|
+
:NMT_DEP
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
class ISO14443a < FFI::Struct
|
|
51
|
+
pack 1
|
|
52
|
+
layout(
|
|
53
|
+
:abtAtqa, [:uint8, 2],
|
|
54
|
+
:btSak, :uint8,
|
|
55
|
+
:szUidLen, :size_t,
|
|
56
|
+
:abtUid, [:uint8, 10],
|
|
57
|
+
:szAtsLen, :size_t,
|
|
58
|
+
:abtAts, [:uint8, 254]
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class Felica < FFI::Struct
|
|
63
|
+
pack 1
|
|
64
|
+
layout(
|
|
65
|
+
:szLen, :size_t,
|
|
66
|
+
:btResCode, :uint8,
|
|
67
|
+
:abtId, [:uint8, 8],
|
|
68
|
+
:abtPad, [:uint8, 8],
|
|
69
|
+
:abtSysCode, [:uint8, 2]
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class ISO14443b < FFI::Struct
|
|
74
|
+
pack 1
|
|
75
|
+
layout(
|
|
76
|
+
:abtPupi, [:uint8, 4],
|
|
77
|
+
:abtApplicationData, [:uint8, 4],
|
|
78
|
+
:abtProtocolInfo, [:uint8, 3],
|
|
79
|
+
:ui8CardIdentifier, :uint8
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class ISO14443bi < FFI::Struct
|
|
84
|
+
pack 1
|
|
85
|
+
layout(
|
|
86
|
+
:abtDIV, [:uint8, 4],
|
|
87
|
+
:btVerLog, :uint8,
|
|
88
|
+
:btConfig, :uint8,
|
|
89
|
+
:szAtrLen, :size_t,
|
|
90
|
+
:abtAtr, [:uint8, 33]
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class ISO14443b2sr < FFI::Struct
|
|
95
|
+
pack 1
|
|
96
|
+
layout(
|
|
97
|
+
:abtUID, [:uint8, 8]
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class ISO14443b2ct < FFI::Struct
|
|
102
|
+
pack 1
|
|
103
|
+
layout(
|
|
104
|
+
:abtUID, [:uint8, 4],
|
|
105
|
+
:btProdCode, :uint8,
|
|
106
|
+
:btFabCode, :uint8
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
class Jewel < FFI::Struct
|
|
111
|
+
pack 1
|
|
112
|
+
layout(
|
|
113
|
+
:btSensRes, [:uint8, 2],
|
|
114
|
+
:btId, [:uint8, 4]
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
DepEnum = enum(DEP_MODE)
|
|
119
|
+
|
|
120
|
+
class DepInfo < FFI::Struct
|
|
121
|
+
pack 1
|
|
122
|
+
layout(
|
|
123
|
+
:abtNFCID3, [:uint8, 10],# uint8_t abtNFCID3[10];
|
|
124
|
+
:btDID, :uint8,# uint8_t btDID;
|
|
125
|
+
:btBS, :uint8,# uint8_t btBS;
|
|
126
|
+
:btBR, :uint8,# uint8_t btBR;
|
|
127
|
+
:btTO, :uint8,# uint8_t btTO;
|
|
128
|
+
:btPP, :uint8,# uint8_t btPP;
|
|
129
|
+
:abtGB, [:uint8, 48],# uint8_t abtGB[48];
|
|
130
|
+
:szGB, :size_t,# size_t szGB;
|
|
131
|
+
:ndm, DepEnum
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TargetInfo < FFI::Union
|
|
137
|
+
pack 1
|
|
138
|
+
layout(
|
|
139
|
+
:nai, ISO14443a,
|
|
140
|
+
:nfi, Felica,
|
|
141
|
+
:nbi, ISO14443b,
|
|
142
|
+
:nii, ISO14443bi,
|
|
143
|
+
:nsi, ISO14443b2sr,
|
|
144
|
+
:nci, ISO14443b2ct,
|
|
145
|
+
:nji, Jewel,
|
|
146
|
+
:ndi, DepInfo
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
ModulationType = enum(MODULATION_TYPE)
|
|
151
|
+
BaudRate = enum(BAUD_RATE)
|
|
152
|
+
|
|
153
|
+
class Modulation < FFI::Struct
|
|
154
|
+
pack 1
|
|
155
|
+
layout(
|
|
156
|
+
:nmt, ModulationType,
|
|
157
|
+
:nbr, BaudRate
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
class Target < FFI::Struct
|
|
162
|
+
pack 1
|
|
163
|
+
layout(
|
|
164
|
+
:nti, TargetInfo,
|
|
165
|
+
:nm, Modulation
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def processed!
|
|
169
|
+
@processed = true
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def processed?
|
|
173
|
+
defined?(@processed) && @processed
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def sak
|
|
177
|
+
self[:nti][:nai][:btSak]
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
###
|
|
182
|
+
#Version checking before wrapping the rest of the library to prevent
|
|
183
|
+
#name method matching errors.
|
|
184
|
+
attach_function :nfc_version, [], :string
|
|
185
|
+
|
|
186
|
+
attach_function :nfc_perror, [ :pointer, :string], :void
|
|
187
|
+
attach_function :nfc_list_devices, [ :pointer, :pointer, :size_t], :size_t
|
|
188
|
+
|
|
189
|
+
attach_function :nfc_initiator_init, [:pointer], :int
|
|
190
|
+
|
|
191
|
+
attach_function :nfc_device_set_property_bool, [
|
|
192
|
+
:pointer, #nfc_device *pnd
|
|
193
|
+
enum(PROPERTIES), #property constant
|
|
194
|
+
:bool # value
|
|
195
|
+
], :int
|
|
196
|
+
|
|
197
|
+
attach_function :nfc_open, [ :pointer, :string ], :pointer
|
|
198
|
+
attach_function :nfc_close, [ :pointer ], :void
|
|
199
|
+
|
|
200
|
+
attach_function :iso14443a_crc, [
|
|
201
|
+
:pointer, #uint8_t *pbtData
|
|
202
|
+
:size_t, #size_t szLen
|
|
203
|
+
:pointer #uint8_t *pbtCrc
|
|
204
|
+
], :void
|
|
205
|
+
|
|
206
|
+
attach_function :nfc_initiator_poll_dep_target, [
|
|
207
|
+
:pointer, #nfc_device *pnd
|
|
208
|
+
enum(DEP_MODE), #nfc_dep_mode
|
|
209
|
+
enum(BAUD_RATE), #nfc_baud_rate
|
|
210
|
+
:pointer, #nfc_dep_info *pndiInitiator
|
|
211
|
+
:pointer, #nfc_target *pnt
|
|
212
|
+
:int #timout
|
|
213
|
+
], :int
|
|
214
|
+
|
|
215
|
+
attach_function :nfc_initiator_poll_target, [
|
|
216
|
+
:pointer, #device
|
|
217
|
+
:pointer, #modulations
|
|
218
|
+
:size_t, #modulations size
|
|
219
|
+
:uint8, #targets count
|
|
220
|
+
:uint8, #period
|
|
221
|
+
:pointer #targets
|
|
222
|
+
], :int
|
|
223
|
+
|
|
224
|
+
attach_function :nfc_initiator_list_passive_targets, [
|
|
225
|
+
:pointer, # device
|
|
226
|
+
Modulation.by_value, # modulation
|
|
227
|
+
:pointer, # array of targets
|
|
228
|
+
:size_t # maximum amount of targets
|
|
229
|
+
], :int
|
|
230
|
+
|
|
231
|
+
attach_function :nfc_initiator_select_passive_target, [
|
|
232
|
+
:pointer, #device
|
|
233
|
+
Modulation.by_value, # modulation
|
|
234
|
+
:pointer, # pbInitData (uid)
|
|
235
|
+
:size_t, # uid size
|
|
236
|
+
:pointer, #target
|
|
237
|
+
], :int
|
|
238
|
+
|
|
239
|
+
attach_function :nfc_initiator_transceive_bytes, [
|
|
240
|
+
:pointer, #device
|
|
241
|
+
:pointer, #byte array to transmit
|
|
242
|
+
:size_t, #number of bytes to transmit
|
|
243
|
+
:pointer, #response buffer
|
|
244
|
+
:size_t, #response buffer size
|
|
245
|
+
:int, #timeout
|
|
246
|
+
], :int
|
|
247
|
+
|
|
248
|
+
attach_function :nfc_initiator_poll_target, [
|
|
249
|
+
:pointer, # device
|
|
250
|
+
:pointer, # array of modulations
|
|
251
|
+
:size_t, #number of modulations
|
|
252
|
+
:uint8, #number of tags for each modulation,
|
|
253
|
+
:uint8, #period
|
|
254
|
+
:pointer #target
|
|
255
|
+
], :int
|
|
256
|
+
|
|
257
|
+
attach_function :str_nfc_target, [:pointer, :pointer, :bool], :int
|
|
258
|
+
|
|
259
|
+
attach_function :nfc_initiator_select_dep_target, [
|
|
260
|
+
:pointer,
|
|
261
|
+
enum(DEP_MODE),
|
|
262
|
+
enum(BAUD_RATE),
|
|
263
|
+
:pointer,
|
|
264
|
+
:pointer,
|
|
265
|
+
:int
|
|
266
|
+
], :int
|
|
267
|
+
|
|
268
|
+
attach_function :nfc_initiator_deselect_target, [:pointer], :int
|
|
269
|
+
attach_function :nfc_init, [ :pointer ], :pointer
|
|
270
|
+
attach_function :nfc_exit, [ :pointer ], :void
|
|
271
|
+
|
|
272
|
+
def self.crc(data)
|
|
273
|
+
data_ptr = FFI::MemoryPointer.new(:uint8, data.length)
|
|
274
|
+
data_ptr.put_bytes(0, data)
|
|
275
|
+
|
|
276
|
+
crc_ptr = FFI::MemoryPointer.new(:uint8, 2)
|
|
277
|
+
crc_ptr.put_bytes(0, "\x0\x0")
|
|
278
|
+
|
|
279
|
+
iso14443a_crc(data_ptr, data.length, crc_ptr)
|
|
280
|
+
crc_ptr.get_bytes(0, 2).to_s
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def self.crc_hex(data)
|
|
284
|
+
crc(data).unpack('H*').pop
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def self.debug_target(target)
|
|
288
|
+
str_pointer = FFI::MemoryPointer.new(:pointer)
|
|
289
|
+
str_nfc_target(str_pointer, target, true)
|
|
290
|
+
puts str_pointer.get_pointer(0).get_string(0)
|
|
291
|
+
end
|
|
292
|
+
end
|
data/lib/ruby-nfc/nfc.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require 'ffi'
|
|
2
|
+
require 'logger'
|
|
3
|
+
require_relative './libnfc'
|
|
4
|
+
|
|
5
|
+
module NFC
|
|
6
|
+
class Error < ::Exception;end
|
|
7
|
+
|
|
8
|
+
@@context = nil
|
|
9
|
+
# TODO
|
|
10
|
+
@@logger = Logger.new(STDERR)
|
|
11
|
+
|
|
12
|
+
def self.version
|
|
13
|
+
LibNFC.nfc_version
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.context
|
|
17
|
+
unless @@context
|
|
18
|
+
ptr = FFI::MemoryPointer.new(:pointer, 1)
|
|
19
|
+
LibNFC.nfc_init(ptr)
|
|
20
|
+
@@context = ptr.read_pointer
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.logger
|
|
25
|
+
@@logger
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module NFC
|
|
2
|
+
class Reader
|
|
3
|
+
attr_reader :ptr
|
|
4
|
+
|
|
5
|
+
def initialize(device_name)
|
|
6
|
+
@name = device_name
|
|
7
|
+
@ptr = nil
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def set_flag(name, value)
|
|
11
|
+
LibNFC.nfc_device_set_property_bool(@ptr, name, value)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def connect
|
|
15
|
+
@ptr ||= LibNFC.nfc_open(NFC.context, @name)
|
|
16
|
+
raise NFC::Error.new('Cant connect to ' << @name) if @ptr.null?
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns list of tags applied to reader
|
|
21
|
+
def discover(*card_types)
|
|
22
|
+
# TODO: по правильному здесь надо делать низкоуровневый
|
|
23
|
+
card_types.inject([]) do |tags, card_type|
|
|
24
|
+
raise NFC::Error.new('Wrong card type') unless card_type.respond_to? :discover
|
|
25
|
+
tags += card_type.discover(connect)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def poll(*card_types)
|
|
30
|
+
connect
|
|
31
|
+
|
|
32
|
+
LibNFC.nfc_initiator_init(@ptr) # we'll be initiator not a target
|
|
33
|
+
|
|
34
|
+
set_flag(:NP_ACTIVATE_FIELD, false)
|
|
35
|
+
set_flag(:NP_HANDLE_CRC, true)
|
|
36
|
+
set_flag(:NP_HANDLE_PARITY, true)
|
|
37
|
+
set_flag(:NP_AUTO_ISO14443_4, true)
|
|
38
|
+
set_flag(:NP_ACTIVATE_FIELD, true)
|
|
39
|
+
|
|
40
|
+
modulation = LibNFC::Modulation.new
|
|
41
|
+
modulation[:nmt] = :NMT_ISO14443A
|
|
42
|
+
modulation[:nbr] = :NBR_106
|
|
43
|
+
|
|
44
|
+
targets = FFI::MemoryPointer.new(:uchar, LibNFC::Target.size * 10)
|
|
45
|
+
|
|
46
|
+
loop do
|
|
47
|
+
res = LibNFC.nfc_initiator_list_passive_targets(@ptr, modulation,
|
|
48
|
+
targets, 10)
|
|
49
|
+
|
|
50
|
+
# iterate over all applied targets and iterate
|
|
51
|
+
0.upto(res - 1) do |i|
|
|
52
|
+
target = LibNFC::Target.new(targets + i * LibNFC::Target.size)
|
|
53
|
+
# iterate over requested card types for each target
|
|
54
|
+
# notice that some targets can match several types i.e.
|
|
55
|
+
# contactless credit cards (PayPass/payWave) with mifare chip
|
|
56
|
+
# on board
|
|
57
|
+
card_types.each do |card_type|
|
|
58
|
+
if card_type.match?(target)
|
|
59
|
+
tag = card_type.new(target, self)
|
|
60
|
+
yield tag
|
|
61
|
+
# if this tag was marked as processed - continue with next tag
|
|
62
|
+
break if target.processed?
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end # upto
|
|
66
|
+
end # loop
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_s
|
|
70
|
+
@name
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.all
|
|
74
|
+
ptr = FFI::MemoryPointer.new(:char, 1024 * 10)
|
|
75
|
+
len = LibNFC.nfc_list_devices(NFC.context, ptr, 10)
|
|
76
|
+
|
|
77
|
+
if len <= 0
|
|
78
|
+
raise NFC::Error, "No compatible NFC readers found"
|
|
79
|
+
else
|
|
80
|
+
names = ptr.get_bytes(0, 1024 * len).split("\x00").reject {|e| e.empty?}
|
|
81
|
+
names.map {|name| Reader.new(name)}
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require_relative '../nfc'
|
|
2
|
+
require_relative './tag'
|
|
3
|
+
|
|
4
|
+
module IsoDep
|
|
5
|
+
ISO_14443_4_COMPATIBLE = 0x20
|
|
6
|
+
|
|
7
|
+
class Error < ::Exception; end
|
|
8
|
+
|
|
9
|
+
class Tag < NFC::Tag
|
|
10
|
+
|
|
11
|
+
def self.match?(target)
|
|
12
|
+
target[:nti][:nai][:btSak] & IsoDep::ISO_14443_4_COMPATIBLE > 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def select(aid = nil, &block)
|
|
16
|
+
@reader.set_flag(:NP_AUTO_ISO14443_4, true)
|
|
17
|
+
|
|
18
|
+
modulation = LibNFC::Modulation.new
|
|
19
|
+
modulation[:nmt] = :NMT_ISO14443A
|
|
20
|
+
modulation[:nbr] = :NBR_106
|
|
21
|
+
|
|
22
|
+
nai_ptr = @target[:nti][:nai].pointer
|
|
23
|
+
|
|
24
|
+
# abt + sak + szUidLen offset
|
|
25
|
+
uid_ptr = nai_ptr + FFI.type_size(:uint8) * 3 + FFI.type_size(:size_t)
|
|
26
|
+
|
|
27
|
+
res = LibNFC.nfc_initiator_select_passive_target(
|
|
28
|
+
@reader.ptr,
|
|
29
|
+
modulation,
|
|
30
|
+
uid_ptr,
|
|
31
|
+
uid.length,
|
|
32
|
+
@target.pointer
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if res > 0
|
|
36
|
+
# trying to select applet if applet identifier was given
|
|
37
|
+
if aid
|
|
38
|
+
sw = send_apdu("\x00\xA4\x04\x00#{aid.size.chr}#{aid}")
|
|
39
|
+
raise IsoDep::Error, "Application not found: #{aid.unpack('H*').pop}" unless "\x90\x00" == sw
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
super(&block)
|
|
43
|
+
else
|
|
44
|
+
raise IsoDep::Error, "Can't select tag: #{res}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def deselect
|
|
49
|
+
0 == LibNFC.nfc_initiator_deselect_target(@reader.ptr)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def send_apdu(apdu)
|
|
53
|
+
cmd = apdu
|
|
54
|
+
cmd.force_encoding('ASCII-8BIT')
|
|
55
|
+
command_buffer = FFI::MemoryPointer.new(:uint8, cmd.length)
|
|
56
|
+
command_buffer.write_string_length(cmd, cmd.length)
|
|
57
|
+
|
|
58
|
+
response_buffer = FFI::MemoryPointer.new(:uint8, 254)
|
|
59
|
+
|
|
60
|
+
res_len = LibNFC.nfc_initiator_transceive_bytes(@reader.ptr,
|
|
61
|
+
command_buffer, cmd.length, response_buffer, 254, 0)
|
|
62
|
+
|
|
63
|
+
raise IsoDep::Error, "APDU sending failed: #{res_len}" if res_len < 0
|
|
64
|
+
|
|
65
|
+
response_buffer.get_bytes(0, res_len).to_s
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
alias :'<<' :send_apdu
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
require_relative './tag'
|
|
2
|
+
|
|
3
|
+
module Mifare
|
|
4
|
+
# Adding some Classic related stuff to Mifare module
|
|
5
|
+
|
|
6
|
+
# tag
|
|
7
|
+
attach_function :mifare_classic_connect, [:pointer], :int
|
|
8
|
+
# tag
|
|
9
|
+
attach_function :mifare_classic_disconnect, [:pointer], :int
|
|
10
|
+
# tag, blocknumber, key
|
|
11
|
+
attach_function :mifare_classic_authenticate, [:pointer, :uchar, :pointer, enum(:key_a, :key_b)], :int
|
|
12
|
+
|
|
13
|
+
# tag, blocknumber, block data
|
|
14
|
+
attach_function :mifare_classic_read, [:pointer, :uchar, :pointer], :int
|
|
15
|
+
|
|
16
|
+
# tag, blocknumber, block data
|
|
17
|
+
attach_function :mifare_classic_write, [:pointer, :uchar, :pointer], :int
|
|
18
|
+
|
|
19
|
+
#mifare_classic_init_value (MifareTag tag, const MifareClassicBlockNumber block, const int32_t value, const MifareClassicBlockNumber adr)
|
|
20
|
+
# tag, blocknumber, value, addr
|
|
21
|
+
attach_function :mifare_classic_init_value, [:pointer, :uchar, :int32, :uchar], :int
|
|
22
|
+
|
|
23
|
+
#mifare_classic_increment (MifareTag tag, const MifareClassicBlockNumber block, const uint32_t amount)
|
|
24
|
+
attach_function :mifare_classic_increment, [:pointer, :uchar, :int32], :int
|
|
25
|
+
|
|
26
|
+
#mifare_classic_decrement (MifareTag tag, const MifareClassicBlockNumber block, const uint32_t amount)
|
|
27
|
+
attach_function :mifare_classic_decrement, [:pointer, :uchar, :int32], :int
|
|
28
|
+
|
|
29
|
+
#mifare_classic_read_value (MifareTag tag, const MifareClassicBlockNumber block, int32_t *value, MifareClassicBlockNumber *adr)
|
|
30
|
+
attach_function :mifare_classic_read_value, [:pointer, :uchar, :pointer, :pointer], :int
|
|
31
|
+
|
|
32
|
+
#mifare_classic_transfer (MifareTag tag, const MifareClassicBlockNumber block)
|
|
33
|
+
attach_function :mifare_classic_transfer, [:pointer, :uchar], :int
|
|
34
|
+
|
|
35
|
+
module Classic
|
|
36
|
+
class Tag < Mifare::Tag
|
|
37
|
+
def initialize(target, reader)
|
|
38
|
+
super(target, reader)
|
|
39
|
+
|
|
40
|
+
@auth_block = nil #last authenticated block
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def select(&block)
|
|
44
|
+
@reader.set_flag(:NP_AUTO_ISO14443_4, false)
|
|
45
|
+
|
|
46
|
+
res = Mifare.mifare_classic_connect(@pointer)
|
|
47
|
+
if 0 == res
|
|
48
|
+
super
|
|
49
|
+
else
|
|
50
|
+
raise Mifare::Error, "Can't select tag: #{res}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def deselect
|
|
55
|
+
Mifare.mifare_classic_disconnect(@pointer)
|
|
56
|
+
super
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# keytype can be :key_a or :key_b
|
|
60
|
+
# key - hexadecimal string key representation like "FFFFFFFFFFFF"
|
|
61
|
+
def auth(block_num, key_type, key)
|
|
62
|
+
raise Mifare::Error.new('Wrong key type') unless [:key_a, :key_b].include? key_type
|
|
63
|
+
raise Mifare::Error.new('Wrong key length') unless [6, 12].include? key.size
|
|
64
|
+
|
|
65
|
+
key_ptr = FFI::MemoryPointer.new(:uchar, 6)
|
|
66
|
+
key_ptr.put_bytes(0, 6 == key.size ? key : [key].pack('H*'))
|
|
67
|
+
|
|
68
|
+
res = Mifare.mifare_classic_authenticate(@pointer, block_num, key_ptr,
|
|
69
|
+
key_type)
|
|
70
|
+
if 0 == res
|
|
71
|
+
@auth_block = block_num
|
|
72
|
+
else
|
|
73
|
+
raise Mifare::Error.new("Can't autenticate to block 0x%02x" % block_num)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# block number to read
|
|
78
|
+
def read(block_num = nil)
|
|
79
|
+
block_num ||= @auth_block
|
|
80
|
+
raise Mifare::Error.new('Not authenticated') unless block_num
|
|
81
|
+
|
|
82
|
+
data_ptr = FFI::MemoryPointer.new(:uchar, 16)
|
|
83
|
+
res = Mifare.mifare_classic_read(@pointer, block_num, data_ptr)
|
|
84
|
+
|
|
85
|
+
raise Mifare::Error.new("Can't read block 0x%02x" % block_num) unless 0 == res
|
|
86
|
+
|
|
87
|
+
data_ptr.get_bytes(0, 16).force_encoding('ASCII-8BIT')
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @data - 16 bytes represented by hexadecimal string
|
|
91
|
+
# @block_num - number of block to write to
|
|
92
|
+
def write(data, block_num = nil)
|
|
93
|
+
raise Mifare::Error.new('Wrong data given') if data !~ /^[\da-f]{32}$/i
|
|
94
|
+
|
|
95
|
+
block_num ||= @auth_block
|
|
96
|
+
raise Mifare::Error.new('Not authenticated') unless block_num
|
|
97
|
+
|
|
98
|
+
data_ptr = FFI::MemoryPointer.new(:uchar, 16)
|
|
99
|
+
data_ptr.put_bytes(0, [data].pack('H*'))
|
|
100
|
+
|
|
101
|
+
res = Mifare.mifare_classic_write(@pointer, block_num, data_ptr)
|
|
102
|
+
(0 == res) || raise(Mifare::Error.new("Can't write block 0x%02x" % block_num))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Create value block structure and write it to block
|
|
106
|
+
def init_value(value, addr = nil, block_num = nil)
|
|
107
|
+
block_num ||= @auth_block
|
|
108
|
+
raise Mifare::Error.new('Not authenticated') unless block_num
|
|
109
|
+
|
|
110
|
+
addr ||= 0
|
|
111
|
+
|
|
112
|
+
res = Mifare.mifare_classic_init_value(@pointer, block_num, value, addr)
|
|
113
|
+
(0 == res) || raise(Mifare::Error.new("Can't init value block 0x%02x" % block_num))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# returns only value part of value block
|
|
117
|
+
def value(block_num = nil)
|
|
118
|
+
v, _ = value_with_addr(block_num)
|
|
119
|
+
v
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# returns value and addr
|
|
123
|
+
def value_with_addr(block_num = nil)
|
|
124
|
+
block_num ||= @auth_block
|
|
125
|
+
raise Mifare::Error.new('Not authenticated') unless block_num
|
|
126
|
+
|
|
127
|
+
value_ptr = FFI::MemoryPointer.new(:int32, 1)
|
|
128
|
+
addr_ptr = FFI::MemoryPointer.new(:uchar, 1)
|
|
129
|
+
res = Mifare.mifare_classic_read_value(@pointer, block_num, value_ptr, addr_ptr)
|
|
130
|
+
raise Mifare::Error.new("Can't read value block 0x%02x" % block_num) unless 0 == res
|
|
131
|
+
|
|
132
|
+
[value_ptr.get_int32(0), addr_ptr.get_uchar(0)]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Mifare classic increment
|
|
136
|
+
def inc(amount = 1, block_num = nil)
|
|
137
|
+
block_num ||= @auth_block
|
|
138
|
+
raise Mifare::Error.new('Not authenticated') unless block_num
|
|
139
|
+
|
|
140
|
+
res = Mifare.mifare_classic_increment(@pointer, block_num, amount)
|
|
141
|
+
(0 == res) || raise(Mifare::Error.new("Can't increment block 0x%02x" % block_num))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Mifare classic decrement
|
|
145
|
+
def dec(amount = 1, block_num = nil)
|
|
146
|
+
block_num ||= @auth_block
|
|
147
|
+
raise Mifare::Error.new('Not authenticated') unless block_num
|
|
148
|
+
|
|
149
|
+
res = Mifare.mifare_classic_decrement(@pointer, block_num, amount)
|
|
150
|
+
(0 == res) || raise(Mifare::Error.new("Can't decrement block 0x%02x" % block_num))
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def transfer(block_num = nil)
|
|
154
|
+
block_num ||= @auth_block
|
|
155
|
+
raise Mifare::Error.new('Not authenticated') unless block_num
|
|
156
|
+
|
|
157
|
+
res = Mifare.mifare_classic_transfer(@pointer, block_num)
|
|
158
|
+
(0 == res) || raise(Mifare::Error.new("Can't transfer to block 0x%02x" % block_num))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Check's if our tag class is able to handle this LibNFC::Target
|
|
162
|
+
def self.match?(target)
|
|
163
|
+
keys = [:CLASSIC_1K, :CLASSIC_1K_EMULATED, :CLASSIC_1K_ANDROID,
|
|
164
|
+
:INFINEON_CLASSIC_1k, :CLASSIC_4K, :CLASSIC_4K_EMULATED]
|
|
165
|
+
|
|
166
|
+
Mifare::SAKS.values_at(*keys).include? target.sak
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require 'ffi'
|
|
2
|
+
require_relative '../../nfc'
|
|
3
|
+
require_relative '../tag'
|
|
4
|
+
|
|
5
|
+
module Mifare
|
|
6
|
+
class Error < Exception; end
|
|
7
|
+
|
|
8
|
+
extend FFI::Library
|
|
9
|
+
|
|
10
|
+
ffi_lib ['libfreefare', 'libfreefare.so', '/usr/local/lib/libfreefare.so']
|
|
11
|
+
|
|
12
|
+
# List of known mifare card's SAKs
|
|
13
|
+
SAKS = {
|
|
14
|
+
CLASSIC_1K: 0x08,
|
|
15
|
+
CLASSIC_1K_EMULATED: 0x28,
|
|
16
|
+
CLASSIC_1K_ANDROID: 0x68, # Android with mifare emulated by USIM or eSE
|
|
17
|
+
INFINEON_CLASSIC_1k: 0x88,
|
|
18
|
+
CLASSIC_4K: 0x18,
|
|
19
|
+
CLASSIC_4K_EMULATED: 0x38,
|
|
20
|
+
ULTRALIGHT: 0x00
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# common freefare functions prototypes
|
|
24
|
+
attach_function :freefare_tag_new, [:pointer, LibNFC::ISO14443a.by_value], :pointer
|
|
25
|
+
attach_function :freefare_free_tag, [:pointer], :void
|
|
26
|
+
|
|
27
|
+
# tag
|
|
28
|
+
attach_function :freefare_get_tag_uid, [:pointer], :string
|
|
29
|
+
# tag
|
|
30
|
+
attach_function :freefare_get_tag_friendly_name, [:pointer], :string
|
|
31
|
+
|
|
32
|
+
# device, pointer to array of pointers to tags
|
|
33
|
+
# and we need to go deeper :)
|
|
34
|
+
attach_function :freefare_get_tags, [:pointer], :pointer
|
|
35
|
+
|
|
36
|
+
class Tag < NFC::Tag
|
|
37
|
+
def initialize(target, reader)
|
|
38
|
+
super(target, reader)
|
|
39
|
+
|
|
40
|
+
@pointer = Mifare.freefare_tag_new(reader.ptr, target[:nti][:nai])
|
|
41
|
+
|
|
42
|
+
raise Mifare::Error, "Unknown mifare tag" if @pointer.null?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def name
|
|
46
|
+
Mifare.freefare_get_tag_friendly_name(@pointer)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_s
|
|
50
|
+
"#{uid_hex} #{name} SAK: 0x#{@target.sak.to_s(16)}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# frees memory allocated for mifare tag
|
|
54
|
+
def deselect
|
|
55
|
+
Mifare.freefare_free_tag(@pointer)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def sak
|
|
59
|
+
target.sak
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.match?(target)
|
|
63
|
+
SAKS.values.include?(target[:nti][:nai][:btSak])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require_relative './tag'
|
|
2
|
+
|
|
3
|
+
module Mifare
|
|
4
|
+
# tag
|
|
5
|
+
attach_function :mifare_ultralight_connect, [:pointer], :int
|
|
6
|
+
# tag
|
|
7
|
+
attach_function :mifare_ultralight_disconnect, [:pointer], :int
|
|
8
|
+
|
|
9
|
+
# tag, page number, page data
|
|
10
|
+
attach_function :mifare_ultralight_read, [:pointer, :uchar, :pointer], :int
|
|
11
|
+
|
|
12
|
+
# tag, blocknumber, block data
|
|
13
|
+
attach_function :mifare_ultralight_write, [:pointer, :uchar, :pointer], :int
|
|
14
|
+
|
|
15
|
+
module Ultralight
|
|
16
|
+
class Tag < Mifare::Tag
|
|
17
|
+
def select(&block)
|
|
18
|
+
@reader.set_flag(:NP_AUTO_ISO14443_4, false)
|
|
19
|
+
|
|
20
|
+
res = Mifare.mifare_ultralight_connect(@pointer)
|
|
21
|
+
if 0 == res
|
|
22
|
+
super
|
|
23
|
+
else
|
|
24
|
+
raise Mifare::Error, "Can't select tag: #{res}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def deselect
|
|
29
|
+
Mifare.mifare_ultralight_disconnect(@pointer)
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# block number to read
|
|
34
|
+
def read(page_num = nil)
|
|
35
|
+
data_ptr = FFI::MemoryPointer.new(:uchar, 4)
|
|
36
|
+
res = Mifare.mifare_ultralight_read(@pointer, page_num, data_ptr)
|
|
37
|
+
|
|
38
|
+
raise Mifare::Error, ("Can't read page 0x%02x" % page_num) unless 0 == res
|
|
39
|
+
|
|
40
|
+
data_ptr.get_bytes(0, 4).force_encoding('ASCII-8BIT')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @data - 16 bytes represented by hexadecimal string
|
|
44
|
+
# @block_num - number of block to write to
|
|
45
|
+
def write(data, page_num = nil)
|
|
46
|
+
raise Mifare::Error, "Wrong data given" if data !~ /^[\da-f]{8}$/i
|
|
47
|
+
|
|
48
|
+
data_ptr = FFI::MemoryPointer.new(:uchar, 4)
|
|
49
|
+
data_ptr.put_bytes(0, [data].pack('H*'))
|
|
50
|
+
|
|
51
|
+
res = Mifare.mifare_classic_write(@pointer, block_num, data_ptr)
|
|
52
|
+
raise Mifare::Error, ("Can't write page 0x%02x" % page_num) unless 0 == res
|
|
53
|
+
|
|
54
|
+
res
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Check's if our tag class is able to handle this LibNFC::Target
|
|
58
|
+
def self.match?(target)
|
|
59
|
+
target.sak == Mifare::SAKS[:ULTRALIGHT]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module NFC
|
|
2
|
+
class Tag
|
|
3
|
+
def initialize(target, reader)
|
|
4
|
+
@target = target
|
|
5
|
+
@reader = reader
|
|
6
|
+
@processed = false
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def select(&block)
|
|
10
|
+
if block_given?
|
|
11
|
+
begin
|
|
12
|
+
self.instance_eval(&block)
|
|
13
|
+
ensure
|
|
14
|
+
deselect
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def processed!
|
|
20
|
+
@target.processed!
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def processed?
|
|
24
|
+
@target.processed?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def uid
|
|
28
|
+
uid_size = @target[:nti][:nai][:szUidLen]
|
|
29
|
+
@target[:nti][:nai][:abtUid].to_s[0...uid_size]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def uid_hex
|
|
33
|
+
uid.unpack('H*').pop
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_s
|
|
37
|
+
uid_hex
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Matches any NFC tag
|
|
41
|
+
def self.match?(target)
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby-nfc
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Maxim Chechel
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2014-10-01 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: ffi
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ! '>='
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ! '>='
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
description: ! " \tThis gem is built on top of libnfc and libfreefare using ffi and
|
|
28
|
+
supports:\n\t\t * Reading and writing Mifare Classic and Ultralight tags\n\t\t *
|
|
29
|
+
Android HCE / Blackberry VTE emulated tags\n\t\t * Dual-interface smart cards like
|
|
30
|
+
MasterCard PayPass or Visa payWave\n"
|
|
31
|
+
email: maximchick@gmail.com
|
|
32
|
+
executables: []
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- ./lib/ruby-nfc.rb
|
|
37
|
+
- ./lib/ruby-nfc/libnfc.rb
|
|
38
|
+
- ./lib/ruby-nfc/nfc.rb
|
|
39
|
+
- ./lib/ruby-nfc/reader.rb
|
|
40
|
+
- ./lib/ruby-nfc/tags/isodep.rb
|
|
41
|
+
- ./lib/ruby-nfc/tags/mifare/classic.rb
|
|
42
|
+
- ./lib/ruby-nfc/tags/mifare/tag.rb
|
|
43
|
+
- ./lib/ruby-nfc/tags/mifare/ultralight.rb
|
|
44
|
+
- ./lib/ruby-nfc/tags/tag.rb
|
|
45
|
+
homepage: https://github.com/maximchick/ruby-nfc
|
|
46
|
+
licenses:
|
|
47
|
+
- MIT
|
|
48
|
+
metadata: {}
|
|
49
|
+
post_install_message: ! "Don't forget to install libnfc and libfreefare\nsee installation
|
|
50
|
+
instructions here: \n\t https://github.com/maximchick/ruby-nfc"
|
|
51
|
+
rdoc_options: []
|
|
52
|
+
require_paths:
|
|
53
|
+
- lib
|
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ! '>='
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ! '>='
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '0'
|
|
64
|
+
requirements:
|
|
65
|
+
- libnfc, v1.7.x
|
|
66
|
+
- libfreefare
|
|
67
|
+
rubyforge_project:
|
|
68
|
+
rubygems_version: 2.2.2
|
|
69
|
+
signing_key:
|
|
70
|
+
specification_version: 4
|
|
71
|
+
summary: Provides NFC functionality for Ruby
|
|
72
|
+
test_files: []
|