@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.
- package/CHANGELOG.md +40 -0
- package/LICENSE +201 -0
- package/NOTICE +2 -0
- package/NecessaryReactNative.podspec +23 -0
- package/README.md +429 -2
- package/android/build.gradle +83 -0
- package/android/src/main/AndroidManifest.xml +41 -0
- package/android/src/main/cpp/CMakeLists.txt +7 -0
- package/android/src/main/cpp/code_integrity.cpp +484 -0
- package/android/src/main/java/com/necessarysdk/reactnative/Causes.kt +49 -0
- package/android/src/main/java/com/necessarysdk/reactnative/InstallationKeyManager.kt +388 -0
- package/android/src/main/java/com/necessarysdk/reactnative/IntegrityProbeManager.kt +1157 -0
- package/android/src/main/java/com/necessarysdk/reactnative/KeyPresence.kt +62 -0
- package/android/src/main/java/com/necessarysdk/reactnative/NecessaryModules.kt +94 -0
- package/android/src/main/java/com/necessarysdk/reactnative/NecessaryNative.kt +338 -0
- package/android/src/main/java/com/necessarysdk/reactnative/NecessaryPackage.kt +38 -0
- package/android/src/main/java/com/necessarysdk/reactnative/ProbeReads.kt +333 -0
- package/android/src/main/java/com/necessarysdk/reactnative/ScreenGuard.kt +56 -0
- package/android/src/main/java/com/necessarysdk/reactnative/SecureStore.kt +127 -0
- package/android/src/main/java/com/necessarysdk/reactnative/StepUpKeyEntry.kt +57 -0
- package/android/src/main/java/com/necessarysdk/reactnative/StepUpKeyManager.kt +767 -0
- package/contract/report-1/android-8-ios-5/RULES.md +149 -0
- package/contract/report-1/android-8-ios-5/report.schema.json +2753 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-complete.json +205 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-incomplete.json +205 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-invalid.json +205 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-location-one-fix.json +228 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-location-two-fixes.json +228 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-refused.json +208 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-complete.json +106 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-incomplete.json +105 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-invalid.json +107 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-partial.json +110 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-refused.json +109 -0
- package/doc/integration_guide.md +771 -0
- package/doc/measurements.md +774 -0
- package/ios/InstallationKeyManager.swift +694 -0
- package/ios/IntegrityProbeManager.swift +959 -0
- package/ios/NecessaryModules.h +34 -0
- package/ios/NecessaryModules.mm +208 -0
- package/ios/NecessaryNative.swift +327 -0
- package/ios/PrivacyInfo.xcprivacy +14 -0
- package/ios/ProbeReads.swift +370 -0
- package/lib/client.d.ts +105 -0
- package/lib/client.js +176 -0
- package/lib/collector.d.ts +18 -0
- package/lib/collector.js +49 -0
- package/lib/deviceTrust.d.ts +253 -0
- package/lib/deviceTrust.js +699 -0
- package/lib/errors.d.ts +43 -0
- package/lib/errors.js +56 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +34 -0
- package/lib/installationKey.d.ts +62 -0
- package/lib/installationKey.js +185 -0
- package/lib/installationStore.d.ts +49 -0
- package/lib/installationStore.js +127 -0
- package/lib/native/NativeNecessaryInstallationKey.d.ts +10 -0
- package/lib/native/NativeNecessaryInstallationKey.js +4 -0
- package/lib/native/NativeNecessaryIntegrity.d.ts +8 -0
- package/lib/native/NativeNecessaryIntegrity.js +4 -0
- package/lib/native/NativeNecessarySecureScreen.d.ts +7 -0
- package/lib/native/NativeNecessarySecureScreen.js +4 -0
- package/lib/native/NativeNecessaryStepUpKey.d.ts +9 -0
- package/lib/native/NativeNecessaryStepUpKey.js +4 -0
- package/lib/native/NativeNecessaryStorage.d.ts +14 -0
- package/lib/native/NativeNecessaryStorage.js +4 -0
- package/lib/protocol.d.ts +24 -0
- package/lib/protocol.js +50 -0
- package/lib/reinstallHint.d.ts +23 -0
- package/lib/reinstallHint.js +65 -0
- package/lib/report.d.ts +62 -0
- package/lib/report.js +134 -0
- package/lib/requestProof.d.ts +116 -0
- package/lib/requestProof.js +220 -0
- package/lib/secureScreen.d.ts +15 -0
- package/lib/secureScreen.js +32 -0
- package/lib/stepUpKey.d.ts +105 -0
- package/lib/stepUpKey.js +187 -0
- package/lib/transport.d.ts +39 -0
- package/lib/transport.js +117 -0
- package/lib/util/answers.d.ts +6 -0
- package/lib/util/answers.js +16 -0
- package/lib/util/bytes.d.ts +18 -0
- package/lib/util/bytes.js +207 -0
- package/lib/util/native.d.ts +20 -0
- package/lib/util/native.js +49 -0
- package/lib/util/release.d.ts +2 -0
- package/lib/util/release.js +7 -0
- package/lib/util/target.d.ts +18 -0
- package/lib/util/target.js +128 -0
- package/package.json +81 -4
- package/src/client.ts +221 -0
- package/src/collector.ts +64 -0
- package/src/deviceTrust.ts +851 -0
- package/src/errors.ts +71 -0
- package/src/index.ts +19 -0
- package/src/installationKey.ts +223 -0
- package/src/installationStore.ts +143 -0
- package/src/native/NativeNecessaryInstallationKey.ts +12 -0
- package/src/native/NativeNecessaryIntegrity.ts +15 -0
- package/src/native/NativeNecessarySecureScreen.ts +9 -0
- package/src/native/NativeNecessaryStepUpKey.ts +11 -0
- package/src/native/NativeNecessaryStorage.ts +16 -0
- package/src/protocol.ts +52 -0
- package/src/reinstallHint.ts +69 -0
- package/src/report.ts +168 -0
- package/src/requestProof.ts +295 -0
- package/src/secureScreen.ts +29 -0
- package/src/stepUpKey.ts +238 -0
- package/src/transport.ts +145 -0
- package/src/util/answers.ts +15 -0
- package/src/util/bytes.ts +194 -0
- package/src/util/native.ts +56 -0
- package/src/util/release.ts +6 -0
- package/src/util/target.ts +132 -0
- 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
|
+
}
|