ciesign-react-native 0.0.1 → 0.2.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.
@@ -0,0 +1,47 @@
1
+ import { type Bytes } from './certificate';
2
+ import { type PinStatus } from './native';
3
+ export type { PinStatus } from './native';
4
+ /** Pass instead of the PIN to use the one saved with `savePin`, unlocked with Face ID, Touch ID or a fingerprint. */
5
+ export declare const SAVED_PIN: unique symbol;
6
+ /** The full 8-digit PIN, or `SAVED_PIN`. */
7
+ export type Pin = string | typeof SAVED_PIN;
8
+ /**
9
+ * Progress of a session, in order: `detected` the card is on the phone; `pin` the PIN is being verified; `ready` the PIN
10
+ * is verified and the certificate read, `use` starts; `signing` a signature starts, once per `sign`; `done` `use`
11
+ * resolved and the session is over.
12
+ */
13
+ export type CieStep = 'detected' | 'pin' | 'ready' | 'signing' | 'done';
14
+ /**
15
+ * `prompt`, `error`: texts of the iOS NFC sheet while it waits for the card and when the session fails; `messages`: its
16
+ * text at each step. `onStep`: called at each step, e.g. to show progress on Android, which has no NFC sheet.
17
+ * `savePin`: once the card accepts the PIN, save it for `SAVED_PIN`. `biometricPrompt`, `biometricCancel`: texts of the
18
+ * biometric prompt shown before the card is read, wherever the secure hardware requires one: `SAVED_PIN` everywhere,
19
+ * `savePin` on Android only.
20
+ */
21
+ export type CieOptions = {
22
+ prompt?: string;
23
+ error?: string;
24
+ messages?: Partial<Record<CieStep, string>>;
25
+ onStep?: (step: CieStep) => void;
26
+ savePin?: boolean;
27
+ biometricPrompt?: string;
28
+ biometricCancel?: string;
29
+ };
30
+ /** A card whose PIN has been verified, for as long as `use` runs. */
31
+ export type CieSession = {
32
+ /** DER authentication certificate; its subject holds the holder's name and fiscal code. */
33
+ certificate: Bytes;
34
+ /** RSA PKCS#1 v1.5 signature of a DER DigestInfo, checked against `certificate`. Calls run one at a time. */
35
+ sign(digestInfo: Bytes): Promise<Bytes>;
36
+ };
37
+ /**
38
+ * Waits for the card and verifies the PIN, then runs `use`: every `sign` goes through this NFC session, which ends when
39
+ * `use` settles.
40
+ */
41
+ export declare function cieSession<T>(pin: Pin, use: (session: CieSession) => Promise<T>, options?: CieOptions): Promise<T>;
42
+ /** Aborts the CIE session in progress. */
43
+ export declare const cancel: () => void;
44
+ /** Whether a PIN is saved, without showing any UI. */
45
+ export declare const pinStatus: () => Promise<PinStatus>;
46
+ /** Deletes the saved PIN. */
47
+ export declare const forgetPin: () => Promise<void>;
package/build/index.js ADDED
@@ -0,0 +1,67 @@
1
+ import { trimDer, verify } from './certificate';
2
+ import Native, {} from './native';
3
+ /** Pass instead of the PIN to use the one saved with `savePin`, unlocked with Face ID, Touch ID or a fingerprint. */
4
+ export const SAVED_PIN = Symbol('SAVED_PIN');
5
+ const STEPS = ['detected', 'pin', 'ready', 'signing', 'done'];
6
+ const MESSAGES = {
7
+ detected: 'CIE rilevata, non allontanarla',
8
+ pin: 'Verifica del PIN…',
9
+ ready: 'PIN verificato',
10
+ signing: 'Firma in corso…',
11
+ done: 'Operazione completata',
12
+ };
13
+ let session = 0; // id of the latest NFC session
14
+ /**
15
+ * Waits for the card and verifies the PIN, then runs `use`: every `sign` goes through this NFC session, which ends when
16
+ * `use` settles.
17
+ */
18
+ export async function cieSession(pin, use, options = {}) {
19
+ if (pin !== SAVED_PIN && !/^\d{8}$/.test(pin))
20
+ throw new Error('The CIE PIN has 8 digits');
21
+ const { prompt = "Avvicina la CIE al retro dell'iPhone", error = 'Operazione non riuscita', savePin = false, biometricPrompt = pin === SAVED_PIN ? 'Sblocca il PIN della CIE' : 'Salva il PIN della CIE', biometricCancel = 'Annulla', onStep, } = options;
22
+ const message = (s) => options.messages?.[s] ?? MESSAGES[s];
23
+ const id = ++session;
24
+ let done = false;
25
+ // Native events can arrive after `ready`: steps only move forward, except one `signing` per signature.
26
+ let reached = -1;
27
+ const step = (s) => {
28
+ const i = STEPS.indexOf(s);
29
+ if (i < reached || (i === reached && s !== 'signing'))
30
+ return;
31
+ reached = i;
32
+ onStep?.(s);
33
+ };
34
+ const subscription = onStep && Native.addListener('onStep', (event) => event.id === id && step(event.step));
35
+ try {
36
+ const sheet = { prompt, detected: message('detected'), pin: message('pin'), ready: message('ready'), signing: message('signing') };
37
+ const certificate = trimDer(await Native.connect(id, pin === SAVED_PIN ? null : pin, savePin, sheet, biometricPrompt, biometricCancel));
38
+ step('ready');
39
+ let last = Promise.resolve();
40
+ const sign = (digestInfo) => {
41
+ const input = digestInfo.slice(); // signed later, once the calls before it finish
42
+ const signature = last.then(async () => {
43
+ step('signing');
44
+ const signature = await Native.sign(id, input);
45
+ verify(certificate, input, signature);
46
+ return signature;
47
+ });
48
+ last = signature.catch(() => { });
49
+ return signature;
50
+ };
51
+ const result = await use({ certificate, sign });
52
+ done = true;
53
+ return result;
54
+ }
55
+ finally {
56
+ subscription?.remove();
57
+ Native.close(id, done ? null : error, done ? message('done') : null);
58
+ if (done)
59
+ step('done');
60
+ }
61
+ }
62
+ /** Aborts the CIE session in progress. */
63
+ export const cancel = () => Native.close(session, null, null);
64
+ /** Whether a PIN is saved, without showing any UI. */
65
+ export const pinStatus = () => Native.pinStatus();
66
+ /** Deletes the saved PIN. */
67
+ export const forgetPin = () => Native.forgetPin();
@@ -0,0 +1,38 @@
1
+ /** `saved`: a PIN is saved. `available`: a strong biometric is enrolled, so one can be. `unavailable`: neither. */
2
+ export type PinStatus = 'saved' | 'available' | 'unavailable';
3
+ /** Steps only the native side sees: the card was detected; the PIN is being verified. */
4
+ export type NativeStep = 'detected' | 'pin';
5
+ /** iOS NFC sheet texts: `prompt` while waiting for the card, then one per step. */
6
+ export type NativeMessages = {
7
+ prompt: string;
8
+ detected: string;
9
+ pin: string;
10
+ ready: string;
11
+ signing: string;
12
+ };
13
+ type CieSignModule = {
14
+ /**
15
+ * Waits for the card, verifies the PIN and returns its certificate; NFC session `id` stays open.
16
+ * A null `pin` unlocks the saved one; `save` saves `pin` once the card accepts it. Each shows the biometric prompt first
17
+ * only where the secure hardware requires it: unlocking everywhere, saving on Android.
18
+ */
19
+ connect(id: number, pin: string | null, save: boolean, messages: NativeMessages, biometricPrompt: string, biometricCancel: string): Promise<Uint8Array>;
20
+ /** Signs a DigestInfo in session `id`, as many times as needed until it closes. */
21
+ sign(id: number, digestInfo: Uint8Array): Promise<Uint8Array>;
22
+ /**
23
+ * Ends every session up to `id`, also one whose connect has not arrived yet. iOS shows `error` in the NFC sheet, or
24
+ * else `done` next to its checkmark.
25
+ */
26
+ close(id: number, error: string | null, done: string | null): void;
27
+ pinStatus(): Promise<PinStatus>;
28
+ forgetPin(): Promise<void>;
29
+ /** `onStep` fires as session `id` reaches a step only the native side sees. */
30
+ addListener(event: 'onStep', listener: (event: {
31
+ id: number;
32
+ step: NativeStep;
33
+ }) => void): {
34
+ remove(): void;
35
+ };
36
+ };
37
+ declare const _default: CieSignModule;
38
+ export default _default;
@@ -0,0 +1,2 @@
1
+ import { requireNativeModule } from 'expo';
2
+ export default requireNativeModule('CieSign');
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["apple", "android"],
3
+ "apple": {
4
+ "modules": ["CieSignModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["it.codingduck.ciesign.CieSignModule"]
8
+ }
9
+ }
@@ -0,0 +1,330 @@
1
+ // Derived from PagoPA cie-sdk-ios (https://github.com/pagopa/cie-sdk-ios), MIT License, Copyright (c) 2025 PagoPA S.p.A.
2
+
3
+ import CommonCrypto
4
+ import CryptoKit
5
+ import Foundation
6
+ import Security
7
+
8
+ enum CieCardError: Error {
9
+ case wrongPin(attemptsLeft: Int), pinLocked, notCie, status(UInt16), secureMessaging, invalidResponse
10
+ }
11
+
12
+ /// CIE operations over a raw APDU transport (CoreNFC is wrapped by the caller).
13
+ final class CieCard {
14
+ private let transmit: (Data) async throws -> Data
15
+ private var sm: (enc: [UInt8], mac: [UInt8], ssc: [UInt8])?
16
+ /// Source of the IFD randoms; replaceable for deterministic tests.
17
+ var random: (Int) -> [UInt8] = { count in
18
+ var bytes = [UInt8](repeating: 0, count: count)
19
+ precondition(SecRandomCopyBytes(kSecRandomDefault, count, &bytes) == errSecSuccess)
20
+ return bytes
21
+ }
22
+
23
+ /// `transmit` sends a command APDU and returns response data followed by SW1 SW2.
24
+ init(transmit: @escaping (Data) async throws -> Data) {
25
+ self.transmit = transmit
26
+ }
27
+
28
+ /// NIS from EF.ID_Servizi (EF 1001): identifies the card and is readable without the PIN.
29
+ func serviceId() async throws -> Data {
30
+ sm = nil
31
+ try await selectApplications()
32
+ try await send(0x00, 0xA4, 0x02, 0x04, [0x10, 0x01])
33
+ let r = try await send(0x00, 0xB0, 0x00, 0x00, le: 0x0C)
34
+ guard r.sw == 0x9000 else { throw CieCardError.notCie }
35
+ return Data(r.data)
36
+ }
37
+
38
+ /// Selects IAS + CIE apps, opens secure messaging (DH key exchange + DAPP device/chip authentication), verifies the 8-digit PIN (key ref 0x81).
39
+ func open(pin: String) async throws {
40
+ sm = nil
41
+ try await selectApplications()
42
+
43
+ var gpq = [[UInt8]]()
44
+ for tag: UInt8 in [0x97, 0x98, 0x99] {
45
+ let r = try await ok(send(0x00, 0xCB, 0x3F, 0xFF, [0x4D, 0x0A, 0x70, 0x08, 0xBF, 0xA1, 0x01, 0x04, 0xA3, 0x02, tag, 0x00]))
46
+ gpq.append(try find(UInt(tag), in: r))
47
+ }
48
+ let (g, p, q) = (gpq[0], gpq[1], gpq[2])
49
+ let chip = try await readFile(0x1004, chunk: 0xFF)
50
+ let iccKey = try children(of: chip, in: children(of: chip, in: 0..<1)[0].value).map { Array(chip[$0.value]) }
51
+ guard iccKey.count >= 2 else { throw CieCardError.invalidResponse }
52
+ let ca = try await ok(send(0x00, 0xCB, 0x3F, 0xFF, [0x4D, 0x09, 0x70, 0x07, 0xBF, 0xA0, 0x04, 0x03, 0x7F, 0x49, 0x80]))
53
+ let (caN, caCHR, caCHA) = try (Array(find(0x81, in: ca).drop { $0 == 0 }), find(0x5F20, in: ca), find(0x5F4C, in: ca))
54
+
55
+ var x: [UInt8]
56
+ repeat { x = random(q.count) } while !x.lexicographicallyPrecedes(q)
57
+ let pubIFD = try modPow(g, x, p)
58
+ try await ok(send(0x00, 0x22, 0x41, 0xA6, tlv(0x80, [0x9B]) + tlv(0x83, [0x81]) + tlv(0x91, pubIFD)))
59
+ let pubICC = try await find(0x91, in: ok(send(0x00, 0xCB, 0x3F, 0xFF, [0x4D, 0x04, 0xA6, 0x02, 0x91, 0x00])))
60
+ let secret = try modPow(pubICC, x, p)
61
+ sm = (Array(sha256(secret + [0, 0, 0, 1]).prefix(16)), Array(sha256(secret + [0, 0, 0, 2]).prefix(16)), [0, 0, 0, 0, 0, 0, 0, 1])
62
+
63
+ // DAPP (IAS-ECC 5.2.3): the IFD certificate is signed with the CA key, recoverable part per ISO 9796-2 scheme 1
64
+ let serialIFD = bytes("2000000000000001")
65
+ let chr = [0, 0, 0, 0] + serialIFD
66
+ let car = Array(caCHR.dropFirst(4))
67
+ let cert = [0x8A] + car + chr + caCHA.prefix(6) + [0x01] + bytes("2A8122F42A02040104") + Self.dappModulus + [0x00, 0x01, 0x00, 0x01]
68
+ let split = caN.count - 34
69
+ guard split > 0, split < cert.count else { throw CieCardError.invalidResponse }
70
+ let certSignature = try modPow([0x6A] + cert.prefix(split) + sha256(cert) + [0xBC], Self.caPrivateExponent, caN)
71
+ try await ok(send(0x00, 0x22, 0x81, 0xB6, tlv(0x80, [0x41]) + tlv(0x83, [0x84])))
72
+ try await ok(send(0x00, 0x2A, 0x00, 0xAE, tlv(0x7F21, tlv(0x5F37, certSignature) + tlv(0x5F38, Array(cert[split...])) + tlv(0x42, car))))
73
+ try await ok(send(0x00, 0x22, 0x81, 0xA4, tlv(0x83, chr)))
74
+ let rndICC = try await ok(send(0x00, 0x84, 0x00, 0x00, le: 0x08))
75
+ let prnd = random(Self.dappModulus.count - 34)
76
+ let digest = sha256(prnd + pubIFD + serialIFD + rndICC + pubICC + g + p + q)
77
+ let extAuth = try modPow([0x6A] + prnd + digest + [0xBC], Self.dappPrivateExponent, Self.dappModulus)
78
+ try await ok(send(0x00, 0x82, 0x00, 0x00, serialIFD + extAuth))
79
+
80
+ try await ok(send(0x00, 0x22, 0x41, 0xA4, tlv(0x84, [0x82]) + tlv(0x80, [0x9B])))
81
+ let rndIFD = random(8)
82
+ let intAuth = try await ok(send(0x00, 0x88, 0x00, 0x00, rndIFD))
83
+ guard intAuth.count > 8 else { throw CieCardError.secureMessaging }
84
+ let m = try modPow(Array(intAuth[8...]), iccKey[1], iccKey[0])
85
+ guard m.count > 34, m.first == 0x6A, m.last == 0xBC,
86
+ Array(m[(m.count - 33)..<(m.count - 1)]) == sha256(m[1..<(m.count - 33)] + pubICC + intAuth.prefix(8) + rndIFD + pubIFD + g + p + q)
87
+ else { throw CieCardError.secureMessaging }
88
+ sm?.ssc = Array(rndICC.suffix(4) + rndIFD.suffix(4))
89
+
90
+ switch try await send(0x00, 0x20, 0x00, 0x81, Array(pin.utf8)).sw {
91
+ case 0x9000: return
92
+ case 0x6983, 0x63C0: throw CieCardError.pinLocked
93
+ case let sw where sw & 0xFFF0 == 0x63C0: throw CieCardError.wrongPin(attemptsLeft: Int(sw & 0xF))
94
+ case let sw: throw CieCardError.status(sw)
95
+ }
96
+ }
97
+
98
+ /// DER authentication certificate from EF 1003, trimmed to its outer ASN.1 length.
99
+ func certificate() async throws -> Data {
100
+ guard sm != nil else { throw CieCardError.secureMessaging }
101
+ return Data(try await readFile(0x1003, chunk: 0x80))
102
+ }
103
+
104
+ /// Card applies PKCS#1 v1.5 type-1 padding to `digestInfo`; returns the raw RSA signature.
105
+ func sign(_ digestInfo: Data) async throws -> Data {
106
+ guard sm != nil else { throw CieCardError.secureMessaging }
107
+ try await ok(send(0x00, 0x22, 0x41, 0xA4, tlv(0x80, [0x02]) + tlv(0x84, [0x81])))
108
+ return Data(try await ok(send(0x00, 0x88, 0x00, 0x00, [UInt8](digestInfo))))
109
+ }
110
+
111
+ private func selectApplications() async throws {
112
+ var sw = try await send(0x00, 0xA4, 0x04, 0x0C, bytes("A0000000308000000009816001")).sw
113
+ if sw == 0x6A82 { sw = try await send(0x00, 0xA4, 0x00, 0x00).sw } // NXP chips have no IAS AID: select MF
114
+ guard sw == 0x9000, try await send(0x00, 0xA4, 0x04, 0x0C, bytes("A00000000039")).sw == 0x9000 else {
115
+ throw CieCardError.notCie
116
+ }
117
+ }
118
+
119
+ /// Reads a transparent EF up to the end of its first TLV (files are zero-padded).
120
+ private func readFile(_ id: UInt16, chunk: UInt8) async throws -> [UInt8] {
121
+ try await ok(send(0x00, 0xA4, 0x02, 0x04, [UInt8(id >> 8), UInt8(id & 0xFF)]))
122
+ var out = [UInt8](), le = chunk
123
+ func end() -> Int? { (try? children(of: out, in: 0..<1))?.first?.value.upperBound }
124
+ while end().map({ out.count < $0 }) ?? true {
125
+ let r = try await send(0x00, 0xB0, UInt8(out.count >> 8), UInt8(out.count & 0xFF), le: le)
126
+ if r.sw >> 8 == 0x6C { le = UInt8(r.sw & 0xFF); continue }
127
+ guard [0x9000, 0x6282, 0x6B00].contains(r.sw) else { throw CieCardError.status(r.sw) }
128
+ out += r.data
129
+ guard r.sw == 0x9000, !r.data.isEmpty else { break }
130
+ le = chunk
131
+ }
132
+ guard let end = end(), out.count >= end else { throw CieCardError.invalidResponse }
133
+ return Array(out[..<end])
134
+ }
135
+
136
+ @discardableResult
137
+ private func ok(_ r: (data: [UInt8], sw: UInt16)) throws -> [UInt8] {
138
+ guard r.sw == 0x9000 else { throw CieCardError.status(r.sw) }
139
+ return r.data
140
+ }
141
+
142
+ /// Sends a command with chaining, GET RESPONSE and (once established) secure messaging.
143
+ @discardableResult
144
+ private func send(_ cla: UInt8, _ ins: UInt8, _ p1: UInt8, _ p2: UInt8, _ data: [UInt8] = [], le: UInt8? = nil) async throws -> (data: [UInt8], sw: UInt16) {
145
+ let size = sm == nil ? 0xFF : 0xE7
146
+ var offset = 0
147
+ while true {
148
+ let chunk = Array(data[offset..<min(offset + size, data.count)])
149
+ offset += chunk.count
150
+ let last = offset == data.count
151
+ let header = [last ? cla : cla | 0x10, ins, p1, p2]
152
+ let leField: [UInt8] = last ? le.map { [$0] } ?? [] : []
153
+ var r = try await exchange(sm == nil ? header + (chunk.isEmpty ? [] : [UInt8(chunk.count)] + chunk) + leField : wrap(header, chunk, leField))
154
+ while r.sw >> 8 == 0x61 {
155
+ let next = try await exchange([0x00, 0xC0, 0x00, 0x00, UInt8(r.sw & 0xFF)])
156
+ r = (r.data + next.data, next.sw)
157
+ }
158
+ if sm != nil { r = try unwrap(r.data, r.sw) }
159
+ if last || r.sw != 0x9000 { return r }
160
+ }
161
+ }
162
+
163
+ private func exchange(_ apdu: [UInt8]) async throws -> (data: [UInt8], sw: UInt16) {
164
+ let r = [UInt8](try await transmit(Data(apdu)))
165
+ guard r.count >= 2 else { throw CieCardError.invalidResponse }
166
+ return (Array(r.dropLast(2)), UInt16(r[r.count - 2]) << 8 | UInt16(r[r.count - 1]))
167
+ }
168
+
169
+ /// IAS-ECC 7.1.8: 3DES-CBC (zero IV) cryptogram, retail MAC over pad(SSC || header) || DOs.
170
+ private func wrap(_ header: [UInt8], _ data: [UInt8], _ le: [UInt8]) throws -> [UInt8] {
171
+ guard var s = sm else { throw CieCardError.secureMessaging }
172
+ increment(&s.ssc)
173
+ sm = s
174
+ let header = [header[0] | 0x0C] + header[1...]
175
+ var dos = [UInt8]()
176
+ if !data.isEmpty {
177
+ let c = try cipher(kCCEncrypt, kCCAlgorithm3DES, s.enc + s.enc.prefix(8), pad(data))
178
+ dos += header[1] & 1 == 0 ? tlv(0x87, [0x01] + c) : tlv(0x85, c)
179
+ }
180
+ if !le.isEmpty { dos += tlv(0x97, le) }
181
+ dos += try tlv(0x8E, mac(s.mac, pad(s.ssc + header) + dos))
182
+ return header + [UInt8(dos.count)] + dos + [0x00]
183
+ }
184
+
185
+ private func unwrap(_ body: [UInt8], _ sw: UInt16) throws -> (data: [UInt8], sw: UInt16) {
186
+ guard var s = sm else { throw CieCardError.secureMessaging }
187
+ if body.isEmpty {
188
+ guard sw != 0x9000 else { throw CieCardError.secureMessaging }
189
+ return ([], sw)
190
+ }
191
+ increment(&s.ssc)
192
+ sm = s
193
+ var macInput = s.ssc, cc: [UInt8]?, inner: UInt16?, cryptogram: [UInt8]?
194
+ for item in try children(of: body, in: body.indices) {
195
+ let v = Array(body[item.value])
196
+ switch item.tag {
197
+ case 0x8E: cc = v; continue
198
+ case 0x99 where v.count == 2: inner = UInt16(v[0]) << 8 | UInt16(v[1])
199
+ case 0x87 where v.first == 0x01: cryptogram = Array(v.dropFirst())
200
+ case 0x85: cryptogram = v
201
+ default: throw CieCardError.secureMessaging
202
+ }
203
+ macInput += body[item.raw]
204
+ }
205
+ guard let cc, let inner, try cc == mac(s.mac, macInput) else { throw CieCardError.secureMessaging }
206
+ guard let cryptogram else { return ([], inner) }
207
+ let plain = try cipher(kCCDecrypt, kCCAlgorithm3DES, s.enc + s.enc.prefix(8), cryptogram)
208
+ guard let end = plain.lastIndex(where: { $0 != 0 }), plain[end] == 0x80 else { throw CieCardError.secureMessaging }
209
+ return (Array(plain[..<end]), inner)
210
+ }
211
+
212
+ private static let dappModulus = bytes(
213
+ "BA2837AB4C6BB827577BFF4EB7B1E49CDDE0F16614D1EF24C1B75CF70FB12CD18F4D14E2814BA4877EA800E175906076B562BA535973C5D8B378051D8AFC7407"
214
+ + "A1D919529E03C106CDA18D699AFB0D8AB4FDDD9DC719159A50DE9468F02AB103E282A50E716EC23CDA5BFC4A232B09A4B2C70745939549099B4483CBAE62D009"
215
+ + "9674DBF6F39B7223A99D88E33F1A0CDEDEEBBDC35517ABE9880AAB240E1EA166283A274A9AD93B4B1D19F3679F3E8B5FF6A1E0ED736E84D5ABE03C59E7346B42"
216
+ + "18755D75366CBF4136F0A26C3DC70A69ABAAF66E13A1B2FAAD052CA6EC9C51E2AED14D16E090254DC3F64EA2BD8A836BBA99DEFACBA3A613AEEDD93A9615273D")
217
+ private static let dappPrivateExponent = bytes(
218
+ "4716C2A38CCC7A07B415EB1A6175F2AAA0E49CEAF1BA75CBA09A684B04D8111879D3E2CCD8B94D3C5CF6C55753F0ED9587910B3C77258A01460FE84C2EDE5764"
219
+ + "EEBE9C37FB95CD69CEAF09F4B1357C276314AB43EC5B3CEFB0403F868F688E2EC09A4973E987756F8DA7A101A2CA75A54A8C4CCF9A1B6147E4DE56423AF70B20"
220
+ + "67179C5EEB646867863478D752C7F412DB277541575AA0619D30BCCC8D87E6170B33439A2C93F2D97E18C0A82343A6012A5BB1822808F01B5CFD85673AC0964C"
221
+ + "5F3CFD2DAF8142359764A9ADB9E3F76DB613461C1BC913DC9AC0AB50D365F77CB93194C98AA966D89CDD555125A5E59ECF4FA3F0C3FD610CD3D056439338FD81")
222
+ /// Private exponent matching the card's external-authentication CA public key (GET DATA 7F49).
223
+ private static let caPrivateExponent = bytes(
224
+ "186B31488C25DCF85D953D3630C0D073BA1C6AA24581AD254F3B6719C5D72CCA3E5CDC5A1E5316578D75954FF73B237B532C9F8DE4A2C4C911385A23E63E33E4"
225
+ + "7EE45E66EFD49B18E02CFF87598C39109E8F86A66BC330249CE3FCAD655DCDBF98C9C5E479321AF53B517D041061880A647BBE0FF813683470E6C50094CE81D0"
226
+ + "64E204E351BD3AE0A7947D8E91C3FD5C0A15233C349A5215A4E66E21C5D33498E71991EA24473B29F1475F6FD9BE3996E19BD474FAD1B41EA0DCD2FC16C9BFFA"
227
+ + "071BFEC1B22415184811C1985FBFE3E7B4F44A4B3C8DFAB4D90CECFC5E608D673E6762C62CB798341271149BA688162EC7D0E3468F65A94AB4AD1AB67E37BFC1")
228
+ }
229
+
230
+ private func bytes(_ hex: String) -> [UInt8] {
231
+ let c = Array(hex.utf8)
232
+ return stride(from: 0, to: c.count, by: 2).map { UInt8(String(decoding: c[$0...$0 + 1], as: UTF8.self), radix: 16)! }
233
+ }
234
+
235
+ private func sha256<D: DataProtocol>(_ data: D) -> [UInt8] { Array(SHA256.hash(data: data)) }
236
+
237
+ private func pad(_ data: [UInt8]) -> [UInt8] {
238
+ data + [0x80] + [UInt8](repeating: 0, count: 7 - data.count % 8)
239
+ }
240
+
241
+ private func increment(_ counter: inout [UInt8]) {
242
+ for i in counter.indices.reversed() {
243
+ counter[i] &+= 1
244
+ if counter[i] != 0 { return }
245
+ }
246
+ }
247
+
248
+ /// CBC with zero IV, no padding.
249
+ private func cipher(_ op: Int, _ alg: Int, _ key: [UInt8], _ data: [UInt8]) throws -> [UInt8] {
250
+ var out = [UInt8](repeating: 0, count: data.count)
251
+ var moved = 0
252
+ let (count, iv) = (data.count, [UInt8](repeating: 0, count: 8))
253
+ guard CCCrypt(CCOperation(op), CCAlgorithm(alg), 0, key, key.count, iv, data, count, &out, count, &moved) == kCCSuccess else {
254
+ throw CieCardError.secureMessaging
255
+ }
256
+ return out
257
+ }
258
+
259
+ /// ISO 9797-1 MAC algorithm 3 (retail MAC) with ISO padding.
260
+ private func mac(_ key: [UInt8], _ data: [UInt8]) throws -> [UInt8] {
261
+ let (k1, k2) = (Array(key[0..<8]), Array(key[8..<16]))
262
+ let last = Array(try cipher(kCCEncrypt, kCCAlgorithmDES, k1, pad(data)).suffix(8))
263
+ return try cipher(kCCEncrypt, kCCAlgorithmDES, k1, cipher(kCCDecrypt, kCCAlgorithmDES, k2, last))
264
+ }
265
+
266
+ /// base^exp mod modulus, computed as a raw RSA public-key operation (corecrypto accepts any exponent, even ones).
267
+ private func modPow(_ base: [UInt8], _ exp: [UInt8], _ modulus: [UInt8]) throws -> [UInt8] {
268
+ func integer(_ v: [UInt8]) -> [UInt8] {
269
+ let v = Array(v.drop { $0 == 0 })
270
+ return tlv(0x02, (v.first ?? 0x80) < 0x80 ? v : [0] + v)
271
+ }
272
+ let (n, b) = (Array(modulus.drop { $0 == 0 }), Array(base.drop { $0 == 0 }))
273
+ let attributes = [kSecAttrKeyType: kSecAttrKeyTypeRSA, kSecAttrKeyClass: kSecAttrKeyClassPublic] as CFDictionary
274
+ guard b.count <= n.count, let key = SecKeyCreateWithData(Data(tlv(0x30, integer(n) + integer(exp))) as CFData, attributes, nil),
275
+ let r = SecKeyCreateEncryptedData(key, .rsaEncryptionRaw, Data(count: n.count - b.count) + b as CFData, nil)
276
+ else { throw CieCardError.secureMessaging }
277
+ return [UInt8](r as Data)
278
+ }
279
+
280
+ private func tlv(_ tag: UInt, _ value: [UInt8]) -> [UInt8] {
281
+ let n = value.count
282
+ let length: [UInt8] = n < 0x80 ? [UInt8(n)] : n < 0x100 ? [0x81, UInt8(n)] : [0x82, UInt8(n >> 8), UInt8(n & 0xFF)]
283
+ return (tag > 0xFF ? [UInt8(tag >> 8), UInt8(tag & 0xFF)] : [UInt8(tag)]) + length + value
284
+ }
285
+
286
+ private typealias TLV = (tag: UInt, constructed: Bool, raw: Range<Int>, value: Range<Int>)
287
+
288
+ /// BER-TLV items starting within `range` of `bytes`; 00/FF bytes between items are skipped as padding.
289
+ private func children(of bytes: [UInt8], in range: Range<Int>) throws -> [TLV] {
290
+ var items = [TLV]()
291
+ var i = range.lowerBound
292
+ func next() throws -> UInt8 {
293
+ guard i < bytes.count else { throw CieCardError.invalidResponse }
294
+ defer { i += 1 }
295
+ return bytes[i]
296
+ }
297
+ while i < range.upperBound {
298
+ let start = i, first = try next()
299
+ if first == 0x00 || first == 0xFF { continue }
300
+ var tag = UInt(first)
301
+ if first & 0x1F == 0x1F {
302
+ var b: UInt8
303
+ repeat { b = try next(); tag = tag << 8 | UInt(b) } while b & 0x80 != 0
304
+ }
305
+ var length = try Int(next())
306
+ if length > 0x80 {
307
+ guard length <= 0x83 else { throw CieCardError.invalidResponse }
308
+ length = try (0..<(length & 0x7F)).reduce(0) { acc, _ in try acc << 8 | Int(next()) }
309
+ } else if length == 0x80 {
310
+ throw CieCardError.invalidResponse
311
+ }
312
+ guard i + length <= bytes.count else { throw CieCardError.invalidResponse }
313
+ items.append((tag, first & 0x20 != 0, start..<(i + length), i..<(i + length)))
314
+ i += length
315
+ }
316
+ return items
317
+ }
318
+
319
+ /// Depth-first search for the value of `tag`.
320
+ private func find(_ tag: UInt, in bytes: [UInt8]) throws -> [UInt8] {
321
+ func search(_ range: Range<Int>) throws -> [UInt8]? {
322
+ for item in try children(of: bytes, in: range) {
323
+ if item.tag == tag { return Array(bytes[item.value]) }
324
+ if item.constructed, let v = try search(item.value) { return v }
325
+ }
326
+ return nil
327
+ }
328
+ guard let v = try search(bytes.indices), v.count > 0 else { throw CieCardError.invalidResponse }
329
+ return v
330
+ }
@@ -0,0 +1,18 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'CieSign'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.license = package['license']
10
+ s.author = package['author']
11
+ s.homepage = package['homepage']
12
+ s.platforms = { :ios => '16.4' }
13
+ s.swift_version = '5.9'
14
+ s.source = { git: 'https://github.com/codingducksrl/ciesign-react-native.git' }
15
+ s.static_framework = true
16
+ s.dependency 'ExpoModulesCore'
17
+ s.source_files = '*.swift'
18
+ end