@necessarysdk/react-native 0.0.0-stage → 0.1.6

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 (117) hide show
  1. package/CHANGELOG.md +40 -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 +1157 -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 +338 -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 +333 -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 +767 -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 +959 -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 +370 -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 +699 -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 +134 -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/answers.d.ts +6 -0
  83. package/lib/util/answers.js +16 -0
  84. package/lib/util/bytes.d.ts +18 -0
  85. package/lib/util/bytes.js +207 -0
  86. package/lib/util/native.d.ts +20 -0
  87. package/lib/util/native.js +49 -0
  88. package/lib/util/release.d.ts +2 -0
  89. package/lib/util/release.js +7 -0
  90. package/lib/util/target.d.ts +18 -0
  91. package/lib/util/target.js +128 -0
  92. package/package.json +81 -4
  93. package/src/client.ts +221 -0
  94. package/src/collector.ts +64 -0
  95. package/src/deviceTrust.ts +851 -0
  96. package/src/errors.ts +71 -0
  97. package/src/index.ts +19 -0
  98. package/src/installationKey.ts +223 -0
  99. package/src/installationStore.ts +143 -0
  100. package/src/native/NativeNecessaryInstallationKey.ts +12 -0
  101. package/src/native/NativeNecessaryIntegrity.ts +15 -0
  102. package/src/native/NativeNecessarySecureScreen.ts +9 -0
  103. package/src/native/NativeNecessaryStepUpKey.ts +11 -0
  104. package/src/native/NativeNecessaryStorage.ts +16 -0
  105. package/src/protocol.ts +52 -0
  106. package/src/reinstallHint.ts +69 -0
  107. package/src/report.ts +168 -0
  108. package/src/requestProof.ts +295 -0
  109. package/src/secureScreen.ts +29 -0
  110. package/src/stepUpKey.ts +238 -0
  111. package/src/transport.ts +145 -0
  112. package/src/util/answers.ts +15 -0
  113. package/src/util/bytes.ts +194 -0
  114. package/src/util/native.ts +56 -0
  115. package/src/util/release.ts +6 -0
  116. package/src/util/target.ts +132 -0
  117. 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,338 @@
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
+ import java.util.concurrent.atomic.AtomicReference
27
+
28
+ /**
29
+ * The SDK's native side on Android, one per React Native runtime and shared by its five modules:
30
+ * the installation key, the step-up key, the collector, screen protection and the secure storage.
31
+ * The keys and the collector run on one worker thread; the step-up prompt, the location prompt and
32
+ * the window are the current activity's, on the main thread.
33
+ */
34
+ internal class NecessaryNative(private val reactContext: ReactApplicationContext) :
35
+ ActivityEventListener,
36
+ LifecycleEventListener {
37
+
38
+ companion object {
39
+ private const val LOCATION_REQUEST = 4721
40
+ private val random = SecureRandom()
41
+ }
42
+
43
+ private val context: Context = reactContext.applicationContext
44
+ private val worker: ExecutorService = Executors.newSingleThreadExecutor()
45
+ private val storageWorker: ExecutorService = Executors.newSingleThreadExecutor()
46
+ private val mainHandler = Handler(Looper.getMainLooper())
47
+ private val installationKeys = InstallationKeyManager(context)
48
+ private val integrityProbes = IntegrityProbeManager(context) { reactContext.currentActivity }
49
+ private val stepUpKeys = StepUpKeyManager(context)
50
+ private val secureStore = SecureStore(context)
51
+ private val screenGuard = ScreenGuard()
52
+ // The app's own request for the location permission, answered when the person chooses, or when
53
+ // the activity that asked goes; the collector itself never asks. Whichever comes first takes
54
+ // it: invalidate() runs off the main thread.
55
+ private val pendingLocation = AtomicReference<Promise?>(null)
56
+ // The activity the window state belongs to: a new one, while the screen is protected, is
57
+ // protected too.
58
+ private var attached: WeakReference<Activity>? = null
59
+
60
+ init {
61
+ reactContext.addActivityEventListener(this)
62
+ reactContext.addLifecycleEventListener(this)
63
+ }
64
+
65
+ // MARK: the installation key
66
+
67
+ fun getOrCreateKey(promise: Promise) = runOffMainThread(promise) { installationKeys.getOrCreateKey() }
68
+
69
+ fun sign(payload: String, promise: Promise) = runOffMainThread(promise) { installationKeys.signPayload(payload) }
70
+
71
+ fun deleteKey(promise: Promise) = runOffMainThread(promise) { installationKeys.deleteKey() }
72
+
73
+ // The reinstall hint's source, ANDROID_ID: scoped to the phone, the user and the app's signing
74
+ // key. The JavaScript side hashes it before it leaves the phone; empty when the platform gives
75
+ // none.
76
+ fun reinstallHintIdentifier(promise: Promise) = runOffMainThread(promise) {
77
+ Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: ""
78
+ }
79
+
80
+ // MARK: the collector
81
+
82
+ fun collect(
83
+ requiredProbes: ReadableArray,
84
+ challengeNonce: String,
85
+ optionalProbes: ReadableArray,
86
+ enabledMeasurements: ReadableArray,
87
+ promise: Promise,
88
+ ) {
89
+ val required = strings(requiredProbes)
90
+ val optional = strings(optionalProbes)
91
+ val enabled = strings(enabledMeasurements).toSet()
92
+ runOffMainThread(promise) { integrityProbes.collect(required, challengeNonce, optional, enabled) }
93
+ }
94
+
95
+ // For the location and mock_location measurements: asks the person once, from a button on the
96
+ // app's own screen.
97
+ fun requestLocationPermission(promise: Promise) = mainHandler.post {
98
+ val activity = reactContext.currentActivity
99
+ if (
100
+ locationState() == "precise" ||
101
+ activity !is PermissionAwareActivity ||
102
+ !pendingLocation.compareAndSet(null, promise)
103
+ ) {
104
+ promise.resolve(locationState())
105
+ return@post
106
+ }
107
+ activity.requestPermissions(
108
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION),
109
+ LOCATION_REQUEST,
110
+ PermissionListener { requestCode, _, _ ->
111
+ if (requestCode != LOCATION_REQUEST) return@PermissionListener false
112
+ answerLocationRequest()
113
+ true
114
+ },
115
+ )
116
+ }
117
+
118
+ /** The request still waiting, if one is, hears what the app holds now. */
119
+ private fun answerLocationRequest() {
120
+ pendingLocation.getAndSet(null)?.resolve(locationState())
121
+ }
122
+
123
+ private fun locationState(): String = when {
124
+ context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) ==
125
+ PackageManager.PERMISSION_GRANTED -> "precise"
126
+ context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) ==
127
+ PackageManager.PERMISSION_GRANTED -> "approximate"
128
+ else -> "none"
129
+ }
130
+
131
+ // MARK: the step-up key
132
+
133
+ fun getOrCreateStepUpKey(factor: String, mode: String, windowSeconds: Int, promise: Promise) =
134
+ runOffMainThread(promise) { stepUpKeys.getOrCreateKey(factor, mode, windowSeconds) }
135
+
136
+ // On the main thread: it shows the system prompt, and the keystore signs inside the prompt's
137
+ // callback.
138
+ fun signStepUp(payload: String, reason: String, mode: String, promise: Promise) = mainHandler.post {
139
+ val activity = reactContext.currentActivity
140
+ if (activity == null) {
141
+ promise.reject("NO_ACTIVITY", "The step-up prompt needs a visible activity.")
142
+ return@post
143
+ }
144
+ try {
145
+ stepUpKeys.sign(activity, payload, reason, mode) { outcome ->
146
+ outcome.fold(
147
+ { signature -> promise.resolve(signature) },
148
+ { error -> reportStepUpFailure(promise, error) },
149
+ )
150
+ }
151
+ } catch (error: Throwable) {
152
+ // Whatever failed before the prompt, said with its causes.
153
+ reportStepUpFailure(promise, error)
154
+ }
155
+ }
156
+
157
+ fun deleteStepUpKey(promise: Promise) = runOffMainThread(promise) { stepUpKeys.deleteKey() }
158
+
159
+ // MARK: screen protection
160
+
161
+ // On the main thread: it changes the window.
162
+ fun protect(on: Boolean, promise: Promise) = mainHandler.post {
163
+ val activity = reactContext.currentActivity
164
+ if (on) {
165
+ if (activity == null) {
166
+ promise.reject("NO_ACTIVITY", "Screen protection needs a visible activity.")
167
+ return@post
168
+ }
169
+ attached = WeakReference(activity)
170
+ screenGuard.acquire(windowState(activity))?.let { applyScreen(activity, it) }
171
+ } else {
172
+ // Counted even without a window, so a release is never lost.
173
+ val change = screenGuard.release()
174
+ if (activity != null && change != null) applyScreen(activity, change)
175
+ }
176
+ promise.resolve(Arguments.makeNativeMap(screenState(activity)))
177
+ }
178
+
179
+ /**
180
+ * While a secret is typed: the window cannot be captured in screenshots, recordings or casts
181
+ * (FLAG_SECURE); other apps' windows are not drawn over it (Android 12 and later,
182
+ * HIDE_OVERLAY_WINDOWS, a normal permission); and a touch that passed through another app's
183
+ * window is dropped. ScreenGuard decides when, and what the window gets back. The phone's own
184
+ * screen-lock prompt is the system's window and needs none of this from the app.
185
+ */
186
+ private fun applyScreen(activity: Activity, change: ScreenGuard.Change) {
187
+ val window = activity.window
188
+ if (change.secure) {
189
+ window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
190
+ } else {
191
+ window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
192
+ }
193
+ if (overlaysHideable()) window.setHideOverlayWindows(change.hideOverlays)
194
+ window.decorView.filterTouchesWhenObscured = change.dropsObscuredTouches
195
+ }
196
+
197
+ private fun overlaysHideable(): Boolean =
198
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
199
+ context.checkSelfPermission("android.permission.HIDE_OVERLAY_WINDOWS") ==
200
+ PackageManager.PERMISSION_GRANTED
201
+
202
+ private fun windowState(activity: Activity) = ScreenGuard.WindowState(
203
+ secure = (activity.window.attributes.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0,
204
+ dropsObscuredTouches = activity.window.decorView.filterTouchesWhenObscured,
205
+ )
206
+
207
+ /** What the window has now, and whether the SDK holds it. */
208
+ private fun screenState(activity: Activity?): Map<String, Any> = mapOf(
209
+ "held" to screenGuard.held,
210
+ "secure" to (activity?.let { windowState(it).secure } ?: false),
211
+ "overlays_hidden" to (screenGuard.held && overlaysHideable()),
212
+ "obscured_touches_dropped" to (activity?.let { windowState(it).dropsObscuredTouches } ?: false),
213
+ )
214
+
215
+ // MARK: the secure storage and the random source
216
+
217
+ fun read(name: String, promise: Promise) = runOn(storageWorker, promise) { secureStore.read(name) }
218
+
219
+ fun write(name: String, value: String, promise: Promise) =
220
+ runOn(storageWorker, promise) { secureStore.write(name, value) }
221
+
222
+ fun remove(name: String, promise: Promise) = runOn(storageWorker, promise) { secureStore.remove(name) }
223
+
224
+ fun randomBytes(count: Int, promise: Promise) {
225
+ if (count < 0 || count > 4096) {
226
+ promise.reject("INVALID_ARGUMENT", "count must be from 0 to 4096.")
227
+ return
228
+ }
229
+ val bytes = ByteArray(count)
230
+ random.nextBytes(bytes)
231
+ promise.resolve(Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP))
232
+ }
233
+
234
+ // MARK: the activity
235
+
236
+ // Delivers the confirm-credential result the step-up key uses before Android 11.
237
+ override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
238
+ stepUpKeys.onActivityResult(requestCode, resultCode)
239
+ }
240
+
241
+ override fun onNewIntent(intent: Intent) {}
242
+
243
+ // A new activity while the screen is protected -- the old one recreated -- is protected too.
244
+ override fun onHostResume() {
245
+ val activity = reactContext.currentActivity ?: return
246
+ if (attached?.get() === activity) return
247
+ attached = WeakReference(activity)
248
+ screenGuard.reattach(windowState(activity))?.let { applyScreen(activity, it) }
249
+ }
250
+
251
+ override fun onHostPause() {}
252
+
253
+ // A location request ends with the activity that asked, recreated or not: React Native keeps
254
+ // the permission listener in that activity's delegate, and the next one has none, so the
255
+ // person's choice would never reach it.
256
+ //
257
+ // Gone for good: a step-up waiting for the confirm-credential result would never hear it, and
258
+ // would hold every later approval at STEPUP_BUSY. A recreated activity still receives the
259
+ // result, so a step-up waiting for it keeps waiting.
260
+ override fun onHostDestroy() {
261
+ answerLocationRequest()
262
+ val activity = attached?.get() ?: reactContext.currentActivity
263
+ if (activity?.isChangingConfigurations == true) return
264
+ stepUpKeys.cancelPendingConfirmation()
265
+ }
266
+
267
+ /** The runtime is going away: its threads with it. */
268
+ fun invalidate() {
269
+ reactContext.removeActivityEventListener(this)
270
+ reactContext.removeLifecycleEventListener(this)
271
+ stepUpKeys.cancelPendingConfirmation()
272
+ answerLocationRequest()
273
+ worker.shutdown()
274
+ storageWorker.shutdown()
275
+ }
276
+
277
+ // MARK: answers
278
+
279
+ private fun strings(array: ReadableArray): List<String> =
280
+ (0 until array.size()).mapNotNull { index -> array.getString(index) }
281
+
282
+ private fun runOffMainThread(promise: Promise, operation: () -> Any?) = runOn(worker, promise, operation)
283
+
284
+ private fun runOn(executor: ExecutorService, promise: Promise, operation: () -> Any?) {
285
+ try {
286
+ executor.execute {
287
+ try {
288
+ promise.resolve(answer(operation()))
289
+ } catch (error: InstallationKeyFailure) {
290
+ promise.reject(error.errorCode, error.report(::keystoreDetail))
291
+ } catch (error: Throwable) {
292
+ // Name the exception and its cause: a release build's log output is suppressed on
293
+ // some phones, so the message shown in the app is the only diagnostic there is.
294
+ val detail = buildString {
295
+ append("The Android native operation failed: ")
296
+ append(Causes.chain(error, ::keystoreDetail))
297
+ append(" at ")
298
+ append(error.stackTrace.take(6).joinToString(" < ") {
299
+ "${it.className.substringAfterLast('.')}.${it.methodName}:${it.lineNumber}"
300
+ })
301
+ }
302
+ promise.reject("NATIVE_INTEGRITY_ERROR", detail)
303
+ }
304
+ }
305
+ } catch (error: java.util.concurrent.RejectedExecutionException) {
306
+ promise.reject("DETACHED", "The SDK's native side has been shut down with its runtime.")
307
+ }
308
+ }
309
+
310
+ @Suppress("UNCHECKED_CAST")
311
+ private fun answer(value: Any?): Any? = when (value) {
312
+ is Map<*, *> -> Arguments.makeNativeMap(value as Map<String, Any?>)
313
+ is List<*> -> Arguments.makeNativeArray(value)
314
+ else -> value
315
+ }
316
+
317
+ /** A step-up signature that failed: its code, and its words with every cause beneath them. */
318
+ private fun reportStepUpFailure(promise: Promise, error: Throwable) {
319
+ if (error is InstallationKeyFailure) {
320
+ promise.reject(error.errorCode, error.report(::keystoreDetail))
321
+ } else {
322
+ promise.reject(
323
+ "NATIVE_STEPUP_ERROR",
324
+ "The step-up signature failed: " + Causes.chain(error, ::keystoreDetail),
325
+ )
326
+ }
327
+ }
328
+
329
+ /**
330
+ * AndroidKeyStore's own error code, which its exception's message may leave out: public from
331
+ * Android 13 (API 33), with whether the keystore deems the failure transient.
332
+ */
333
+ private fun keystoreDetail(error: Throwable): String? {
334
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return null
335
+ val keystore = error as? android.security.KeyStoreException ?: return null
336
+ return "code ${keystore.numericErrorCode}" + if (keystore.isTransientFailure) ", transient" else ""
337
+ }
338
+ }
@@ -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
+ }