@necessarysdk/react-native 0.0.0-stage → 0.1.5

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.
Files changed (114) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +2 -0
  4. package/NecessaryReactNative.podspec +23 -0
  5. package/README.md +429 -2
  6. package/android/build.gradle +83 -0
  7. package/android/src/main/AndroidManifest.xml +41 -0
  8. package/android/src/main/cpp/CMakeLists.txt +7 -0
  9. package/android/src/main/cpp/code_integrity.cpp +484 -0
  10. package/android/src/main/java/com/necessarysdk/reactnative/Causes.kt +49 -0
  11. package/android/src/main/java/com/necessarysdk/reactnative/InstallationKeyManager.kt +388 -0
  12. package/android/src/main/java/com/necessarysdk/reactnative/IntegrityProbeManager.kt +1152 -0
  13. package/android/src/main/java/com/necessarysdk/reactnative/KeyPresence.kt +62 -0
  14. package/android/src/main/java/com/necessarysdk/reactnative/NecessaryModules.kt +94 -0
  15. package/android/src/main/java/com/necessarysdk/reactnative/NecessaryNative.kt +323 -0
  16. package/android/src/main/java/com/necessarysdk/reactnative/NecessaryPackage.kt +38 -0
  17. package/android/src/main/java/com/necessarysdk/reactnative/ProbeReads.kt +314 -0
  18. package/android/src/main/java/com/necessarysdk/reactnative/ScreenGuard.kt +56 -0
  19. package/android/src/main/java/com/necessarysdk/reactnative/SecureStore.kt +127 -0
  20. package/android/src/main/java/com/necessarysdk/reactnative/StepUpKeyEntry.kt +57 -0
  21. package/android/src/main/java/com/necessarysdk/reactnative/StepUpKeyManager.kt +762 -0
  22. package/contract/report-1/android-8-ios-5/RULES.md +149 -0
  23. package/contract/report-1/android-8-ios-5/report.schema.json +2753 -0
  24. package/contract/report-1/android-8-ios-5/samples/android-8-complete.json +205 -0
  25. package/contract/report-1/android-8-ios-5/samples/android-8-incomplete.json +205 -0
  26. package/contract/report-1/android-8-ios-5/samples/android-8-invalid.json +205 -0
  27. package/contract/report-1/android-8-ios-5/samples/android-8-location-one-fix.json +228 -0
  28. package/contract/report-1/android-8-ios-5/samples/android-8-location-two-fixes.json +228 -0
  29. package/contract/report-1/android-8-ios-5/samples/android-8-refused.json +208 -0
  30. package/contract/report-1/android-8-ios-5/samples/ios-5-complete.json +106 -0
  31. package/contract/report-1/android-8-ios-5/samples/ios-5-incomplete.json +105 -0
  32. package/contract/report-1/android-8-ios-5/samples/ios-5-invalid.json +107 -0
  33. package/contract/report-1/android-8-ios-5/samples/ios-5-partial.json +110 -0
  34. package/contract/report-1/android-8-ios-5/samples/ios-5-refused.json +109 -0
  35. package/doc/integration_guide.md +771 -0
  36. package/doc/measurements.md +774 -0
  37. package/ios/InstallationKeyManager.swift +694 -0
  38. package/ios/IntegrityProbeManager.swift +960 -0
  39. package/ios/NecessaryModules.h +34 -0
  40. package/ios/NecessaryModules.mm +208 -0
  41. package/ios/NecessaryNative.swift +327 -0
  42. package/ios/PrivacyInfo.xcprivacy +14 -0
  43. package/ios/ProbeReads.swift +353 -0
  44. package/lib/client.d.ts +105 -0
  45. package/lib/client.js +176 -0
  46. package/lib/collector.d.ts +18 -0
  47. package/lib/collector.js +49 -0
  48. package/lib/deviceTrust.d.ts +253 -0
  49. package/lib/deviceTrust.js +690 -0
  50. package/lib/errors.d.ts +43 -0
  51. package/lib/errors.js +56 -0
  52. package/lib/index.d.ts +18 -0
  53. package/lib/index.js +34 -0
  54. package/lib/installationKey.d.ts +62 -0
  55. package/lib/installationKey.js +185 -0
  56. package/lib/installationStore.d.ts +49 -0
  57. package/lib/installationStore.js +127 -0
  58. package/lib/native/NativeNecessaryInstallationKey.d.ts +10 -0
  59. package/lib/native/NativeNecessaryInstallationKey.js +4 -0
  60. package/lib/native/NativeNecessaryIntegrity.d.ts +8 -0
  61. package/lib/native/NativeNecessaryIntegrity.js +4 -0
  62. package/lib/native/NativeNecessarySecureScreen.d.ts +7 -0
  63. package/lib/native/NativeNecessarySecureScreen.js +4 -0
  64. package/lib/native/NativeNecessaryStepUpKey.d.ts +9 -0
  65. package/lib/native/NativeNecessaryStepUpKey.js +4 -0
  66. package/lib/native/NativeNecessaryStorage.d.ts +14 -0
  67. package/lib/native/NativeNecessaryStorage.js +4 -0
  68. package/lib/protocol.d.ts +24 -0
  69. package/lib/protocol.js +50 -0
  70. package/lib/reinstallHint.d.ts +23 -0
  71. package/lib/reinstallHint.js +65 -0
  72. package/lib/report.d.ts +62 -0
  73. package/lib/report.js +140 -0
  74. package/lib/requestProof.d.ts +116 -0
  75. package/lib/requestProof.js +220 -0
  76. package/lib/secureScreen.d.ts +15 -0
  77. package/lib/secureScreen.js +32 -0
  78. package/lib/stepUpKey.d.ts +105 -0
  79. package/lib/stepUpKey.js +187 -0
  80. package/lib/transport.d.ts +39 -0
  81. package/lib/transport.js +117 -0
  82. package/lib/util/bytes.d.ts +18 -0
  83. package/lib/util/bytes.js +207 -0
  84. package/lib/util/native.d.ts +20 -0
  85. package/lib/util/native.js +49 -0
  86. package/lib/util/release.d.ts +2 -0
  87. package/lib/util/release.js +7 -0
  88. package/lib/util/target.d.ts +18 -0
  89. package/lib/util/target.js +128 -0
  90. package/package.json +81 -4
  91. package/src/client.ts +221 -0
  92. package/src/collector.ts +64 -0
  93. package/src/deviceTrust.ts +842 -0
  94. package/src/errors.ts +71 -0
  95. package/src/index.ts +19 -0
  96. package/src/installationKey.ts +223 -0
  97. package/src/installationStore.ts +143 -0
  98. package/src/native/NativeNecessaryInstallationKey.ts +12 -0
  99. package/src/native/NativeNecessaryIntegrity.ts +15 -0
  100. package/src/native/NativeNecessarySecureScreen.ts +9 -0
  101. package/src/native/NativeNecessaryStepUpKey.ts +11 -0
  102. package/src/native/NativeNecessaryStorage.ts +16 -0
  103. package/src/protocol.ts +52 -0
  104. package/src/reinstallHint.ts +69 -0
  105. package/src/report.ts +174 -0
  106. package/src/requestProof.ts +295 -0
  107. package/src/secureScreen.ts +29 -0
  108. package/src/stepUpKey.ts +238 -0
  109. package/src/transport.ts +145 -0
  110. package/src/util/bytes.ts +194 -0
  111. package/src/util/native.ts +56 -0
  112. package/src/util/release.ts +6 -0
  113. package/src/util/target.ts +132 -0
  114. package/vectors/client_server.json +264 -0
