ciesign-react-native 0.1.0 → 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.
- package/README.md +22 -1
- package/android/src/main/java/it/codingduck/ciesign/CieSignModule.kt +9 -2
- package/build/index.d.ts +13 -3
- package/build/index.js +29 -4
- package/build/native.d.ts +23 -3
- package/ios/CieSignModule.swift +45 -12
- package/package.json +1 -1
- package/src/index.ts +50 -7
- package/src/native.ts +14 -3
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ Inputs and outputs are `Uint8Array`. The promise stays pending until the card is
|
|
|
66
66
|
|
|
67
67
|
| Export | |
|
|
68
68
|
|---|---|
|
|
69
|
-
| `cieSession(pin, use, { prompt?, error?, savePin?, biometricPrompt?, biometricCancel? })` | Waits for the card and verifies the PIN, then resolves to what `use({ certificate, sign })` resolves to. The NFC session ends when `use` settles: the card must stay on the phone until then. `pin` is the 8-digit PIN or `SAVED_PIN`. `prompt` and `error` are the iOS NFC sheet texts
|
|
69
|
+
| `cieSession(pin, use, { prompt?, error?, messages?, onStep?, savePin?, biometricPrompt?, biometricCancel? })` | Waits for the card and verifies the PIN, then resolves to what `use({ certificate, sign })` resolves to. The NFC session ends when `use` settles: the card must stay on the phone until then. `pin` is the 8-digit PIN or `SAVED_PIN`. `prompt` and `error` are the iOS NFC sheet texts while waiting for the card and on failure; `messages` and `onStep` are explained under [Progress](#progress), the others under [Saved PIN](#saved-pin). |
|
|
70
70
|
| `certificate` | The card's DER authentication certificate: holder name and fiscal code are in its subject. |
|
|
71
71
|
| `sign(digestInfo)` | RSA PKCS#1 v1.5 signature (the card adds the padding) of a DER DigestInfo, checked against `certificate`. Call it as often as needed while `use` runs; calls run one at a time, in order. |
|
|
72
72
|
| `SAVED_PIN`, `pinStatus()`, `forgetPin()` | See [Saved PIN](#saved-pin). |
|
|
@@ -93,6 +93,27 @@ A PIN that is not 8 digits throws a plain `Error`. Card errors carry a `code`:
|
|
|
93
93
|
|
|
94
94
|
On Android the app must stay in the foreground while the card is read.
|
|
95
95
|
|
|
96
|
+
### Progress
|
|
97
|
+
|
|
98
|
+
A session goes through these steps. On iOS the NFC sheet shows the text of each one; `messages` overrides any of them. `onStep(step)` is called as each one is reached, for example to show progress in your own UI on Android, which has no NFC sheet.
|
|
99
|
+
|
|
100
|
+
| Step | When | Default iOS sheet text |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `detected` | The card is on the phone. | CIE rilevata, non allontanarla |
|
|
103
|
+
| `pin` | The PIN is being verified. | Verifica del PIN… |
|
|
104
|
+
| `ready` | PIN verified and certificate read: `use` starts. | PIN verificato |
|
|
105
|
+
| `signing` | A signature starts, once per `sign`. | Firma in corso… |
|
|
106
|
+
| `done` | `use` resolved and the session is over. The sheet shows it next to its checkmark. | Operazione completata |
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const signatures = await cieSession(pin, use, {
|
|
110
|
+
messages: { done: 'Verbale firmato' },
|
|
111
|
+
onStep: (step) => setStatus(step),
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Steps only move forward. A failed session stops at the last step it reached and rejects; on iOS the sheet shows `error`.
|
|
116
|
+
|
|
96
117
|
### Saved PIN
|
|
97
118
|
|
|
98
119
|
Optional: apps that always pass the PIN don't need any of this. With `savePin`, the typed PIN is saved once the card accepts it. Later operations pass `SAVED_PIN`, which asks for Face ID, Touch ID or a fingerprint instead of the PIN.
|
|
@@ -33,8 +33,11 @@ class CieSignModule : Module() {
|
|
|
33
33
|
override fun definition() = ModuleDefinition {
|
|
34
34
|
Name("CieSign")
|
|
35
35
|
|
|
36
|
+
Events("onStep")
|
|
37
|
+
|
|
36
38
|
// A null `pin` unlocks the saved one; `save` saves `pin` once the card accepts it. Both show the biometric prompt first.
|
|
37
|
-
|
|
39
|
+
// The messages are for the iOS NFC sheet: Android has none.
|
|
40
|
+
AsyncFunction("connect") { id: Int, pin: String?, save: Boolean, _: Map<String, String>, biometricPrompt: String, biometricCancel: String, promise: Promise ->
|
|
38
41
|
val activity = appContext.currentActivity ?: throw CodedException("NFC_UNAVAILABLE", "No foreground activity", null)
|
|
39
42
|
val adapter = NfcAdapter.getDefaultAdapter(activity)
|
|
40
43
|
if (adapter?.isEnabled != true) throw CodedException("NFC_UNAVAILABLE", "NFC is missing or turned off", null)
|
|
@@ -77,7 +80,7 @@ class CieSignModule : Module() {
|
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
Function("close") { id: Int, _: String? ->
|
|
83
|
+
Function("close") { id: Int, _: String?, _: String? ->
|
|
81
84
|
synchronized(lock) {
|
|
82
85
|
closed = maxOf(closed, id)
|
|
83
86
|
if (session in 1..id) end()
|
|
@@ -144,6 +147,7 @@ class CieSignModule : Module() {
|
|
|
144
147
|
}
|
|
145
148
|
tag = isoDep
|
|
146
149
|
}
|
|
150
|
+
step(id, "detected")
|
|
147
151
|
val c = CieCard(object : OnTransmit {
|
|
148
152
|
override fun sendCommand(apdu: ByteArray, nfcEvents: NfcEvent): ApduResponse {
|
|
149
153
|
val response = isoDep.transceive(apdu)
|
|
@@ -155,6 +159,7 @@ class CieSignModule : Module() {
|
|
|
155
159
|
})
|
|
156
160
|
val nis = c.serviceId()
|
|
157
161
|
if (owner != null && owner != nis) throw CodedException("WRONG_CARD", "Not the card the PIN was saved for", null)
|
|
162
|
+
step(id, "pin")
|
|
158
163
|
val certificate = c.open(pin)
|
|
159
164
|
if (sealed != null) savedPin.store(nis, sealed)
|
|
160
165
|
synchronized(lock) {
|
|
@@ -169,6 +174,8 @@ class CieSignModule : Module() {
|
|
|
169
174
|
}
|
|
170
175
|
}
|
|
171
176
|
|
|
177
|
+
private fun step(id: Int, step: String) = sendEvent("onStep", mapOf("id" to id, "step" to step))
|
|
178
|
+
|
|
172
179
|
private fun end() {
|
|
173
180
|
pending?.reject(cancelled())
|
|
174
181
|
pending = null
|
package/build/index.d.ts
CHANGED
|
@@ -6,13 +6,23 @@ export declare const SAVED_PIN: unique symbol;
|
|
|
6
6
|
/** The full 8-digit PIN, or `SAVED_PIN`. */
|
|
7
7
|
export type Pin = string | typeof SAVED_PIN;
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
12
20
|
*/
|
|
13
21
|
export type CieOptions = {
|
|
14
22
|
prompt?: string;
|
|
15
23
|
error?: string;
|
|
24
|
+
messages?: Partial<Record<CieStep, string>>;
|
|
25
|
+
onStep?: (step: CieStep) => void;
|
|
16
26
|
savePin?: boolean;
|
|
17
27
|
biometricPrompt?: string;
|
|
18
28
|
biometricCancel?: string;
|
package/build/index.js
CHANGED
|
@@ -2,6 +2,14 @@ import { trimDer, verify } from './certificate';
|
|
|
2
2
|
import Native, {} from './native';
|
|
3
3
|
/** Pass instead of the PIN to use the one saved with `savePin`, unlocked with Face ID, Touch ID or a fingerprint. */
|
|
4
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
|
+
};
|
|
5
13
|
let session = 0; // id of the latest NFC session
|
|
6
14
|
/**
|
|
7
15
|
* Waits for the card and verifies the PIN, then runs `use`: every `sign` goes through this NFC session, which ends when
|
|
@@ -10,15 +18,29 @@ let session = 0; // id of the latest NFC session
|
|
|
10
18
|
export async function cieSession(pin, use, options = {}) {
|
|
11
19
|
if (pin !== SAVED_PIN && !/^\d{8}$/.test(pin))
|
|
12
20
|
throw new Error('The CIE PIN has 8 digits');
|
|
13
|
-
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', } = options;
|
|
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];
|
|
14
23
|
const id = ++session;
|
|
15
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));
|
|
16
35
|
try {
|
|
17
|
-
const
|
|
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');
|
|
18
39
|
let last = Promise.resolve();
|
|
19
40
|
const sign = (digestInfo) => {
|
|
20
41
|
const input = digestInfo.slice(); // signed later, once the calls before it finish
|
|
21
42
|
const signature = last.then(async () => {
|
|
43
|
+
step('signing');
|
|
22
44
|
const signature = await Native.sign(id, input);
|
|
23
45
|
verify(certificate, input, signature);
|
|
24
46
|
return signature;
|
|
@@ -31,11 +53,14 @@ export async function cieSession(pin, use, options = {}) {
|
|
|
31
53
|
return result;
|
|
32
54
|
}
|
|
33
55
|
finally {
|
|
34
|
-
|
|
56
|
+
subscription?.remove();
|
|
57
|
+
Native.close(id, done ? null : error, done ? message('done') : null);
|
|
58
|
+
if (done)
|
|
59
|
+
step('done');
|
|
35
60
|
}
|
|
36
61
|
}
|
|
37
62
|
/** Aborts the CIE session in progress. */
|
|
38
|
-
export const cancel = () => Native.close(session, null);
|
|
63
|
+
export const cancel = () => Native.close(session, null, null);
|
|
39
64
|
/** Whether a PIN is saved, without showing any UI. */
|
|
40
65
|
export const pinStatus = () => Native.pinStatus();
|
|
41
66
|
/** Deletes the saved PIN. */
|
package/build/native.d.ts
CHANGED
|
@@ -1,18 +1,38 @@
|
|
|
1
1
|
/** `saved`: a PIN is saved. `available`: a strong biometric is enrolled, so one can be. `unavailable`: neither. */
|
|
2
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
|
+
};
|
|
3
13
|
type CieSignModule = {
|
|
4
14
|
/**
|
|
5
15
|
* Waits for the card, verifies the PIN and returns its certificate; NFC session `id` stays open.
|
|
6
16
|
* A null `pin` unlocks the saved one; `save` saves `pin` once the card accepts it. Each shows the biometric prompt first
|
|
7
17
|
* only where the secure hardware requires it: unlocking everywhere, saving on Android.
|
|
8
18
|
*/
|
|
9
|
-
connect(id: number, pin: string | null, save: boolean,
|
|
19
|
+
connect(id: number, pin: string | null, save: boolean, messages: NativeMessages, biometricPrompt: string, biometricCancel: string): Promise<Uint8Array>;
|
|
10
20
|
/** Signs a DigestInfo in session `id`, as many times as needed until it closes. */
|
|
11
21
|
sign(id: number, digestInfo: Uint8Array): Promise<Uint8Array>;
|
|
12
|
-
/**
|
|
13
|
-
|
|
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;
|
|
14
27
|
pinStatus(): Promise<PinStatus>;
|
|
15
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
|
+
};
|
|
16
36
|
};
|
|
17
37
|
declare const _default: CieSignModule;
|
|
18
38
|
export default _default;
|
package/ios/CieSignModule.swift
CHANGED
|
@@ -10,15 +10,18 @@ public class CieSignModule: Module {
|
|
|
10
10
|
public func definition() -> ModuleDefinition {
|
|
11
11
|
Name("CieSign")
|
|
12
12
|
|
|
13
|
+
Events("onStep")
|
|
14
|
+
|
|
15
|
+
// `messages`: NFC sheet texts by step, `prompt` first.
|
|
13
16
|
AsyncFunction("connect") {
|
|
14
|
-
@MainActor (id: Int, pin: String?, save: Bool,
|
|
17
|
+
@MainActor (id: Int, pin: String?, save: Bool, messages: [String: String], biometricPrompt: String, biometricCancel: String) async throws -> Data in
|
|
15
18
|
guard NFCTagReaderSession.readingAvailable else { throw failure("NFC_UNAVAILABLE", "NFC tag reading is not available") }
|
|
16
19
|
guard id > self.closed else { throw cancelled }
|
|
17
|
-
self.reader?.close(nil)
|
|
18
|
-
let reader = Reader(id: id)
|
|
20
|
+
self.reader?.close(nil, nil)
|
|
21
|
+
let reader = Reader(id: id, messages: messages, onStep: self.reporter(id))
|
|
19
22
|
self.reader = reader
|
|
20
23
|
do {
|
|
21
|
-
return try await reader.connect(pin: pin, save: save,
|
|
24
|
+
return try await reader.connect(pin: pin, save: save, biometricPrompt: biometricPrompt, biometricCancel: biometricCancel)
|
|
22
25
|
} catch {
|
|
23
26
|
let error = coded(reader.invalidation ?? error)
|
|
24
27
|
if pin == nil, let code = (error as? Exception)?.code, code.hasPrefix("WRONG_PIN") || code == "PIN_LOCKED" { SavedPin.delete() }
|
|
@@ -30,16 +33,17 @@ public class CieSignModule: Module {
|
|
|
30
33
|
guard let reader = self.reader, reader.id == id else { throw cancelled }
|
|
31
34
|
do {
|
|
32
35
|
guard let card = reader.card else { throw cancelled } // the reason is in `invalidation`, e.g. a timeout
|
|
36
|
+
reader.show("signing")
|
|
33
37
|
return try await card.sign(digestInfo)
|
|
34
38
|
} catch {
|
|
35
39
|
throw coded(reader.invalidation ?? error)
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
AsyncFunction("close") { @MainActor (id: Int, error: String?) async in
|
|
43
|
+
AsyncFunction("close") { @MainActor (id: Int, error: String?, done: String?) async in
|
|
40
44
|
self.closed = max(self.closed, id)
|
|
41
45
|
guard let reader = self.reader, reader.id <= id else { return }
|
|
42
|
-
reader.close(error)
|
|
46
|
+
reader.close(error, done)
|
|
43
47
|
self.reader = nil
|
|
44
48
|
}
|
|
45
49
|
|
|
@@ -50,14 +54,21 @@ public class CieSignModule: Module {
|
|
|
50
54
|
AsyncFunction("forgetPin") { SavedPin.delete() }
|
|
51
55
|
|
|
52
56
|
OnDestroy {
|
|
53
|
-
DispatchQueue.main.async { self.reader?.close(nil) }
|
|
57
|
+
DispatchQueue.main.async { self.reader?.close(nil, nil) }
|
|
54
58
|
}
|
|
55
59
|
}
|
|
60
|
+
|
|
61
|
+
/// Sends session `id`'s steps to JS as `onStep` events, without keeping the module alive.
|
|
62
|
+
private func reporter(_ id: Int) -> (String) -> Void {
|
|
63
|
+
{ [weak self] step in self?.sendEvent("onStep", ["id": id, "step": step]) }
|
|
64
|
+
}
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
/// One NFCTagReaderSession; used on the main thread only.
|
|
59
68
|
private final class Reader: NSObject, NFCTagReaderSessionDelegate {
|
|
60
69
|
let id: Int
|
|
70
|
+
private let messages: [String: String]
|
|
71
|
+
private let onStep: (String) -> Void
|
|
61
72
|
private var session: NFCTagReaderSession?
|
|
62
73
|
private var pending: CheckedContinuation<Data, Error>?
|
|
63
74
|
private var pin = ""
|
|
@@ -68,13 +79,26 @@ private final class Reader: NSObject, NFCTagReaderSessionDelegate {
|
|
|
68
79
|
private(set) var card: CieCard?
|
|
69
80
|
private(set) var invalidation: Error?
|
|
70
81
|
|
|
71
|
-
init(id: Int) {
|
|
82
|
+
init(id: Int, messages: [String: String], onStep: @escaping (String) -> Void) {
|
|
72
83
|
self.id = id
|
|
84
|
+
self.messages = messages
|
|
85
|
+
self.onStep = onStep
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Shows the NFC sheet text for `step`, if any.
|
|
89
|
+
func show(_ step: String) {
|
|
90
|
+
if let message = messages[step] { session?.alertMessage = message }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Shows `step` in the NFC sheet and reports it to JS.
|
|
94
|
+
private func reach(_ step: String) {
|
|
95
|
+
show(step)
|
|
96
|
+
onStep(step)
|
|
73
97
|
}
|
|
74
98
|
|
|
75
99
|
/// A nil `pin` unlocks the saved one with Face ID or Touch ID; `save` saves `pin` once the card accepts it, with no
|
|
76
100
|
/// prompt, since writing the Keychain item needs no authentication.
|
|
77
|
-
func connect(pin: String?, save: Bool,
|
|
101
|
+
func connect(pin: String?, save: Bool, biometricPrompt: String, biometricCancel: String) async throws -> Data {
|
|
78
102
|
if let pin {
|
|
79
103
|
if save, !SavedPin.canSave { throw SavedPinError.biometryUnavailable }
|
|
80
104
|
(self.pin, self.save) = (pin, save)
|
|
@@ -92,16 +116,22 @@ private final class Reader: NSObject, NFCTagReaderSessionDelegate {
|
|
|
92
116
|
}
|
|
93
117
|
pending = continuation
|
|
94
118
|
self.session = session
|
|
95
|
-
|
|
119
|
+
show("prompt")
|
|
96
120
|
session.begin()
|
|
97
121
|
}
|
|
98
122
|
}
|
|
99
123
|
|
|
100
|
-
|
|
124
|
+
/// Ends the session; the NFC sheet shows `error` if there is one, otherwise `done` next to its checkmark.
|
|
125
|
+
func close(_ error: String?, _ done: String?) {
|
|
101
126
|
closed = true
|
|
102
127
|
biometrics?.invalidate()
|
|
103
128
|
finish(.failure(cancelled))
|
|
104
|
-
if let error {
|
|
129
|
+
if let error {
|
|
130
|
+
session?.invalidate(errorMessage: error)
|
|
131
|
+
} else {
|
|
132
|
+
if let done { session?.alertMessage = done }
|
|
133
|
+
session?.invalidate()
|
|
134
|
+
}
|
|
105
135
|
session = nil
|
|
106
136
|
card = nil
|
|
107
137
|
}
|
|
@@ -120,6 +150,7 @@ private final class Reader: NSObject, NFCTagReaderSessionDelegate {
|
|
|
120
150
|
Task { @MainActor in
|
|
121
151
|
do {
|
|
122
152
|
try await session.connect(to: tags[0])
|
|
153
|
+
reach("detected")
|
|
123
154
|
let card = CieCard { apdu in
|
|
124
155
|
guard let command = NFCISO7816APDU(data: apdu) else { throw failure("CARD_ERROR", "Malformed APDU") }
|
|
125
156
|
let (data, sw1, sw2) = try await tag.sendCommand(apdu: command)
|
|
@@ -127,10 +158,12 @@ private final class Reader: NSObject, NFCTagReaderSessionDelegate {
|
|
|
127
158
|
}
|
|
128
159
|
let serviceId = try await card.serviceId()
|
|
129
160
|
if let owner, owner != serviceId { throw SavedPinError.otherCard }
|
|
161
|
+
reach("pin")
|
|
130
162
|
try await card.open(pin: pin)
|
|
131
163
|
if save { try SavedPin.store(pin, card: serviceId) }
|
|
132
164
|
let certificate = try await card.certificate()
|
|
133
165
|
self.card = card
|
|
166
|
+
show("ready") // JS reports `ready` once connect resolves
|
|
134
167
|
finish(.success(certificate))
|
|
135
168
|
} catch let error as NFCReaderError where error.code == .readerTransceiveErrorSessionInvalidated {
|
|
136
169
|
// didInvalidateWithError follows with the reason (cancel, timeout).
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -10,11 +10,38 @@ export const SAVED_PIN: unique symbol = Symbol('SAVED_PIN');
|
|
|
10
10
|
export type Pin = string | typeof SAVED_PIN;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Progress of a session, in order: `detected` the card is on the phone; `pin` the PIN is being verified; `ready` the PIN
|
|
14
|
+
* is verified and the certificate read, `use` starts; `signing` a signature starts, once per `sign`; `done` `use`
|
|
15
|
+
* resolved and the session is over.
|
|
16
16
|
*/
|
|
17
|
-
export type
|
|
17
|
+
export type CieStep = 'detected' | 'pin' | 'ready' | 'signing' | 'done';
|
|
18
|
+
|
|
19
|
+
const STEPS: CieStep[] = ['detected', 'pin', 'ready', 'signing', 'done'];
|
|
20
|
+
|
|
21
|
+
const MESSAGES: Record<CieStep, string> = {
|
|
22
|
+
detected: 'CIE rilevata, non allontanarla',
|
|
23
|
+
pin: 'Verifica del PIN…',
|
|
24
|
+
ready: 'PIN verificato',
|
|
25
|
+
signing: 'Firma in corso…',
|
|
26
|
+
done: 'Operazione completata',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `prompt`, `error`: texts of the iOS NFC sheet while it waits for the card and when the session fails; `messages`: its
|
|
31
|
+
* text at each step. `onStep`: called at each step, e.g. to show progress on Android, which has no NFC sheet.
|
|
32
|
+
* `savePin`: once the card accepts the PIN, save it for `SAVED_PIN`. `biometricPrompt`, `biometricCancel`: texts of the
|
|
33
|
+
* biometric prompt shown before the card is read, wherever the secure hardware requires one: `SAVED_PIN` everywhere,
|
|
34
|
+
* `savePin` on Android only.
|
|
35
|
+
*/
|
|
36
|
+
export type CieOptions = {
|
|
37
|
+
prompt?: string;
|
|
38
|
+
error?: string;
|
|
39
|
+
messages?: Partial<Record<CieStep, string>>;
|
|
40
|
+
onStep?: (step: CieStep) => void;
|
|
41
|
+
savePin?: boolean;
|
|
42
|
+
biometricPrompt?: string;
|
|
43
|
+
biometricCancel?: string;
|
|
44
|
+
};
|
|
18
45
|
|
|
19
46
|
/** A card whose PIN has been verified, for as long as `use` runs. */
|
|
20
47
|
export type CieSession = {
|
|
@@ -38,15 +65,29 @@ export async function cieSession<T>(pin: Pin, use: (session: CieSession) => Prom
|
|
|
38
65
|
savePin = false,
|
|
39
66
|
biometricPrompt = pin === SAVED_PIN ? 'Sblocca il PIN della CIE' : 'Salva il PIN della CIE',
|
|
40
67
|
biometricCancel = 'Annulla',
|
|
68
|
+
onStep,
|
|
41
69
|
} = options;
|
|
70
|
+
const message = (s: CieStep) => options.messages?.[s] ?? MESSAGES[s];
|
|
42
71
|
const id = ++session;
|
|
43
72
|
let done = false;
|
|
73
|
+
// Native events can arrive after `ready`: steps only move forward, except one `signing` per signature.
|
|
74
|
+
let reached = -1;
|
|
75
|
+
const step = (s: CieStep) => {
|
|
76
|
+
const i = STEPS.indexOf(s);
|
|
77
|
+
if (i < reached || (i === reached && s !== 'signing')) return;
|
|
78
|
+
reached = i;
|
|
79
|
+
onStep?.(s);
|
|
80
|
+
};
|
|
81
|
+
const subscription = onStep && Native.addListener('onStep', (event) => event.id === id && step(event.step));
|
|
44
82
|
try {
|
|
45
|
-
const
|
|
83
|
+
const sheet = { prompt, detected: message('detected'), pin: message('pin'), ready: message('ready'), signing: message('signing') };
|
|
84
|
+
const certificate = trimDer(await Native.connect(id, pin === SAVED_PIN ? null : pin, savePin, sheet, biometricPrompt, biometricCancel));
|
|
85
|
+
step('ready');
|
|
46
86
|
let last: Promise<unknown> = Promise.resolve();
|
|
47
87
|
const sign = (digestInfo: Bytes) => {
|
|
48
88
|
const input = digestInfo.slice(); // signed later, once the calls before it finish
|
|
49
89
|
const signature = last.then(async () => {
|
|
90
|
+
step('signing');
|
|
50
91
|
const signature = await Native.sign(id, input);
|
|
51
92
|
verify(certificate, input, signature);
|
|
52
93
|
return signature;
|
|
@@ -58,12 +99,14 @@ export async function cieSession<T>(pin: Pin, use: (session: CieSession) => Prom
|
|
|
58
99
|
done = true;
|
|
59
100
|
return result;
|
|
60
101
|
} finally {
|
|
61
|
-
|
|
102
|
+
subscription?.remove();
|
|
103
|
+
Native.close(id, done ? null : error, done ? message('done') : null);
|
|
104
|
+
if (done) step('done');
|
|
62
105
|
}
|
|
63
106
|
}
|
|
64
107
|
|
|
65
108
|
/** Aborts the CIE session in progress. */
|
|
66
|
-
export const cancel = () => Native.close(session, null);
|
|
109
|
+
export const cancel = () => Native.close(session, null, null);
|
|
67
110
|
|
|
68
111
|
/** Whether a PIN is saved, without showing any UI. */
|
|
69
112
|
export const pinStatus = (): Promise<PinStatus> => Native.pinStatus();
|
package/src/native.ts
CHANGED
|
@@ -3,19 +3,30 @@ import { requireNativeModule } from 'expo';
|
|
|
3
3
|
/** `saved`: a PIN is saved. `available`: a strong biometric is enrolled, so one can be. `unavailable`: neither. */
|
|
4
4
|
export type PinStatus = 'saved' | 'available' | 'unavailable';
|
|
5
5
|
|
|
6
|
+
/** Steps only the native side sees: the card was detected; the PIN is being verified. */
|
|
7
|
+
export type NativeStep = 'detected' | 'pin';
|
|
8
|
+
|
|
9
|
+
/** iOS NFC sheet texts: `prompt` while waiting for the card, then one per step. */
|
|
10
|
+
export type NativeMessages = { prompt: string; detected: string; pin: string; ready: string; signing: string };
|
|
11
|
+
|
|
6
12
|
type CieSignModule = {
|
|
7
13
|
/**
|
|
8
14
|
* Waits for the card, verifies the PIN and returns its certificate; NFC session `id` stays open.
|
|
9
15
|
* A null `pin` unlocks the saved one; `save` saves `pin` once the card accepts it. Each shows the biometric prompt first
|
|
10
16
|
* only where the secure hardware requires it: unlocking everywhere, saving on Android.
|
|
11
17
|
*/
|
|
12
|
-
connect(id: number, pin: string | null, save: boolean,
|
|
18
|
+
connect(id: number, pin: string | null, save: boolean, messages: NativeMessages, biometricPrompt: string, biometricCancel: string): Promise<Uint8Array>;
|
|
13
19
|
/** Signs a DigestInfo in session `id`, as many times as needed until it closes. */
|
|
14
20
|
sign(id: number, digestInfo: Uint8Array): Promise<Uint8Array>;
|
|
15
|
-
/**
|
|
16
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Ends every session up to `id`, also one whose connect has not arrived yet. iOS shows `error` in the NFC sheet, or
|
|
23
|
+
* else `done` next to its checkmark.
|
|
24
|
+
*/
|
|
25
|
+
close(id: number, error: string | null, done: string | null): void;
|
|
17
26
|
pinStatus(): Promise<PinStatus>;
|
|
18
27
|
forgetPin(): Promise<void>;
|
|
28
|
+
/** `onStep` fires as session `id` reaches a step only the native side sees. */
|
|
29
|
+
addListener(event: 'onStep', listener: (event: { id: number; step: NativeStep }) => void): { remove(): void };
|
|
19
30
|
};
|
|
20
31
|
|
|
21
32
|
export default requireNativeModule<CieSignModule>('CieSign');
|