ciesign-react-native 0.2.0 → 0.3.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 -4
- package/android/src/main/java/it/codingduck/ciesign/CieCard.java +6 -2
- package/android/src/main/java/it/codingduck/ciesign/CieSignModule.kt +63 -26
- package/android/src/main/java/it/codingduck/ciesign/SavedPin.kt +17 -1
- package/build/index.d.ts +18 -6
- package/build/index.js +15 -2
- package/build/native.d.ts +10 -2
- package/ios/CieCard.swift +5 -1
- package/ios/CieSignModule.swift +56 -18
- package/ios/SavedPin.swift +23 -1
- package/package.json +1 -1
- package/src/index.ts +28 -6
- package/src/native.ts +10 -2
package/README.md
CHANGED
|
@@ -66,10 +66,10 @@ 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?, 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). |
|
|
69
|
+
| `cieSession(pin, use, { prompt?, error?, messages?, onStep?, savePin?, biometricPrompt?, biometricCancel?, useSavedCertificate?, beforeCard? })` | 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
|
-
| `SAVED_PIN`, `pinStatus()`, `forgetPin()` | See [Saved PIN](#saved-pin). |
|
|
72
|
+
| `SAVED_PIN`, `pinStatus()`, `forgetPin()`, `savedCertificate()` | See [Saved PIN](#saved-pin). |
|
|
73
73
|
| `cancel()` | Aborts the session in progress. Android has no system NFC sheet, so give users a cancel button. |
|
|
74
74
|
|
|
75
75
|
iOS ends an NFC session 60 seconds after it starts, `use` included: prepare what does not depend on the certificate, such as a PDF's byte range, before calling `cieSession`.
|
|
@@ -101,7 +101,7 @@ A session goes through these steps. On iOS the NFC sheet shows the text of each
|
|
|
101
101
|
|---|---|---|
|
|
102
102
|
| `detected` | The card is on the phone. | CIE rilevata, non allontanarla |
|
|
103
103
|
| `pin` | The PIN is being verified. | Verifica del PIN… |
|
|
104
|
-
| `ready` | PIN verified and certificate read: `use` starts. | PIN verificato |
|
|
104
|
+
| `ready` | PIN verified and certificate read, or taken from the saved one: `use` starts. | PIN verificato |
|
|
105
105
|
| `signing` | A signature starts, once per `sign`. | Firma in corso… |
|
|
106
106
|
| `done` | `use` resolved and the session is over. The sheet shows it next to its checkmark. | Operazione completata |
|
|
107
107
|
|
|
@@ -138,9 +138,27 @@ const signatures =
|
|
|
138
138
|
- **Native only.** A saved PIN is decrypted in native code and sent to the card; it never reaches JavaScript.
|
|
139
139
|
- One PIN per app: saving again replaces it. `forgetPin()` deletes it. `savePin` fails with `BIOMETRIC_UNAVAILABLE` before the card is read when `pinStatus()` is `'unavailable'`.
|
|
140
140
|
|
|
141
|
+
#### Saved certificate
|
|
142
|
+
|
|
143
|
+
The card certificate is saved with the PIN: on iOS in a Keychain item of its own, readable while the device is unlocked, never synced or backed up; on Android next to the encrypted PIN in `noBackupFilesDir`. It is public (it goes into every signature), so no biometric guards it. It is replaced and deleted with the PIN; a `SAVED_PIN` session that reads the certificate from the card saves it too, for PINs saved before it was.
|
|
144
|
+
|
|
145
|
+
`savedCertificate()` returns it without any UI, or `null`. Knowing it before the card is on the phone lets you prepare what to sign before the NFC session, which iOS ends after 60 seconds. Then `useSavedCertificate: true` makes the `SAVED_PIN` session skip reading it: the card is still checked by its NIS (`WRONG_CARD` otherwise) and every signature against the certificate. Without the option, or without a saved certificate, the session reads it from the card as usual.
|
|
146
|
+
|
|
147
|
+
To prepare it after the biometric prompt rather than before, pass `beforeCard`: with `SAVED_PIN`, the session calls it once the prompt has unlocked the PIN, with the saved certificate it will use (or `null`), and waits for it before the NFC session starts. A user who cancels the prompt never reaches your server, and the wait doesn't count toward iOS's 60 seconds. If it throws, the session ends with that error and the card is never read.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
let prepared: Prepared | null = null;
|
|
151
|
+
const signatures = await cieSession(SAVED_PIN, async (cie) => sign(prepared ?? (await prepare(cie.certificate)), cie), {
|
|
152
|
+
useSavedCertificate: true,
|
|
153
|
+
beforeCard: async (certificate) => {
|
|
154
|
+
if (certificate) prepared = await prepare(certificate); // e.g. your server's hashes to sign
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
141
159
|
## How it works
|
|
142
160
|
|
|
143
|
-
`cieSession` opens one NFC session: NIS read (EF 1001), secure messaging (Diffie-Hellman and device authentication), PIN verification, certificate read (EF 1003). Each `sign` then sends `MSE SET` (key `0x81`, RSA PKCS#1) and `INTERNAL AUTHENTICATE` with the DigestInfo over the same secure channel.
|
|
161
|
+
`cieSession` opens one NFC session: NIS read (EF 1001), secure messaging (Diffie-Hellman and device authentication), PIN verification, certificate read (EF 1003, skipped with `useSavedCertificate`). Each `sign` then sends `MSE SET` (key `0x81`, RSA PKCS#1) and `INTERNAL AUTHENTICATE` with the DigestInfo over the same secure channel.
|
|
144
162
|
|
|
145
163
|
- Android drives `CieCommands` from PagoPA's [`cie-sdk-android`](https://github.com/pagopa/cie-sdk-android) through a Java shim, because the class is Kotlin-internal. The dependency is pinned to `0.1.11`: check the shim before upgrading.
|
|
146
164
|
- iOS implements the same protocol in `ios/CieCard.swift`, derived from PagoPA's [`cie-sdk-ios`](https://github.com/pagopa/cie-sdk-ios), whose signing code is not public.
|
|
@@ -17,9 +17,13 @@ final class CieCard {
|
|
|
17
17
|
return commands.getServiceID();
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/** Opens secure messaging
|
|
21
|
-
|
|
20
|
+
/** Opens secure messaging and verifies the PIN. */
|
|
21
|
+
void open(String pin) throws Exception {
|
|
22
22
|
commands.startSecureChannel(pin);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The certificate file (EF 1003, zero padded), once {@link #open} has verified the PIN. */
|
|
26
|
+
byte[] certificate() throws Exception {
|
|
23
27
|
return commands.readCertCie();
|
|
24
28
|
}
|
|
25
29
|
|
|
@@ -28,6 +28,7 @@ class CieSignModule : Module() {
|
|
|
28
28
|
private var tag: IsoDep? = null
|
|
29
29
|
private var card: CieCard? = null
|
|
30
30
|
private var prompt: BiometricPrompt? = null
|
|
31
|
+
private var unlocked: Unlocked? = null
|
|
31
32
|
private val savedPin by lazy { SavedPin(requireNotNull(appContext.reactContext)) }
|
|
32
33
|
|
|
33
34
|
override fun definition() = ModuleDefinition {
|
|
@@ -35,36 +36,54 @@ class CieSignModule : Module() {
|
|
|
35
36
|
|
|
36
37
|
Events("onStep")
|
|
37
38
|
|
|
38
|
-
//
|
|
39
|
-
//
|
|
39
|
+
// Unlocks the saved PIN for `connect` behind the biometric prompt. With `useSavedCertificate`, resolves to the
|
|
40
|
+
// certificate saved with it when it is of the same card, which `connect` then returns instead of reading it.
|
|
41
|
+
AsyncFunction("unlock") { id: Int, biometricPrompt: String, biometricCancel: String, useSavedCertificate: Boolean, promise: Promise ->
|
|
42
|
+
val activity = appContext.currentActivity ?: throw CodedException("NFC_UNAVAILABLE", "No foreground activity", null)
|
|
43
|
+
if (NfcAdapter.getDefaultAdapter(activity)?.isEnabled != true) throw CodedException("NFC_UNAVAILABLE", "NFC is missing or turned off", null)
|
|
44
|
+
start(id, promise)
|
|
45
|
+
try {
|
|
46
|
+
val saved = savedPin.saved() ?: throw CodedException("PIN_NOT_SAVED", "No saved PIN", null)
|
|
47
|
+
authenticate(activity, id, saved.cipher, biometricPrompt, biometricCancel) {
|
|
48
|
+
val pin = try {
|
|
49
|
+
String(it.doFinal(saved.pin))
|
|
50
|
+
} catch (e: Exception) {
|
|
51
|
+
savedPin.delete()
|
|
52
|
+
throw CodedException("PIN_NOT_SAVED", "The saved PIN cannot be decrypted", e)
|
|
53
|
+
}
|
|
54
|
+
val certificate = if (useSavedCertificate) savedPin.certificate(saved.card) else null
|
|
55
|
+
val waiting = synchronized(lock) { take(id)?.also { unlocked = Unlocked(pin, saved.card, certificate) } }
|
|
56
|
+
waiting?.resolve(certificate)
|
|
57
|
+
}
|
|
58
|
+
} catch (e: Exception) {
|
|
59
|
+
take(id)?.reject(coded(e))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A null `pin` continues the session `unlock` started; `save` saves `pin` once the card accepts it, after the biometric
|
|
64
|
+
// prompt. The messages are for the iOS NFC sheet: Android has none.
|
|
40
65
|
AsyncFunction("connect") { id: Int, pin: String?, save: Boolean, _: Map<String, String>, biometricPrompt: String, biometricCancel: String, promise: Promise ->
|
|
41
66
|
val activity = appContext.currentActivity ?: throw CodedException("NFC_UNAVAILABLE", "No foreground activity", null)
|
|
42
67
|
val adapter = NfcAdapter.getDefaultAdapter(activity)
|
|
43
68
|
if (adapter?.isEnabled != true) throw CodedException("NFC_UNAVAILABLE", "NFC is missing or turned off", null)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
69
|
+
val saved = if (pin == null) {
|
|
70
|
+
synchronized(lock) {
|
|
71
|
+
if (id <= closed || session != id) throw cancelled()
|
|
72
|
+
val found = unlocked ?: throw CodedException("PIN_NOT_SAVED", "The saved PIN was not unlocked", null)
|
|
73
|
+
pending = promise
|
|
74
|
+
found
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
start(id, promise)
|
|
78
|
+
null
|
|
49
79
|
}
|
|
50
80
|
try {
|
|
51
81
|
when {
|
|
52
|
-
|
|
53
|
-
val saved = savedPin.saved() ?: throw CodedException("PIN_NOT_SAVED", "No saved PIN", null)
|
|
54
|
-
authenticate(activity, id, saved.cipher, biometricPrompt, biometricCancel) {
|
|
55
|
-
val unlocked = try {
|
|
56
|
-
String(it.doFinal(saved.pin))
|
|
57
|
-
} catch (e: Exception) {
|
|
58
|
-
savedPin.delete()
|
|
59
|
-
throw CodedException("PIN_NOT_SAVED", "The saved PIN cannot be decrypted", e)
|
|
60
|
-
}
|
|
61
|
-
read(activity, adapter, id, unlocked, saved.card, null)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
82
|
+
saved != null -> read(activity, adapter, id, saved.pin, saved.card, null, saved.certificate)
|
|
64
83
|
save -> authenticate(activity, id, savedPin.encrypter(), biometricPrompt, biometricCancel) {
|
|
65
|
-
read(activity, adapter, id, pin, null, savedPin.seal(it, pin))
|
|
84
|
+
read(activity, adapter, id, requireNotNull(pin), null, savedPin.seal(it, requireNotNull(pin)), null)
|
|
66
85
|
}
|
|
67
|
-
else -> read(activity, adapter, id, pin, null, null)
|
|
86
|
+
else -> read(activity, adapter, id, requireNotNull(pin), null, null, null)
|
|
68
87
|
}
|
|
69
88
|
} catch (e: Exception) {
|
|
70
89
|
take(id)?.reject(coded(e))
|
|
@@ -91,16 +110,29 @@ class CieSignModule : Module() {
|
|
|
91
110
|
|
|
92
111
|
AsyncFunction("forgetPin") { savedPin.delete() }
|
|
93
112
|
|
|
113
|
+
AsyncFunction("savedCertificate") { savedPin.certificate() }
|
|
114
|
+
|
|
94
115
|
OnDestroy { synchronized(lock) { end() } }
|
|
95
116
|
}
|
|
96
117
|
|
|
118
|
+
/** Ends any other session and makes `id` the current one, settled by `promise`. */
|
|
119
|
+
private fun start(id: Int, promise: Promise) = synchronized(lock) {
|
|
120
|
+
if (id <= closed) throw cancelled()
|
|
121
|
+
end()
|
|
122
|
+
session = id
|
|
123
|
+
pending = promise
|
|
124
|
+
}
|
|
125
|
+
|
|
97
126
|
private fun take(id: Int): Promise? = synchronized(lock) { pending.takeIf { session == id }.also { if (it != null) pending = null } }
|
|
98
127
|
|
|
99
|
-
/**
|
|
100
|
-
|
|
128
|
+
/**
|
|
129
|
+
* `owner`: NIS the saved PIN belongs to; `sealed`: encrypted PIN to save once the card accepts it; `saved`: that card's
|
|
130
|
+
* certificate file, returned instead of reading it.
|
|
131
|
+
*/
|
|
132
|
+
private fun read(activity: Activity, adapter: NfcAdapter, id: Int, pin: String, owner: String?, sealed: String?, saved: ByteArray?) {
|
|
101
133
|
val flags = NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NFC_B or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK
|
|
102
134
|
synchronized(lock) {
|
|
103
|
-
if (session == id) adapter.enableReaderMode(activity, { onTag(it, id, pin, owner, sealed) }, flags, null)
|
|
135
|
+
if (session == id) adapter.enableReaderMode(activity, { onTag(it, id, pin, owner, sealed, saved) }, flags, null)
|
|
104
136
|
}
|
|
105
137
|
}
|
|
106
138
|
|
|
@@ -133,7 +165,7 @@ class CieSignModule : Module() {
|
|
|
133
165
|
}
|
|
134
166
|
|
|
135
167
|
// Runs on the NFC thread; the tag stays connected after returning, until the session ends.
|
|
136
|
-
private fun onTag(discovered: Tag, id: Int, pin: String, owner: String?, sealed: String?) {
|
|
168
|
+
private fun onTag(discovered: Tag, id: Int, pin: String, owner: String?, sealed: String?, saved: ByteArray?) {
|
|
137
169
|
val promise = take(id) ?: return
|
|
138
170
|
var pinStatus = 0
|
|
139
171
|
try {
|
|
@@ -160,8 +192,9 @@ class CieSignModule : Module() {
|
|
|
160
192
|
val nis = c.serviceId()
|
|
161
193
|
if (owner != null && owner != nis) throw CodedException("WRONG_CARD", "Not the card the PIN was saved for", null)
|
|
162
194
|
step(id, "pin")
|
|
163
|
-
|
|
195
|
+
c.open(pin)
|
|
164
196
|
if (sealed != null) savedPin.store(nis, sealed)
|
|
197
|
+
val certificate = saved ?: c.certificate().also { if (sealed != null || owner != null) savedPin.storeCertificate(nis, it) }
|
|
165
198
|
synchronized(lock) {
|
|
166
199
|
if (session != id) throw cancelled()
|
|
167
200
|
card = c
|
|
@@ -181,6 +214,7 @@ class CieSignModule : Module() {
|
|
|
181
214
|
pending = null
|
|
182
215
|
prompt?.let { appContext.currentActivity?.runOnUiThread(it::cancelAuthentication) }
|
|
183
216
|
prompt = null
|
|
217
|
+
unlocked = null
|
|
184
218
|
card = null
|
|
185
219
|
runCatching { tag?.close() }
|
|
186
220
|
tag = null
|
|
@@ -188,6 +222,9 @@ class CieSignModule : Module() {
|
|
|
188
222
|
appContext.currentActivity?.let { NfcAdapter.getDefaultAdapter(it)?.disableReaderMode(it) }
|
|
189
223
|
}
|
|
190
224
|
|
|
225
|
+
/** The saved PIN `unlock` decrypted, its card's NIS and, when asked for, that card's saved certificate. */
|
|
226
|
+
private class Unlocked(val pin: String, val card: String, val certificate: ByteArray?)
|
|
227
|
+
|
|
191
228
|
private fun cancelled() = CodedException("CANCELLED", "Cancelled", null)
|
|
192
229
|
|
|
193
230
|
// Under secure messaging the card's status word is in DO 99, ahead of the MAC; otherwise it ends the response.
|
|
@@ -22,12 +22,14 @@ import javax.crypto.spec.GCMParameterSpec
|
|
|
22
22
|
/**
|
|
23
23
|
* The CIE PIN encrypted with an AES-GCM Keystore key, in StrongBox when the device has it and otherwise in the TEE. The key
|
|
24
24
|
* encrypts and decrypts only right after a strong biometric matches one enrolled when it was made. The file is not backed up.
|
|
25
|
+
* Next to it, its card's certificate: public, so stored as is, and replaced or deleted with the PIN.
|
|
25
26
|
*/
|
|
26
27
|
internal class SavedPin(private val context: Context) {
|
|
27
28
|
/** The encrypted PIN, the NIS of its card and the decrypt cipher that a biometric unlocks. */
|
|
28
29
|
class Saved(val card: String, val pin: ByteArray, val cipher: Cipher)
|
|
29
30
|
|
|
30
31
|
private val file = File(context.noBackupFilesDir, "ciesign-pin")
|
|
32
|
+
private val certificateFile = File(context.noBackupFilesDir, "ciesign-certificate")
|
|
31
33
|
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
|
32
34
|
|
|
33
35
|
val canSave get() = BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS
|
|
@@ -61,10 +63,24 @@ internal class SavedPin(private val context: Context) {
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
/** Replaces the saved PIN; `card` is the NIS of the card that accepted it. */
|
|
64
|
-
fun store(card: String, sealed: String)
|
|
66
|
+
fun store(card: String, sealed: String) {
|
|
67
|
+
certificateFile.delete()
|
|
68
|
+
file.writeText("$card\n$sealed")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Saves the certificate file of the card whose PIN is saved; `card` is its NIS. */
|
|
72
|
+
fun storeCertificate(card: String, certificate: ByteArray) = certificateFile.writeText("$card\n${encode(certificate)}")
|
|
73
|
+
|
|
74
|
+
/** The certificate file saved with the PIN, if it is the card with NIS `card` (any when null); null when none is saved. */
|
|
75
|
+
fun certificate(card: String? = null): ByteArray? {
|
|
76
|
+
if (!file.exists()) return null
|
|
77
|
+
val (owner, certificate) = runCatching { certificateFile.readLines() }.getOrNull()?.takeIf { it.size == 2 } ?: return null
|
|
78
|
+
return if (card == null || card == owner) Base64.decode(certificate, Base64.NO_WRAP) else null
|
|
79
|
+
}
|
|
65
80
|
|
|
66
81
|
fun delete() {
|
|
67
82
|
file.delete()
|
|
83
|
+
certificateFile.delete()
|
|
68
84
|
}
|
|
69
85
|
|
|
70
86
|
private fun cipher(mode: Int, iv: ByteArray? = null): Cipher? {
|
package/build/index.d.ts
CHANGED
|
@@ -7,16 +7,20 @@ export declare const SAVED_PIN: unique symbol;
|
|
|
7
7
|
export type Pin = string | typeof SAVED_PIN;
|
|
8
8
|
/**
|
|
9
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
|
|
11
|
-
* resolved and the session is over.
|
|
10
|
+
* is verified and the certificate read (or taken from the saved one), `use` starts; `signing` a signature starts, once
|
|
11
|
+
* per `sign`; `done` `use` resolved and the session is over.
|
|
12
12
|
*/
|
|
13
13
|
export type CieStep = 'detected' | 'pin' | 'ready' | 'signing' | 'done';
|
|
14
14
|
/**
|
|
15
15
|
* `prompt`, `error`: texts of the iOS NFC sheet while it waits for the card and when the session fails; `messages`: its
|
|
16
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
|
|
18
|
-
* biometric prompt shown before the card is read, wherever the secure hardware requires
|
|
19
|
-
* `savePin` on Android only.
|
|
17
|
+
* `savePin`: once the card accepts the PIN, save it for `SAVED_PIN`, with the card certificate. `biometricPrompt`,
|
|
18
|
+
* `biometricCancel`: texts of the biometric prompt shown before the card is read, wherever the secure hardware requires
|
|
19
|
+
* one: `SAVED_PIN` everywhere, `savePin` on Android only. `useSavedCertificate`: with `SAVED_PIN`, skip reading the
|
|
20
|
+
* certificate from the card and use the one saved with the PIN (see `savedCertificate`), when there is one.
|
|
21
|
+
* `beforeCard`: with `SAVED_PIN`, runs once the biometric prompt has unlocked the PIN and before the NFC session waits for
|
|
22
|
+
* the card, with the saved certificate the session will use (`useSavedCertificate`), or null: the session waits for it, so
|
|
23
|
+
* what to sign can be prepared after the prompt and outside iOS's 60-second NFC limit.
|
|
20
24
|
*/
|
|
21
25
|
export type CieOptions = {
|
|
22
26
|
prompt?: string;
|
|
@@ -26,6 +30,8 @@ export type CieOptions = {
|
|
|
26
30
|
savePin?: boolean;
|
|
27
31
|
biometricPrompt?: string;
|
|
28
32
|
biometricCancel?: string;
|
|
33
|
+
useSavedCertificate?: boolean;
|
|
34
|
+
beforeCard?: (savedCertificate: Bytes | null) => Promise<void>;
|
|
29
35
|
};
|
|
30
36
|
/** A card whose PIN has been verified, for as long as `use` runs. */
|
|
31
37
|
export type CieSession = {
|
|
@@ -43,5 +49,11 @@ export declare function cieSession<T>(pin: Pin, use: (session: CieSession) => Pr
|
|
|
43
49
|
export declare const cancel: () => void;
|
|
44
50
|
/** Whether a PIN is saved, without showing any UI. */
|
|
45
51
|
export declare const pinStatus: () => Promise<PinStatus>;
|
|
46
|
-
/** Deletes the saved PIN. */
|
|
52
|
+
/** Deletes the saved PIN and its certificate. */
|
|
47
53
|
export declare const forgetPin: () => Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* The DER certificate of the card whose PIN is saved, without any UI: known before the card is on the phone, e.g. to
|
|
56
|
+
* prepare what to sign before a `SAVED_PIN` session with `useSavedCertificate`. Null when no PIN is saved, or none was
|
|
57
|
+
* saved with it yet: a `SAVED_PIN` session that reads the certificate from the card saves it.
|
|
58
|
+
*/
|
|
59
|
+
export declare function savedCertificate(): Promise<Bytes | null>;
|
package/build/index.js
CHANGED
|
@@ -18,7 +18,7 @@ let session = 0; // id of the latest NFC session
|
|
|
18
18
|
export async function cieSession(pin, use, options = {}) {
|
|
19
19
|
if (pin !== SAVED_PIN && !/^\d{8}$/.test(pin))
|
|
20
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;
|
|
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', useSavedCertificate = false, onStep, beforeCard, } = options;
|
|
22
22
|
const message = (s) => options.messages?.[s] ?? MESSAGES[s];
|
|
23
23
|
const id = ++session;
|
|
24
24
|
let done = false;
|
|
@@ -34,6 +34,10 @@ export async function cieSession(pin, use, options = {}) {
|
|
|
34
34
|
const subscription = onStep && Native.addListener('onStep', (event) => event.id === id && step(event.step));
|
|
35
35
|
try {
|
|
36
36
|
const sheet = { prompt, detected: message('detected'), pin: message('pin'), ready: message('ready'), signing: message('signing') };
|
|
37
|
+
if (pin === SAVED_PIN) {
|
|
38
|
+
const saved = await Native.unlock(id, biometricPrompt, biometricCancel, useSavedCertificate);
|
|
39
|
+
await beforeCard?.(saved && trimDer(saved));
|
|
40
|
+
}
|
|
37
41
|
const certificate = trimDer(await Native.connect(id, pin === SAVED_PIN ? null : pin, savePin, sheet, biometricPrompt, biometricCancel));
|
|
38
42
|
step('ready');
|
|
39
43
|
let last = Promise.resolve();
|
|
@@ -63,5 +67,14 @@ export async function cieSession(pin, use, options = {}) {
|
|
|
63
67
|
export const cancel = () => Native.close(session, null, null);
|
|
64
68
|
/** Whether a PIN is saved, without showing any UI. */
|
|
65
69
|
export const pinStatus = () => Native.pinStatus();
|
|
66
|
-
/** Deletes the saved PIN. */
|
|
70
|
+
/** Deletes the saved PIN and its certificate. */
|
|
67
71
|
export const forgetPin = () => Native.forgetPin();
|
|
72
|
+
/**
|
|
73
|
+
* The DER certificate of the card whose PIN is saved, without any UI: known before the card is on the phone, e.g. to
|
|
74
|
+
* prepare what to sign before a `SAVED_PIN` session with `useSavedCertificate`. Null when no PIN is saved, or none was
|
|
75
|
+
* saved with it yet: a `SAVED_PIN` session that reads the certificate from the card saves it.
|
|
76
|
+
*/
|
|
77
|
+
export async function savedCertificate() {
|
|
78
|
+
const certificate = await Native.savedCertificate();
|
|
79
|
+
return certificate ? trimDer(certificate) : null;
|
|
80
|
+
}
|
package/build/native.d.ts
CHANGED
|
@@ -11,10 +11,16 @@ export type NativeMessages = {
|
|
|
11
11
|
signing: string;
|
|
12
12
|
};
|
|
13
13
|
type CieSignModule = {
|
|
14
|
+
/**
|
|
15
|
+
* Shows the biometric prompt and unlocks the saved PIN for session `id`, which `connect` with a null `pin` then sends to
|
|
16
|
+
* the card. Returns the certificate saved with the PIN when `useSavedCertificate` and it is of the same card: that
|
|
17
|
+
* session's `connect` returns it instead of reading it. Null otherwise.
|
|
18
|
+
*/
|
|
19
|
+
unlock(id: number, biometricPrompt: string, biometricCancel: string, useSavedCertificate: boolean): Promise<Uint8Array | null>;
|
|
14
20
|
/**
|
|
15
21
|
* Waits for the card, verifies the PIN and returns its certificate; NFC session `id` stays open.
|
|
16
|
-
* A null `pin`
|
|
17
|
-
*
|
|
22
|
+
* A null `pin` is the one `unlock` unlocked for `id`; `save` saves `pin` once the card accepts it, after the biometric
|
|
23
|
+
* prompt where the secure hardware requires one (Android).
|
|
18
24
|
*/
|
|
19
25
|
connect(id: number, pin: string | null, save: boolean, messages: NativeMessages, biometricPrompt: string, biometricCancel: string): Promise<Uint8Array>;
|
|
20
26
|
/** Signs a DigestInfo in session `id`, as many times as needed until it closes. */
|
|
@@ -26,6 +32,8 @@ type CieSignModule = {
|
|
|
26
32
|
close(id: number, error: string | null, done: string | null): void;
|
|
27
33
|
pinStatus(): Promise<PinStatus>;
|
|
28
34
|
forgetPin(): Promise<void>;
|
|
35
|
+
/** The certificate file saved with the PIN, without any UI; null when none is saved. */
|
|
36
|
+
savedCertificate(): Promise<Uint8Array | null>;
|
|
29
37
|
/** `onStep` fires as session `id` reaches a step only the native side sees. */
|
|
30
38
|
addListener(event: 'onStep', listener: (event: {
|
|
31
39
|
id: number;
|
package/ios/CieCard.swift
CHANGED
|
@@ -52,8 +52,12 @@ final class CieCard {
|
|
|
52
52
|
let ca = try await ok(send(0x00, 0xCB, 0x3F, 0xFF, [0x4D, 0x09, 0x70, 0x07, 0xBF, 0xA0, 0x04, 0x03, 0x7F, 0x49, 0x80]))
|
|
53
53
|
let (caN, caCHR, caCHA) = try (Array(find(0x81, in: ca).drop { $0 == 0 }), find(0x5F20, in: ca), find(0x5F4C, in: ca))
|
|
54
54
|
|
|
55
|
+
// Some cards zero-pad q to the length of p (a 256-bit q in 256 bytes): drawn at full length, x would almost never be
|
|
56
|
+
// below it and this loop would spin until iOS drops the idle session. Draw it over q's significant bytes instead.
|
|
57
|
+
let order = Array(q.drop { $0 == 0 })
|
|
58
|
+
guard !order.isEmpty else { throw CieCardError.invalidResponse }
|
|
55
59
|
var x: [UInt8]
|
|
56
|
-
repeat { x = random(
|
|
60
|
+
repeat { x = random(order.count) } while !x.lexicographicallyPrecedes(order)
|
|
57
61
|
let pubIFD = try modPow(g, x, p)
|
|
58
62
|
try await ok(send(0x00, 0x22, 0x41, 0xA6, tlv(0x80, [0x9B]) + tlv(0x83, [0x81]) + tlv(0x91, pubIFD)))
|
|
59
63
|
let pubICC = try await find(0x91, in: ok(send(0x00, 0xCB, 0x3F, 0xFF, [0x4D, 0x04, 0xA6, 0x02, 0x91, 0x00])))
|
package/ios/CieSignModule.swift
CHANGED
|
@@ -12,16 +12,36 @@ public class CieSignModule: Module {
|
|
|
12
12
|
|
|
13
13
|
Events("onStep")
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@MainActor (id: Int, pin: String?, save: Bool, messages: [String: String], biometricPrompt: String, biometricCancel: String) async throws -> Data in
|
|
15
|
+
AsyncFunction("unlock") {
|
|
16
|
+
@MainActor (id: Int, biometricPrompt: String, biometricCancel: String, useSavedCertificate: Bool) async throws -> Data? in
|
|
18
17
|
guard NFCTagReaderSession.readingAvailable else { throw failure("NFC_UNAVAILABLE", "NFC tag reading is not available") }
|
|
19
18
|
guard id > self.closed else { throw cancelled }
|
|
20
19
|
self.reader?.close(nil, nil)
|
|
21
|
-
let reader = Reader(id: id,
|
|
20
|
+
let reader = Reader(id: id, onStep: self.reporter(id))
|
|
22
21
|
self.reader = reader
|
|
23
22
|
do {
|
|
24
|
-
return try await reader.
|
|
23
|
+
return try await reader.unlock(biometricPrompt: biometricPrompt, biometricCancel: biometricCancel, useSavedCertificate: useSavedCertificate)
|
|
24
|
+
} catch {
|
|
25
|
+
throw coded(error)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// `messages`: NFC sheet texts by step, `prompt` first. A nil `pin` continues the session `unlock` started.
|
|
30
|
+
AsyncFunction("connect") {
|
|
31
|
+
@MainActor (id: Int, pin: String?, save: Bool, messages: [String: String], biometricPrompt: String, biometricCancel: String) async throws -> Data in
|
|
32
|
+
guard NFCTagReaderSession.readingAvailable else { throw failure("NFC_UNAVAILABLE", "NFC tag reading is not available") }
|
|
33
|
+
guard id > self.closed else { throw cancelled }
|
|
34
|
+
let reader: Reader
|
|
35
|
+
if pin == nil {
|
|
36
|
+
guard let unlocked = self.reader, unlocked.id == id else { throw cancelled }
|
|
37
|
+
reader = unlocked
|
|
38
|
+
} else {
|
|
39
|
+
self.reader?.close(nil, nil)
|
|
40
|
+
reader = Reader(id: id, onStep: self.reporter(id))
|
|
41
|
+
self.reader = reader
|
|
42
|
+
}
|
|
43
|
+
do {
|
|
44
|
+
return try await reader.connect(pin: pin, save: save, messages: messages)
|
|
25
45
|
} catch {
|
|
26
46
|
let error = coded(reader.invalidation ?? error)
|
|
27
47
|
if pin == nil, let code = (error as? Exception)?.code, code.hasPrefix("WRONG_PIN") || code == "PIN_LOCKED" { SavedPin.delete() }
|
|
@@ -53,6 +73,8 @@ public class CieSignModule: Module {
|
|
|
53
73
|
|
|
54
74
|
AsyncFunction("forgetPin") { SavedPin.delete() }
|
|
55
75
|
|
|
76
|
+
AsyncFunction("savedCertificate") { () -> Data? in SavedPin.certificate()?.certificate }
|
|
77
|
+
|
|
56
78
|
OnDestroy {
|
|
57
79
|
DispatchQueue.main.async { self.reader?.close(nil, nil) }
|
|
58
80
|
}
|
|
@@ -67,21 +89,21 @@ public class CieSignModule: Module {
|
|
|
67
89
|
/// One NFCTagReaderSession; used on the main thread only.
|
|
68
90
|
private final class Reader: NSObject, NFCTagReaderSessionDelegate {
|
|
69
91
|
let id: Int
|
|
70
|
-
private
|
|
92
|
+
private var messages: [String: String] = [:]
|
|
71
93
|
private let onStep: (String) -> Void
|
|
72
94
|
private var session: NFCTagReaderSession?
|
|
73
95
|
private var pending: CheckedContinuation<Data, Error>?
|
|
74
96
|
private var pin = ""
|
|
75
97
|
private var save = false
|
|
76
98
|
private var owner: Data? // NIS of the card the saved PIN belongs to
|
|
99
|
+
private var savedCertificate: Data? // that card's certificate file, when the session uses it instead of reading it
|
|
77
100
|
private var biometrics: LAContext?
|
|
78
101
|
private var closed = false
|
|
79
102
|
private(set) var card: CieCard?
|
|
80
103
|
private(set) var invalidation: Error?
|
|
81
104
|
|
|
82
|
-
init(id: Int,
|
|
105
|
+
init(id: Int, onStep: @escaping (String) -> Void) {
|
|
83
106
|
self.id = id
|
|
84
|
-
self.messages = messages
|
|
85
107
|
self.onStep = onStep
|
|
86
108
|
}
|
|
87
109
|
|
|
@@ -96,19 +118,29 @@ private final class Reader: NSObject, NFCTagReaderSessionDelegate {
|
|
|
96
118
|
onStep(step)
|
|
97
119
|
}
|
|
98
120
|
|
|
99
|
-
///
|
|
100
|
-
///
|
|
101
|
-
func
|
|
121
|
+
/// Unlocks the saved PIN with Face ID or Touch ID for `connect`. With `useSavedCertificate`, returns the certificate saved
|
|
122
|
+
/// with it, when it is of the same card: `connect` then returns it instead of reading it from the card.
|
|
123
|
+
func unlock(biometricPrompt: String, biometricCancel: String, useSavedCertificate: Bool) async throws -> Data? {
|
|
124
|
+
let context = LAContext()
|
|
125
|
+
context.localizedCancelTitle = biometricCancel
|
|
126
|
+
context.localizedFallbackTitle = ""
|
|
127
|
+
biometrics = context
|
|
128
|
+
(pin, owner) = try await SavedPin.unlock(context, reason: biometricPrompt)
|
|
129
|
+
guard !closed else { throw cancelled }
|
|
130
|
+
if useSavedCertificate, let saved = SavedPin.certificate(), saved.card == owner { savedCertificate = saved.certificate }
|
|
131
|
+
return savedCertificate
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// A nil `pin` is the saved one `unlock` unlocked; `save` saves `pin` once the card accepts it, with no prompt, since
|
|
135
|
+
/// writing the Keychain item needs no authentication.
|
|
136
|
+
func connect(pin: String?, save: Bool, messages: [String: String]) async throws -> Data {
|
|
102
137
|
if let pin {
|
|
103
138
|
if save, !SavedPin.canSave { throw SavedPinError.biometryUnavailable }
|
|
104
139
|
(self.pin, self.save) = (pin, save)
|
|
105
|
-
} else {
|
|
106
|
-
|
|
107
|
-
context.localizedCancelTitle = biometricCancel
|
|
108
|
-
context.localizedFallbackTitle = ""
|
|
109
|
-
biometrics = context
|
|
110
|
-
(self.pin, owner) = try await SavedPin.unlock(context, reason: biometricPrompt)
|
|
140
|
+
} else if owner == nil {
|
|
141
|
+
throw SavedPinError.notSaved
|
|
111
142
|
}
|
|
143
|
+
self.messages = messages
|
|
112
144
|
guard !closed else { throw cancelled }
|
|
113
145
|
return try await withCheckedThrowingContinuation { continuation in
|
|
114
146
|
guard let session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self, queue: .main) else {
|
|
@@ -161,7 +193,13 @@ private final class Reader: NSObject, NFCTagReaderSessionDelegate {
|
|
|
161
193
|
reach("pin")
|
|
162
194
|
try await card.open(pin: pin)
|
|
163
195
|
if save { try SavedPin.store(pin, card: serviceId) }
|
|
164
|
-
let certificate
|
|
196
|
+
let certificate: Data
|
|
197
|
+
if let savedCertificate {
|
|
198
|
+
certificate = savedCertificate
|
|
199
|
+
} else {
|
|
200
|
+
certificate = try await card.certificate()
|
|
201
|
+
if save || owner != nil { SavedPin.storeCertificate(certificate, card: serviceId) }
|
|
202
|
+
}
|
|
165
203
|
self.card = card
|
|
166
204
|
show("ready") // JS reports `ready` once connect resolves
|
|
167
205
|
finish(.success(certificate))
|
package/ios/SavedPin.swift
CHANGED
|
@@ -7,9 +7,11 @@ enum SavedPinError: Error {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/// The CIE PIN as a Keychain item that the Secure Enclave releases only after Face ID or Touch ID matches one of the
|
|
10
|
-
/// faces or fingers enrolled when it was saved. It is never synced, backed up or restored to another device.
|
|
10
|
+
/// faces or fingers enrolled when it was saved. It is never synced, backed up or restored to another device. Next to it,
|
|
11
|
+
/// its card's certificate: public, so readable without biometrics, and replaced or deleted with the PIN.
|
|
11
12
|
enum SavedPin {
|
|
12
13
|
private static let item: [CFString: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrService: "it.codingduck.ciesign.pin"]
|
|
14
|
+
private static let certificateItem: [CFString: Any] = [kSecClass: kSecClassGenericPassword, kSecAttrService: "it.codingduck.ciesign.certificate"]
|
|
13
15
|
|
|
14
16
|
static var canSave: Bool { LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) }
|
|
15
17
|
|
|
@@ -63,8 +65,28 @@ enum SavedPin {
|
|
|
63
65
|
return (String(decoding: pin, as: UTF8.self), card)
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
/// Saves the certificate file of the card whose PIN is saved; `card` is its NIS. Best effort: without it, sessions read
|
|
69
|
+
/// the certificate from the card.
|
|
70
|
+
static func storeCertificate(_ certificate: Data, card: Data) {
|
|
71
|
+
SecItemDelete(certificateItem as CFDictionary)
|
|
72
|
+
let attributes: [CFString: Any] = [kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, kSecAttrGeneric: card, kSecValueData: certificate]
|
|
73
|
+
SecItemAdd(certificateItem.merging(attributes) { $1 } as CFDictionary, nil)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// The certificate file saved with the PIN and its card's NIS, without any UI; nil when none is saved.
|
|
77
|
+
static func certificate() -> (certificate: Data, card: Data)? {
|
|
78
|
+
guard isSaved else { return nil }
|
|
79
|
+
var result: CFTypeRef?
|
|
80
|
+
let query = certificateItem.merging([kSecReturnData: true, kSecReturnAttributes: true]) { $1 }
|
|
81
|
+
guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess, let found = result as? [CFString: Any],
|
|
82
|
+
let certificate = found[kSecValueData] as? Data, let card = found[kSecAttrGeneric] as? Data
|
|
83
|
+
else { return nil }
|
|
84
|
+
return (certificate, card)
|
|
85
|
+
}
|
|
86
|
+
|
|
66
87
|
static func delete() {
|
|
67
88
|
SecItemDelete(item as CFDictionary)
|
|
89
|
+
SecItemDelete(certificateItem as CFDictionary)
|
|
68
90
|
}
|
|
69
91
|
|
|
70
92
|
private static func access() -> SecAccessControl? {
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -11,8 +11,8 @@ export type Pin = string | typeof SAVED_PIN;
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
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
|
|
15
|
-
* resolved and the session is over.
|
|
14
|
+
* is verified and the certificate read (or taken from the saved one), `use` starts; `signing` a signature starts, once
|
|
15
|
+
* per `sign`; `done` `use` resolved and the session is over.
|
|
16
16
|
*/
|
|
17
17
|
export type CieStep = 'detected' | 'pin' | 'ready' | 'signing' | 'done';
|
|
18
18
|
|
|
@@ -29,9 +29,13 @@ const MESSAGES: Record<CieStep, string> = {
|
|
|
29
29
|
/**
|
|
30
30
|
* `prompt`, `error`: texts of the iOS NFC sheet while it waits for the card and when the session fails; `messages`: its
|
|
31
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
|
|
33
|
-
* biometric prompt shown before the card is read, wherever the secure hardware requires
|
|
34
|
-
* `savePin` on Android only.
|
|
32
|
+
* `savePin`: once the card accepts the PIN, save it for `SAVED_PIN`, with the card certificate. `biometricPrompt`,
|
|
33
|
+
* `biometricCancel`: texts of the biometric prompt shown before the card is read, wherever the secure hardware requires
|
|
34
|
+
* one: `SAVED_PIN` everywhere, `savePin` on Android only. `useSavedCertificate`: with `SAVED_PIN`, skip reading the
|
|
35
|
+
* certificate from the card and use the one saved with the PIN (see `savedCertificate`), when there is one.
|
|
36
|
+
* `beforeCard`: with `SAVED_PIN`, runs once the biometric prompt has unlocked the PIN and before the NFC session waits for
|
|
37
|
+
* the card, with the saved certificate the session will use (`useSavedCertificate`), or null: the session waits for it, so
|
|
38
|
+
* what to sign can be prepared after the prompt and outside iOS's 60-second NFC limit.
|
|
35
39
|
*/
|
|
36
40
|
export type CieOptions = {
|
|
37
41
|
prompt?: string;
|
|
@@ -41,6 +45,8 @@ export type CieOptions = {
|
|
|
41
45
|
savePin?: boolean;
|
|
42
46
|
biometricPrompt?: string;
|
|
43
47
|
biometricCancel?: string;
|
|
48
|
+
useSavedCertificate?: boolean;
|
|
49
|
+
beforeCard?: (savedCertificate: Bytes | null) => Promise<void>;
|
|
44
50
|
};
|
|
45
51
|
|
|
46
52
|
/** A card whose PIN has been verified, for as long as `use` runs. */
|
|
@@ -65,7 +71,9 @@ export async function cieSession<T>(pin: Pin, use: (session: CieSession) => Prom
|
|
|
65
71
|
savePin = false,
|
|
66
72
|
biometricPrompt = pin === SAVED_PIN ? 'Sblocca il PIN della CIE' : 'Salva il PIN della CIE',
|
|
67
73
|
biometricCancel = 'Annulla',
|
|
74
|
+
useSavedCertificate = false,
|
|
68
75
|
onStep,
|
|
76
|
+
beforeCard,
|
|
69
77
|
} = options;
|
|
70
78
|
const message = (s: CieStep) => options.messages?.[s] ?? MESSAGES[s];
|
|
71
79
|
const id = ++session;
|
|
@@ -81,6 +89,10 @@ export async function cieSession<T>(pin: Pin, use: (session: CieSession) => Prom
|
|
|
81
89
|
const subscription = onStep && Native.addListener('onStep', (event) => event.id === id && step(event.step));
|
|
82
90
|
try {
|
|
83
91
|
const sheet = { prompt, detected: message('detected'), pin: message('pin'), ready: message('ready'), signing: message('signing') };
|
|
92
|
+
if (pin === SAVED_PIN) {
|
|
93
|
+
const saved = await Native.unlock(id, biometricPrompt, biometricCancel, useSavedCertificate);
|
|
94
|
+
await beforeCard?.(saved && trimDer(saved));
|
|
95
|
+
}
|
|
84
96
|
const certificate = trimDer(await Native.connect(id, pin === SAVED_PIN ? null : pin, savePin, sheet, biometricPrompt, biometricCancel));
|
|
85
97
|
step('ready');
|
|
86
98
|
let last: Promise<unknown> = Promise.resolve();
|
|
@@ -111,5 +123,15 @@ export const cancel = () => Native.close(session, null, null);
|
|
|
111
123
|
/** Whether a PIN is saved, without showing any UI. */
|
|
112
124
|
export const pinStatus = (): Promise<PinStatus> => Native.pinStatus();
|
|
113
125
|
|
|
114
|
-
/** Deletes the saved PIN. */
|
|
126
|
+
/** Deletes the saved PIN and its certificate. */
|
|
115
127
|
export const forgetPin = (): Promise<void> => Native.forgetPin();
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The DER certificate of the card whose PIN is saved, without any UI: known before the card is on the phone, e.g. to
|
|
131
|
+
* prepare what to sign before a `SAVED_PIN` session with `useSavedCertificate`. Null when no PIN is saved, or none was
|
|
132
|
+
* saved with it yet: a `SAVED_PIN` session that reads the certificate from the card saves it.
|
|
133
|
+
*/
|
|
134
|
+
export async function savedCertificate(): Promise<Bytes | null> {
|
|
135
|
+
const certificate = await Native.savedCertificate();
|
|
136
|
+
return certificate ? trimDer(certificate) : null;
|
|
137
|
+
}
|
package/src/native.ts
CHANGED
|
@@ -10,10 +10,16 @@ export type NativeStep = 'detected' | 'pin';
|
|
|
10
10
|
export type NativeMessages = { prompt: string; detected: string; pin: string; ready: string; signing: string };
|
|
11
11
|
|
|
12
12
|
type CieSignModule = {
|
|
13
|
+
/**
|
|
14
|
+
* Shows the biometric prompt and unlocks the saved PIN for session `id`, which `connect` with a null `pin` then sends to
|
|
15
|
+
* the card. Returns the certificate saved with the PIN when `useSavedCertificate` and it is of the same card: that
|
|
16
|
+
* session's `connect` returns it instead of reading it. Null otherwise.
|
|
17
|
+
*/
|
|
18
|
+
unlock(id: number, biometricPrompt: string, biometricCancel: string, useSavedCertificate: boolean): Promise<Uint8Array | null>;
|
|
13
19
|
/**
|
|
14
20
|
* Waits for the card, verifies the PIN and returns its certificate; NFC session `id` stays open.
|
|
15
|
-
* A null `pin`
|
|
16
|
-
*
|
|
21
|
+
* A null `pin` is the one `unlock` unlocked for `id`; `save` saves `pin` once the card accepts it, after the biometric
|
|
22
|
+
* prompt where the secure hardware requires one (Android).
|
|
17
23
|
*/
|
|
18
24
|
connect(id: number, pin: string | null, save: boolean, messages: NativeMessages, biometricPrompt: string, biometricCancel: string): Promise<Uint8Array>;
|
|
19
25
|
/** Signs a DigestInfo in session `id`, as many times as needed until it closes. */
|
|
@@ -25,6 +31,8 @@ type CieSignModule = {
|
|
|
25
31
|
close(id: number, error: string | null, done: string | null): void;
|
|
26
32
|
pinStatus(): Promise<PinStatus>;
|
|
27
33
|
forgetPin(): Promise<void>;
|
|
34
|
+
/** The certificate file saved with the PIN, without any UI; null when none is saved. */
|
|
35
|
+
savedCertificate(): Promise<Uint8Array | null>;
|
|
28
36
|
/** `onStep` fires as session `id` reaches a step only the native side sees. */
|
|
29
37
|
addListener(event: 'onStep', listener: (event: { id: number; step: NativeStep }) => void): { remove(): void };
|
|
30
38
|
};
|