@@ -0,0 +1,388 @@
1
+ package com.necessarysdk.reactnative
2
+
3
+ import android.content.Context
4
+ import android.content.pm.PackageManager
5
+ import android.os.Build
6
+ import android.security.keystore.KeyGenParameterSpec
7
+ import android.security.keystore.KeyInfo
8
+ import android.security.keystore.KeyProperties
9
+ import android.util.Base64
10
+ import java.math.BigInteger
11
+ import java.security.GeneralSecurityException
12
+ import java.security.KeyFactory
13
+ import java.security.KeyPairGenerator
14
+ import java.security.KeyStore
15
+ import java.security.ProviderException
16
+ import java.security.Signature
17
+ import java.security.interfaces.ECPublicKey
18
+ import java.security.spec.ECGenParameterSpec
19
+
20
+ class InstallationKeyFailure(
21
+ val errorCode: String,
22
+ override val message: String,
23
+ cause: Throwable? = null,
24
+ ) : Exception(message, cause) {
25
+ /**
26
+ * What the app is told: this failure's words, then every cause beneath it
27
+ * (Causes). [detail] adds AndroidKeyStore's error code where its API has one.
28
+ */
29
+ fun report(detail: (Throwable) -> String? = { null }): String =
30
+ Causes.withCauses(message, cause, detail)
31
+ }
32
+
33
+ /**
34
+ * Owns one non-exportable P-256 signing key in AndroidKeyStore.
35
+ *
36
+ * The private key object is only a handle. Android performs signing through the
37
+ * keystore provider, and this class returns only the public EC coordinates and
38
+ * DER-encoded ECDSA signatures to the JavaScript side.
39
+ */
40
+ class InstallationKeyManager(private val context: Context) {
41
+ companion object {
42
+ private const val ANDROID_KEY_STORE = "AndroidKeyStore"
43
+ private const val CURVE_NAME = "secp256r1"
44
+ private const val KEY_METADATA_VERSION = 2
45
+ private const val MAX_PAYLOAD_BYTES = 65536
46
+
47
+ // Making and deleting the key run one at a time in the process,
48
+ // whichever runtime asks: two first starts at once would otherwise each
49
+ // find no key, and the second would generate over the first's.
50
+ private val keyLock = Any()
51
+ }
52
+
53
+ private val keyAlias: String =
54
+ "${context.packageName}.device_recognition.installation_key.v2"
55
+
56
+ // Never holds a key: deleting it says whether the keystore answers
57
+ // (KeyPresence).
58
+ private val probeAlias: String = "$keyAlias.probe"
59
+
60
+ fun getOrCreateKey(): Map<String, Any> = synchronized(keyLock) { loadOrMakeKey() }
61
+
62
+ private fun loadOrMakeKey(): Map<String, Any> {
63
+ requireSupportedAndroid()
64
+ val keyStore = loadKeyStore()
65
+ var created = false
66
+
67
+ // A key is made only where the keystore, answering, holds none: a
68
+ // lookup that failed reads as "no key", and generating would replace
69
+ // the installation's key with a new one.
70
+ val found = presence(keyStore)
71
+ when (found.answer) {
72
+ KeyPresence.Answer.PRESENT -> {}
73
+ KeyPresence.Answer.ABSENT -> {
74
+ generateKeyPair(keyStore)
75
+ created = true
76
+ }
77
+ KeyPresence.Answer.UNAVAILABLE -> throw keyUnavailable(found.cause)
78
+ }
79
+
80
+ var entry = privateKeyEntry(keyStore)
81
+ if (!isExpectedP256Key(entry)) {
82
+ // An interrupted development build may have left a different key
83
+ // under the alias. Replace it and say that a new key exists, so the
84
+ // JavaScript side also rotates the installation UUID.
85
+ keyStore.deleteEntry(keyAlias)
86
+ generateKeyPair(keyStore)
87
+ created = true
88
+ entry = privateKeyEntry(keyStore)
89
+ }
90
+
91
+ val publicKey = entry.certificate.publicKey as? ECPublicKey
92
+ ?: throw InstallationKeyFailure(
93
+ "INVALID_PUBLIC_KEY",
94
+ "AndroidKeyStore did not return an EC public key.",
95
+ )
96
+ val x = unsignedFixed(publicKey.w.affineX, 32)
97
+ val y = unsignedFixed(publicKey.w.affineY, 32)
98
+ val security = describeSecurity(entry)
99
+
100
+ return mapOf(
101
+ "version" to KEY_METADATA_VERSION,
102
+ "key_alias" to keyAlias,
103
+ "algorithm" to "ES256",
104
+ "signature_format" to "asn1_der",
105
+ "provider" to ANDROID_KEY_STORE,
106
+ "security_level" to security.first,
107
+ "hardware_backed" to security.second,
108
+ "private_key_exportable" to false,
109
+ "created" to created,
110
+ "public_key" to mapOf(
111
+ "kty" to "EC",
112
+ "crv" to "P-256",
113
+ "alg" to "ES256",
114
+ "x" to base64Url(x),
115
+ "y" to base64Url(y),
116
+ ),
117
+ )
118
+ }
119
+
120
+ fun signPayload(payloadBase64Url: String): String {
121
+ requireSupportedAndroid()
122
+ val payload = decodeBase64Url(payloadBase64Url)
123
+ if (payload.size > MAX_PAYLOAD_BYTES) {
124
+ throw InstallationKeyFailure(
125
+ "PAYLOAD_TOO_LARGE",
126
+ "The challenge payload is larger than $MAX_PAYLOAD_BYTES bytes.",
127
+ )
128
+ }
129
+
130
+ val keyStore = loadKeyStore()
131
+ val found = presence(keyStore)
132
+ when (found.answer) {
133
+ KeyPresence.Answer.PRESENT -> {}
134
+ KeyPresence.Answer.ABSENT -> throw InstallationKeyFailure(
135
+ "KEY_NOT_FOUND",
136
+ "The Android installation key is missing. Restart the app to create a fresh installation identity.",
137
+ )
138
+ KeyPresence.Answer.UNAVAILABLE -> throw keyUnavailable(found.cause)
139
+ }
140
+ val entry = privateKeyEntry(keyStore)
141
+
142
+ try {
143
+ val signer = Signature.getInstance("SHA256withECDSA")
144
+ signer.initSign(entry.privateKey)
145
+ signer.update(payload)
146
+ // The JCA SHA256withECDSA signature format is ASN.1 DER.
147
+ return base64Url(signer.sign())
148
+ } catch (error: GeneralSecurityException) {
149
+ throw InstallationKeyFailure(
150
+ "SIGNING_FAILED",
151
+ "AndroidKeyStore could not sign the installation challenge.",
152
+ error,
153
+ )
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Deletes the key, whatever a lookup says: a lookup that failed reads as
159
+ * "no key", and skipping the deletion then would leave the key in place
160
+ * after a reset. Whether one was there is for the answer only.
161
+ */
162
+ fun deleteKey(): Boolean = synchronized(keyLock) {
163
+ requireSupportedAndroid()
164
+ val keyStore = loadKeyStore()
165
+ try {
166
+ val existed = keyStore.containsAlias(keyAlias)
167
+ keyStore.deleteEntry(keyAlias)
168
+ if (keyStore.containsAlias(keyAlias)) {
169
+ throw InstallationKeyFailure(
170
+ "KEY_DELETE_FAILED",
171
+ "AndroidKeyStore did not delete the installation key: it is still present.",
172
+ )
173
+ }
174
+ existed
175
+ } catch (error: GeneralSecurityException) {
176
+ throw InstallationKeyFailure(
177
+ "KEY_DELETE_FAILED",
178
+ "AndroidKeyStore could not delete the installation key.",
179
+ error,
180
+ )
181
+ }
182
+ }
183
+
184
+ private fun presence(keyStore: KeyStore): KeyPresence.Found =
185
+ KeyPresence.of(object : KeyPresence.Keystore {
186
+ override fun containsKey() = keyStore.containsAlias(keyAlias)
187
+ override fun deleteProbe() = keyStore.deleteEntry(probeAlias)
188
+ })
189
+
190
+ private fun keyUnavailable(cause: Throwable?) = InstallationKeyFailure(
191
+ "KEY_UNAVAILABLE",
192
+ "AndroidKeyStore could not be reached for the installation key; the key was kept, never " +
193
+ "replaced. Try again; if this persists, reset() starts a new installation.",
194
+ cause,
195
+ )
196
+
197
+ private fun requireSupportedAndroid() {
198
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
199
+ throw InstallationKeyFailure(
200
+ "UNSUPPORTED_ANDROID_VERSION",
201
+ "This prototype requires Android 6.0 (API 23) or newer.",
202
+ )
203
+ }
204
+ }
205
+
206
+ private fun loadKeyStore(): KeyStore {
207
+ try {
208
+ return KeyStore.getInstance(ANDROID_KEY_STORE).apply { load(null) }
209
+ } catch (error: GeneralSecurityException) {
210
+ throw InstallationKeyFailure(
211
+ "KEYSTORE_UNAVAILABLE",
212
+ "AndroidKeyStore is unavailable.",
213
+ error,
214
+ )
215
+ }
216
+ }
217
+
218
+ private fun generateKeyPair(keyStore: KeyStore) {
219
+ val canTryStrongBox = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
220
+ context.packageManager.hasSystemFeature(
221
+ PackageManager.FEATURE_STRONGBOX_KEYSTORE,
222
+ )
223
+
224
+ if (canTryStrongBox) {
225
+ try {
226
+ generateKeyPair(preferStrongBox = true)
227
+ return
228
+ } catch (error: Exception) {
229
+ // Some devices advertise StrongBox but cannot satisfy a
230
+ // particular algorithm/digest combination. Remove any partial
231
+ // alias and retry with the regular AndroidKeyStore provider.
232
+ try {
233
+ if (keyStore.containsAlias(keyAlias)) {
234
+ keyStore.deleteEntry(keyAlias)
235
+ }
236
+ } catch (_: Exception) {
237
+ // The normal generation attempt below will produce the
238
+ // actionable error if the keystore is genuinely unusable.
239
+ }
240
+ }
241
+ }
242
+
243
+ generateKeyPair(preferStrongBox = false)
244
+ }
245
+
246
+ private fun generateKeyPair(preferStrongBox: Boolean) {
247
+ try {
248
+ val generator = KeyPairGenerator.getInstance(
249
+ KeyProperties.KEY_ALGORITHM_EC,
250
+ ANDROID_KEY_STORE,
251
+ )
252
+ val builder = KeyGenParameterSpec.Builder(
253
+ keyAlias,
254
+ KeyProperties.PURPOSE_SIGN,
255
+ )
256
+ .setAlgorithmParameterSpec(ECGenParameterSpec(CURVE_NAME))
257
+ .setDigests(KeyProperties.DIGEST_SHA256)
258
+ .setUserAuthenticationRequired(false)
259
+
260
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && preferStrongBox) {
261
+ builder.setIsStrongBoxBacked(true)
262
+ }
263
+
264
+ generator.initialize(builder.build())
265
+ generator.generateKeyPair()
266
+ } catch (error: GeneralSecurityException) {
267
+ throw InstallationKeyFailure(
268
+ "KEY_GENERATION_FAILED",
269
+ if (preferStrongBox) {
270
+ "StrongBox could not create the P-256 installation key."
271
+ } else {
272
+ "AndroidKeyStore could not create the P-256 installation key."
273
+ },
274
+ error,
275
+ )
276
+ } catch (error: ProviderException) {
277
+ throw InstallationKeyFailure(
278
+ "KEY_GENERATION_FAILED",
279
+ if (preferStrongBox) {
280
+ "StrongBox could not create the P-256 installation key."
281
+ } else {
282
+ "AndroidKeyStore could not create the P-256 installation key."
283
+ },
284
+ error,
285
+ )
286
+ }
287
+ }
288
+
289
+ private fun privateKeyEntry(keyStore: KeyStore): KeyStore.PrivateKeyEntry {
290
+ try {
291
+ // getEntry answers null when its own lookup fails (the keystore
292
+ // just reported the key), and an UnsupportedOperationException
293
+ // when the alias holds no key entry -- on Android 7 to 11 also when
294
+ // that lookup fails: both may pass, so the key is kept.
295
+ val entry = keyStore.getEntry(keyAlias, null)
296
+ ?: throw keyUnavailable(null)
297
+ return entry as? KeyStore.PrivateKeyEntry
298
+ ?: throw InstallationKeyFailure(
299
+ "KEY_NOT_FOUND",
300
+ "The Android installation key entry has the wrong type.",
301
+ )
302
+ } catch (error: UnsupportedOperationException) {
303
+ throw keyUnavailable(error)
304
+ } catch (error: GeneralSecurityException) {
305
+ throw InstallationKeyFailure(
306
+ "KEY_LOOKUP_FAILED",
307
+ "AndroidKeyStore could not load the installation key.",
308
+ error,
309
+ )
310
+ }
311
+ }
312
+
313
+ private fun isExpectedP256Key(entry: KeyStore.PrivateKeyEntry): Boolean {
314
+ val publicKey = entry.certificate.publicKey as? ECPublicKey ?: return false
315
+ return publicKey.params.curve.field.fieldSize == 256
316
+ }
317
+
318
+ private fun describeSecurity(
319
+ entry: KeyStore.PrivateKeyEntry,
320
+ ): Pair<String, Boolean> {
321
+ return try {
322
+ val keyFactory = KeyFactory.getInstance(
323
+ entry.privateKey.algorithm,
324
+ ANDROID_KEY_STORE,
325
+ )
326
+ val keyInfo = keyFactory.getKeySpec(entry.privateKey, KeyInfo::class.java)
327
+
328
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
329
+ when (keyInfo.securityLevel) {
330
+ KeyProperties.SECURITY_LEVEL_STRONGBOX -> "strongbox" to true
331
+ KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT ->
332
+ "trusted_execution_environment" to true
333
+ KeyProperties.SECURITY_LEVEL_SOFTWARE -> "software" to false
334
+ else -> "unknown" to false
335
+ }
336
+ } else {
337
+ if (keyInfo.isInsideSecureHardware) {
338
+ "secure_hardware" to true
339
+ } else {
340
+ "software" to false
341
+ }
342
+ }
343
+ } catch (_: Exception) {
344
+ "unknown" to false
345
+ }
346
+ }
347
+
348
+ private fun unsignedFixed(value: BigInteger, size: Int): ByteArray {
349
+ var raw = value.toByteArray()
350
+ if (raw.size == size + 1 && raw[0] == 0.toByte()) {
351
+ raw = raw.copyOfRange(1, raw.size)
352
+ }
353
+ if (raw.size > size) {
354
+ throw InstallationKeyFailure(
355
+ "INVALID_PUBLIC_KEY",
356
+ "The EC coordinate is larger than P-256 permits.",
357
+ )
358
+ }
359
+ return ByteArray(size).also { output ->
360
+ System.arraycopy(raw, 0, output, size - raw.size, raw.size)
361
+ }
362
+ }
363
+
364
+ private fun base64Url(value: ByteArray): String {
365
+ return Base64.encodeToString(
366
+ value,
367
+ Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING,
368
+ )
369
+ }
370
+
371
+ private fun decodeBase64Url(value: String): ByteArray {
372
+ if (value.isBlank()) {
373
+ throw InstallationKeyFailure(
374
+ "INVALID_PAYLOAD",
375
+ "The challenge payload is empty.",
376
+ )
377
+ }
378
+ return try {
379
+ Base64.decode(value, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
380
+ } catch (error: IllegalArgumentException) {
381
+ throw InstallationKeyFailure(
382
+ "INVALID_PAYLOAD",
383
+ "The challenge payload is not valid base64url.",
384
+ error,
385
+ )
386
+ }
387
+ }
388
+ }