@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,62 @@
1
+ package com.necessarysdk.reactnative
2
+
3
+ /**
4
+ * Whether a key is in AndroidKeyStore, when its absence matters: a key is
5
+ * made only where there is none, and generating one replaces whatever the
6
+ * alias holds.
7
+ *
8
+ * `KeyStore.containsAlias` cannot say "absent" on its own. It answers false
9
+ * also when the keystore cannot be reached: on Android 7 to 11
10
+ * `android.security.KeyStore.contains` returns false on a binder failure, and
11
+ * on Android 12 and later `AndroidKeyStoreSpi.getKeyMetadata` returns null on
12
+ * every keystore error, a lost connection included. Deleting an alias that
13
+ * never holds a key does say whether the keystore answered: one that answers
14
+ * reports it not found, which `deleteEntry` takes as done, while one that
15
+ * cannot be reached makes `deleteEntry` throw (`KeyStore.delete` returns false
16
+ * and `engineDeleteEntry` throws on 7 to 11; keystore2's `deleteKey` throws
17
+ * `SYSTEM_ERROR` on 12 and later). So a "no" is believed only when the
18
+ * keystore answered the probe and then said "no" again.
19
+ *
20
+ * The limit: two outages, one over each lookup and neither over the probe
21
+ * between them, still read as absent. A pure decision, so a JVM test holds it.
22
+ */
23
+ internal object KeyPresence {
24
+ /** The two keystore calls the answer rests on. */
25
+ interface Keystore {
26
+ /** `containsAlias` for the key: false also when the keystore could not be reached. */
27
+ fun containsKey(): Boolean
28
+
29
+ /**
30
+ * `deleteEntry` of an alias that never holds a key: returns when the
31
+ * keystore answered, throws when it could not be reached.
32
+ */
33
+ fun deleteProbe()
34
+ }
35
+
36
+ enum class Answer {
37
+ /** The keystore holds the key. */
38
+ PRESENT,
39
+
40
+ /** The keystore answered, and holds no key: one may be made. */
41
+ ABSENT,
42
+
43
+ /** The keystore could not be reached: no key may be made over one it may hold. */
44
+ UNAVAILABLE,
45
+ }
46
+
47
+ class Found(val answer: Answer, val cause: Exception? = null)
48
+
49
+ fun of(keystore: Keystore): Found {
50
+ if (keystore.containsKey()) {
51
+ return Found(Answer.PRESENT)
52
+ }
53
+ try {
54
+ keystore.deleteProbe()
55
+ } catch (error: Exception) {
56
+ return Found(Answer.UNAVAILABLE, error)
57
+ }
58
+ // The keystore answers now: its "no" may have been an outage that has
59
+ // passed, so it is asked again.
60
+ return Found(if (keystore.containsKey()) Answer.PRESENT else Answer.ABSENT)
61
+ }
62
+ }
@@ -0,0 +1,94 @@
1
+ package com.necessarysdk.reactnative
2
+
3
+ import com.facebook.react.bridge.Promise
4
+ import com.facebook.react.bridge.ReactApplicationContext
5
+ import com.facebook.react.bridge.ReadableArray
6
+
7
+ // The SDK's five native modules, each a thin door to the one native side its runtime shares
8
+ // (NecessaryNative). The specs they implement are generated from src/native/*.ts.
9
+
10
+ class InstallationKeyModule internal constructor(
11
+ context: ReactApplicationContext,
12
+ private val native: NecessaryNative,
13
+ ) : NativeNecessaryInstallationKeySpec(context) {
14
+ override fun getName() = NAME
15
+ override fun getOrCreateKey(promise: Promise) = native.getOrCreateKey(promise)
16
+ override fun sign(payload: String, promise: Promise) = native.sign(payload, promise)
17
+ override fun deleteKey(promise: Promise) = native.deleteKey(promise)
18
+ override fun reinstallHintIdentifier(promise: Promise) = native.reinstallHintIdentifier(promise)
19
+
20
+ companion object {
21
+ const val NAME = "NecessaryInstallationKey"
22
+ }
23
+ }
24
+
25
+ class IntegrityModule internal constructor(
26
+ context: ReactApplicationContext,
27
+ private val native: NecessaryNative,
28
+ ) : NativeNecessaryIntegritySpec(context) {
29
+ override fun getName() = NAME
30
+ override fun collect(
31
+ requiredProbes: ReadableArray,
32
+ challengeNonce: String,
33
+ optionalProbes: ReadableArray,
34
+ enabledMeasurements: ReadableArray,
35
+ promise: Promise,
36
+ ) = native.collect(requiredProbes, challengeNonce, optionalProbes, enabledMeasurements, promise)
37
+ override fun requestLocationPermission(promise: Promise) {
38
+ native.requestLocationPermission(promise)
39
+ }
40
+
41
+ companion object {
42
+ const val NAME = "NecessaryIntegrity"
43
+ }
44
+ }
45
+
46
+ class StepUpKeyModule internal constructor(
47
+ context: ReactApplicationContext,
48
+ private val native: NecessaryNative,
49
+ ) : NativeNecessaryStepUpKeySpec(context) {
50
+ override fun getName() = NAME
51
+ override fun getOrCreateKey(factor: String, mode: String, windowSeconds: Double, promise: Promise) =
52
+ native.getOrCreateStepUpKey(factor, mode, windowSeconds.toInt(), promise)
53
+ override fun sign(payload: String, reason: String, mode: String, promise: Promise) {
54
+ native.signStepUp(payload, reason, mode, promise)
55
+ }
56
+ override fun deleteKey(promise: Promise) = native.deleteStepUpKey(promise)
57
+
58
+ companion object {
59
+ const val NAME = "NecessaryStepUpKey"
60
+ }
61
+ }
62
+
63
+ class SecureScreenModule internal constructor(
64
+ context: ReactApplicationContext,
65
+ private val native: NecessaryNative,
66
+ ) : NativeNecessarySecureScreenSpec(context) {
67
+ override fun getName() = NAME
68
+ override fun protect(on: Boolean, promise: Promise) {
69
+ native.protect(on, promise)
70
+ }
71
+
72
+ companion object {
73
+ const val NAME = "NecessarySecureScreen"
74
+ }
75
+ }
76
+
77
+ class StorageModule internal constructor(
78
+ context: ReactApplicationContext,
79
+ private val native: NecessaryNative,
80
+ ) : NativeNecessaryStorageSpec(context) {
81
+ override fun getName() = NAME
82
+ override fun read(key: String, promise: Promise) = native.read(key, promise)
83
+ override fun write(key: String, value: String, promise: Promise) = native.write(key, value, promise)
84
+ override fun remove(key: String, promise: Promise) = native.remove(key, promise)
85
+ override fun randomBytes(count: Double, promise: Promise) = native.randomBytes(count.toInt(), promise)
86
+ override fun invalidate() {
87
+ native.invalidate()
88
+ super.invalidate()
89
+ }
90
+
91
+ companion object {
92
+ const val NAME = "NecessaryStorage"
93
+ }
94
+ }
@@ -0,0 +1,323 @@
1
+ package com.necessarysdk.reactnative
2
+
3
+ import android.Manifest
4
+ import android.app.Activity
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.content.pm.PackageManager
8
+ import android.os.Build
9
+ import android.os.Handler
10
+ import android.os.Looper
11
+ import android.provider.Settings
12
+ import android.util.Base64
13
+ import android.view.WindowManager
14
+ import com.facebook.react.bridge.ActivityEventListener
15
+ import com.facebook.react.bridge.Arguments
16
+ import com.facebook.react.bridge.LifecycleEventListener
17
+ import com.facebook.react.bridge.Promise
18
+ import com.facebook.react.bridge.ReactApplicationContext
19
+ import com.facebook.react.bridge.ReadableArray
20
+ import com.facebook.react.modules.core.PermissionAwareActivity
21
+ import com.facebook.react.modules.core.PermissionListener
22
+ import java.lang.ref.WeakReference
23
+ import java.security.SecureRandom
24
+ import java.util.concurrent.ExecutorService
25
+ import java.util.concurrent.Executors
26
+
27
+ /**
28
+ * The SDK's native side on Android, one per React Native runtime and shared by its five modules:
29
+ * the installation key, the step-up key, the collector, screen protection and the secure storage.
30
+ * The keys and the collector run on one worker thread; the step-up prompt, the location prompt and
31
+ * the window are the current activity's, on the main thread.
32
+ */
33
+ internal class NecessaryNative(private val reactContext: ReactApplicationContext) :
34
+ ActivityEventListener,
35
+ LifecycleEventListener {
36
+
37
+ companion object {
38
+ private const val LOCATION_REQUEST = 4721
39
+ private val random = SecureRandom()
40
+ }
41
+
42
+ private val context: Context = reactContext.applicationContext
43
+ private val worker: ExecutorService = Executors.newSingleThreadExecutor()
44
+ private val storageWorker: ExecutorService = Executors.newSingleThreadExecutor()
45
+ private val mainHandler = Handler(Looper.getMainLooper())
46
+ private val installationKeys = InstallationKeyManager(context)
47
+ private val integrityProbes = IntegrityProbeManager(context) { reactContext.currentActivity }
48
+ private val stepUpKeys = StepUpKeyManager(context)
49
+ private val secureStore = SecureStore(context)
50
+ private val screenGuard = ScreenGuard()
51
+ // The app's own request for the location permission, answered when the person chooses; the
52
+ // collector itself never asks.
53
+ private var pendingLocation: Promise? = null
54
+ // The activity the window state belongs to: a new one, while the screen is protected, is
55
+ // protected too.
56
+ private var attached: WeakReference<Activity>? = null
57
+
58
+ init {
59
+ reactContext.addActivityEventListener(this)
60
+ reactContext.addLifecycleEventListener(this)
61
+ }
62
+
63
+ // MARK: the installation key
64
+
65
+ fun getOrCreateKey(promise: Promise) = runOffMainThread(promise) { installationKeys.getOrCreateKey() }
66
+
67
+ fun sign(payload: String, promise: Promise) = runOffMainThread(promise) { installationKeys.signPayload(payload) }
68
+
69
+ fun deleteKey(promise: Promise) = runOffMainThread(promise) { installationKeys.deleteKey() }
70
+
71
+ // The reinstall hint's source, ANDROID_ID: scoped to the phone, the user and the app's signing
72
+ // key. The JavaScript side hashes it before it leaves the phone; empty when the platform gives
73
+ // none.
74
+ fun reinstallHintIdentifier(promise: Promise) = runOffMainThread(promise) {
75
+ Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: ""
76
+ }
77
+
78
+ // MARK: the collector
79
+
80
+ fun collect(
81
+ requiredProbes: ReadableArray,
82
+ challengeNonce: String,
83
+ optionalProbes: ReadableArray,
84
+ enabledMeasurements: ReadableArray,
85
+ promise: Promise,
86
+ ) {
87
+ val required = strings(requiredProbes)
88
+ val optional = strings(optionalProbes)
89
+ val enabled = strings(enabledMeasurements).toSet()
90
+ runOffMainThread(promise) { integrityProbes.collect(required, challengeNonce, optional, enabled) }
91
+ }
92
+
93
+ // For the location and mock_location measurements: asks the person once, from a button on the
94
+ // app's own screen.
95
+ fun requestLocationPermission(promise: Promise) = mainHandler.post {
96
+ val activity = reactContext.currentActivity
97
+ if (locationState() == "precise" || pendingLocation != null || activity !is PermissionAwareActivity) {
98
+ promise.resolve(locationState())
99
+ return@post
100
+ }
101
+ pendingLocation = promise
102
+ activity.requestPermissions(
103
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION),
104
+ LOCATION_REQUEST,
105
+ PermissionListener { requestCode, _, _ ->
106
+ if (requestCode != LOCATION_REQUEST) return@PermissionListener false
107
+ pendingLocation?.resolve(locationState())
108
+ pendingLocation = null
109
+ true
110
+ },
111
+ )
112
+ }
113
+
114
+ private fun locationState(): String = when {
115
+ context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) ==
116
+ PackageManager.PERMISSION_GRANTED -> "precise"
117
+ context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) ==
118
+ PackageManager.PERMISSION_GRANTED -> "approximate"
119
+ else -> "none"
120
+ }
121
+
122
+ // MARK: the step-up key
123
+
124
+ fun getOrCreateStepUpKey(factor: String, mode: String, windowSeconds: Int, promise: Promise) =
125
+ runOffMainThread(promise) { stepUpKeys.getOrCreateKey(factor, mode, windowSeconds) }
126
+
127
+ // On the main thread: it shows the system prompt, and the keystore signs inside the prompt's
128
+ // callback.
129
+ fun signStepUp(payload: String, reason: String, mode: String, promise: Promise) = mainHandler.post {
130
+ val activity = reactContext.currentActivity
131
+ if (activity == null) {
132
+ promise.reject("NO_ACTIVITY", "The step-up prompt needs a visible activity.")
133
+ return@post
134
+ }
135
+ try {
136
+ stepUpKeys.sign(activity, payload, reason, mode) { outcome ->
137
+ outcome.fold(
138
+ { signature -> promise.resolve(signature) },
139
+ { error -> reportStepUpFailure(promise, error) },
140
+ )
141
+ }
142
+ } catch (error: Throwable) {
143
+ // Whatever failed before the prompt, said with its causes.
144
+ reportStepUpFailure(promise, error)
145
+ }
146
+ }
147
+
148
+ fun deleteStepUpKey(promise: Promise) = runOffMainThread(promise) { stepUpKeys.deleteKey() }
149
+
150
+ // MARK: screen protection
151
+
152
+ // On the main thread: it changes the window.
153
+ fun protect(on: Boolean, promise: Promise) = mainHandler.post {
154
+ val activity = reactContext.currentActivity
155
+ if (on) {
156
+ if (activity == null) {
157
+ promise.reject("NO_ACTIVITY", "Screen protection needs a visible activity.")
158
+ return@post
159
+ }
160
+ attached = WeakReference(activity)
161
+ screenGuard.acquire(windowState(activity))?.let { applyScreen(activity, it) }
162
+ } else {
163
+ // Counted even without a window, so a release is never lost.
164
+ val change = screenGuard.release()
165
+ if (activity != null && change != null) applyScreen(activity, change)
166
+ }
167
+ promise.resolve(Arguments.makeNativeMap(screenState(activity)))
168
+ }
169
+
170
+ /**
171
+ * While a secret is typed: the window cannot be captured in screenshots, recordings or casts
172
+ * (FLAG_SECURE); other apps' windows are not drawn over it (Android 12 and later,
173
+ * HIDE_OVERLAY_WINDOWS, a normal permission); and a touch that passed through another app's
174
+ * window is dropped. ScreenGuard decides when, and what the window gets back. The phone's own
175
+ * screen-lock prompt is the system's window and needs none of this from the app.
176
+ */
177
+ private fun applyScreen(activity: Activity, change: ScreenGuard.Change) {
178
+ val window = activity.window
179
+ if (change.secure) {
180
+ window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
181
+ } else {
182
+ window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
183
+ }
184
+ if (overlaysHideable()) window.setHideOverlayWindows(change.hideOverlays)
185
+ window.decorView.filterTouchesWhenObscured = change.dropsObscuredTouches
186
+ }
187
+
188
+ private fun overlaysHideable(): Boolean =
189
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
190
+ context.checkSelfPermission("android.permission.HIDE_OVERLAY_WINDOWS") ==
191
+ PackageManager.PERMISSION_GRANTED
192
+
193
+ private fun windowState(activity: Activity) = ScreenGuard.WindowState(
194
+ secure = (activity.window.attributes.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0,
195
+ dropsObscuredTouches = activity.window.decorView.filterTouchesWhenObscured,
196
+ )
197
+
198
+ /** What the window has now, and whether the SDK holds it. */
199
+ private fun screenState(activity: Activity?): Map<String, Any> = mapOf(
200
+ "held" to screenGuard.held,
201
+ "secure" to (activity?.let { windowState(it).secure } ?: false),
202
+ "overlays_hidden" to (screenGuard.held && overlaysHideable()),
203
+ "obscured_touches_dropped" to (activity?.let { windowState(it).dropsObscuredTouches } ?: false),
204
+ )
205
+
206
+ // MARK: the secure storage and the random source
207
+
208
+ fun read(name: String, promise: Promise) = runOn(storageWorker, promise) { secureStore.read(name) }
209
+
210
+ fun write(name: String, value: String, promise: Promise) =
211
+ runOn(storageWorker, promise) { secureStore.write(name, value) }
212
+
213
+ fun remove(name: String, promise: Promise) = runOn(storageWorker, promise) { secureStore.remove(name) }
214
+
215
+ fun randomBytes(count: Int, promise: Promise) {
216
+ if (count < 0 || count > 4096) {
217
+ promise.reject("INVALID_ARGUMENT", "count must be from 0 to 4096.")
218
+ return
219
+ }
220
+ val bytes = ByteArray(count)
221
+ random.nextBytes(bytes)
222
+ promise.resolve(Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP))
223
+ }
224
+
225
+ // MARK: the activity
226
+
227
+ // Delivers the confirm-credential result the step-up key uses before Android 11.
228
+ override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
229
+ stepUpKeys.onActivityResult(requestCode, resultCode)
230
+ }
231
+
232
+ override fun onNewIntent(intent: Intent) {}
233
+
234
+ // A new activity while the screen is protected -- the old one recreated -- is protected too.
235
+ override fun onHostResume() {
236
+ val activity = reactContext.currentActivity ?: return
237
+ if (attached?.get() === activity) return
238
+ attached = WeakReference(activity)
239
+ screenGuard.reattach(windowState(activity))?.let { applyScreen(activity, it) }
240
+ }
241
+
242
+ override fun onHostPause() {}
243
+
244
+ // Gone for good: a step-up waiting for the confirm-credential result would never hear it, and
245
+ // would hold every later approval at STEPUP_BUSY. A recreated activity still receives the
246
+ // result, so a step-up waiting for it keeps waiting.
247
+ override fun onHostDestroy() {
248
+ val activity = attached?.get() ?: reactContext.currentActivity
249
+ if (activity?.isChangingConfigurations == true) return
250
+ stepUpKeys.cancelPendingConfirmation()
251
+ }
252
+
253
+ /** The runtime is going away: its threads with it. */
254
+ fun invalidate() {
255
+ reactContext.removeActivityEventListener(this)
256
+ reactContext.removeLifecycleEventListener(this)
257
+ stepUpKeys.cancelPendingConfirmation()
258
+ worker.shutdown()
259
+ storageWorker.shutdown()
260
+ }
261
+
262
+ // MARK: answers
263
+
264
+ private fun strings(array: ReadableArray): List<String> =
265
+ (0 until array.size()).mapNotNull { index -> array.getString(index) }
266
+
267
+ private fun runOffMainThread(promise: Promise, operation: () -> Any?) = runOn(worker, promise, operation)
268
+
269
+ private fun runOn(executor: ExecutorService, promise: Promise, operation: () -> Any?) {
270
+ try {
271
+ executor.execute {
272
+ try {
273
+ promise.resolve(answer(operation()))
274
+ } catch (error: InstallationKeyFailure) {
275
+ promise.reject(error.errorCode, error.report(::keystoreDetail))
276
+ } catch (error: Throwable) {
277
+ // Name the exception and its cause: a release build's log output is suppressed on
278
+ // some phones, so the message shown in the app is the only diagnostic there is.
279
+ val detail = buildString {
280
+ append("The Android native operation failed: ")
281
+ append(Causes.chain(error, ::keystoreDetail))
282
+ append(" at ")
283
+ append(error.stackTrace.take(6).joinToString(" < ") {
284
+ "${it.className.substringAfterLast('.')}.${it.methodName}:${it.lineNumber}"
285
+ })
286
+ }
287
+ promise.reject("NATIVE_INTEGRITY_ERROR", detail)
288
+ }
289
+ }
290
+ } catch (error: java.util.concurrent.RejectedExecutionException) {
291
+ promise.reject("DETACHED", "The SDK's native side has been shut down with its runtime.")
292
+ }
293
+ }
294
+
295
+ @Suppress("UNCHECKED_CAST")
296
+ private fun answer(value: Any?): Any? = when (value) {
297
+ is Map<*, *> -> Arguments.makeNativeMap(value as Map<String, Any?>)
298
+ is List<*> -> Arguments.makeNativeArray(value)
299
+ else -> value
300
+ }
301
+
302
+ /** A step-up signature that failed: its code, and its words with every cause beneath them. */
303
+ private fun reportStepUpFailure(promise: Promise, error: Throwable) {
304
+ if (error is InstallationKeyFailure) {
305
+ promise.reject(error.errorCode, error.report(::keystoreDetail))
306
+ } else {
307
+ promise.reject(
308
+ "NATIVE_STEPUP_ERROR",
309
+ "The step-up signature failed: " + Causes.chain(error, ::keystoreDetail),
310
+ )
311
+ }
312
+ }
313
+
314
+ /**
315
+ * AndroidKeyStore's own error code, which its exception's message may leave out: public from
316
+ * Android 13 (API 33), with whether the keystore deems the failure transient.
317
+ */
318
+ private fun keystoreDetail(error: Throwable): String? {
319
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return null
320
+ val keystore = error as? android.security.KeyStoreException ?: return null
321
+ return "code ${keystore.numericErrorCode}" + if (keystore.isTransientFailure) ", transient" else ""
322
+ }
323
+ }
@@ -0,0 +1,38 @@
1
+ package com.necessarysdk.reactnative
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import java.util.WeakHashMap
9
+
10
+ /** The SDK's five native modules, sharing one native side per React Native runtime. */
11
+ class NecessaryPackage : BaseReactPackage() {
12
+ private val natives = WeakHashMap<ReactApplicationContext, NecessaryNative>()
13
+
14
+ private fun nativeFor(context: ReactApplicationContext): NecessaryNative =
15
+ synchronized(natives) { natives.getOrPut(context) { NecessaryNative(context) } }
16
+
17
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
18
+ when (name) {
19
+ InstallationKeyModule.NAME -> InstallationKeyModule(reactContext, nativeFor(reactContext))
20
+ IntegrityModule.NAME -> IntegrityModule(reactContext, nativeFor(reactContext))
21
+ StepUpKeyModule.NAME -> StepUpKeyModule(reactContext, nativeFor(reactContext))
22
+ SecureScreenModule.NAME -> SecureScreenModule(reactContext, nativeFor(reactContext))
23
+ StorageModule.NAME -> StorageModule(reactContext, nativeFor(reactContext))
24
+ else -> null
25
+ }
26
+
27
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
28
+ listOf(
29
+ InstallationKeyModule.NAME to InstallationKeyModule::class.java.name,
30
+ IntegrityModule.NAME to IntegrityModule::class.java.name,
31
+ StepUpKeyModule.NAME to StepUpKeyModule::class.java.name,
32
+ SecureScreenModule.NAME to SecureScreenModule::class.java.name,
33
+ StorageModule.NAME to StorageModule::class.java.name,
34
+ ).associate { (name, className) ->
35
+ name to ReactModuleInfo(name, className, false, false, false, true)
36
+ }
37
+ }
38
+ }