@scalebun/react-native 1.13.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/android/build.gradle +8 -0
  2. package/android/src/androidTest/java/com/scalebun/rn/ota/ScaleBunOtaVerifierInstrumentedTest.kt +112 -0
  3. package/android/src/main/java/com/scalebun/rn/ota/OtaProtocol.kt +244 -0
  4. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaKeyRegistry.kt +100 -0
  5. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +115 -60
  6. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaReleaseVerifier.kt +129 -0
  7. package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +18 -0
  8. package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +4 -8
  9. package/dist/scalebun.full.js +127 -422
  10. package/dist/scalebun.slim.js +127 -422
  11. package/ios/Ota/OtaProtocol.swift +242 -0
  12. package/ios/Ota/OtaSlotManager.swift +26 -0
  13. package/ios/Ota/ScaleBunOtaBridge.mm +6 -3
  14. package/ios/Ota/ScaleBunOtaKeyRegistry.swift +147 -0
  15. package/ios/Ota/ScaleBunOtaModule.swift +125 -53
  16. package/ios/Ota/ScaleBunOtaReleaseVerifier.swift +115 -0
  17. package/ios/Ota/ScaleBunOtaVerifierTests.swift +92 -0
  18. package/lib/commonjs/core/config/schema.js +3 -12
  19. package/lib/commonjs/core/constants/version.js +1 -1
  20. package/lib/commonjs/features/network/NetworkFeature.js +39 -5
  21. package/lib/commonjs/features/network/thirdParty.js +90 -0
  22. package/lib/commonjs/features/ota/OtaOrchestrator.js +108 -60
  23. package/lib/commonjs/public/ScaleBunFacade.js +5 -41
  24. package/lib/module/core/config/schema.js +3 -12
  25. package/lib/module/core/constants/version.js +1 -1
  26. package/lib/module/features/network/NetworkFeature.js +38 -4
  27. package/lib/module/features/network/thirdParty.js +83 -0
  28. package/lib/module/features/ota/OtaOrchestrator.js +108 -60
  29. package/lib/module/public/ScaleBunFacade.js +5 -41
  30. package/lib/typescript/core/config/schema.d.ts +0 -3
  31. package/lib/typescript/core/constants/version.d.ts +1 -1
  32. package/lib/typescript/features/network/index.d.ts +18 -0
  33. package/lib/typescript/features/network/thirdParty.d.ts +65 -0
  34. package/lib/typescript/features/ota/OtaOrchestrator.d.ts +5 -8
  35. package/lib/typescript/features/ota/OtaTypes.d.ts +22 -0
  36. package/lib/typescript/public/ScaleBunFacade.d.ts +5 -11
  37. package/lib/typescript/specs/NativeScaleBunOta.d.ts +30 -20
  38. package/package.json +3 -13
  39. package/scalebun-react-native.podspec +4 -0
  40. package/src/core/config/schema.ts +3 -9
  41. package/src/core/constants/version.ts +1 -1
  42. package/src/features/network/NetworkFeature.ts +41 -4
  43. package/src/features/network/index.ts +18 -0
  44. package/src/features/network/thirdParty.ts +92 -0
  45. package/src/features/ota/OtaOrchestrator.ts +116 -70
  46. package/src/features/ota/OtaTypes.ts +23 -1
  47. package/src/public/ScaleBunFacade.ts +5 -53
  48. package/src/specs/NativeScaleBunOta.ts +32 -20
  49. package/lib/commonjs/features/ota/crypto/builtinVerifier.js +0 -248
  50. package/lib/commonjs/features/ota/crypto/loadEd25519.js +0 -40
  51. package/lib/commonjs/features/ota/crypto/loadSha512.js +0 -40
  52. package/lib/commonjs/features/ota/crypto/nativeVerifier.js +0 -121
  53. package/lib/commonjs/features/ota/signature.js +0 -175
  54. package/lib/module/features/ota/crypto/builtinVerifier.js +0 -240
  55. package/lib/module/features/ota/crypto/loadEd25519.js +0 -34
  56. package/lib/module/features/ota/crypto/loadSha512.js +0 -34
  57. package/lib/module/features/ota/crypto/nativeVerifier.js +0 -113
  58. package/lib/module/features/ota/signature.js +0 -169
  59. package/lib/typescript/features/ota/crypto/builtinVerifier.d.ts +0 -53
  60. package/lib/typescript/features/ota/crypto/loadEd25519.d.ts +0 -30
  61. package/lib/typescript/features/ota/crypto/loadSha512.d.ts +0 -15
  62. package/lib/typescript/features/ota/crypto/nativeVerifier.d.ts +0 -35
  63. package/lib/typescript/features/ota/signature.d.ts +0 -81
  64. package/src/features/ota/crypto/builtinVerifier.ts +0 -257
  65. package/src/features/ota/crypto/loadEd25519.ts +0 -41
  66. package/src/features/ota/crypto/loadSha512.ts +0 -35
  67. package/src/features/ota/crypto/nativeVerifier.ts +0 -117
  68. package/src/features/ota/signature.ts +0 -206
@@ -1,169 +0,0 @@
1
- import { logger } from '../../core/logger/internalLogger';
2
- import { getBuiltinVerifier } from './crypto/builtinVerifier';
3
- import { getNativeVerifier } from './crypto/nativeVerifier';
4
-
5
- /**
6
- * Bundle signature verification (OTA-03).
7
- *
8
- * WHAT WAS WRONG. `ota-signing-key.service.ts` documents the intended model
9
- * exactly: the CLI generates an ed25519 keypair locally, uploads only the public
10
- * half, and "the device verifies every bundle's signature before install and
11
- * rejects forgeries." Two of those three steps did not exist. The CLI had no
12
- * signing code, and this SDK received `bundle.signature` in the check response
13
- * and never read it. The only client-side check was SHA-256 against a hash
14
- * delivered in the same response as the URL — which defends against a corrupted
15
- * download and nothing else. On a platform whose entire purpose is remote code
16
- * delivery, that is the control that matters most.
17
- *
18
- * WHY IT IS SHAPED LIKE THIS. React Native has no built-in ed25519, so the
19
- * verification primitive has to come from somewhere. It is resolved in order:
20
- * a host-supplied verifier, else the built-in one assembled from the OPTIONAL
21
- * `@noble/ed25519` + `@noble/hashes` peers, else nothing — and the SDK's job is
22
- * to decide, unambiguously, what happens in that last case.
23
- *
24
- * The built-in path exists because requiring every adopter to hand-write a
25
- * verifier put the most security-critical operation in the product in code the
26
- * SDK could neither test nor audit, and made a routine dependency bump able to
27
- * silently stop all updates. See `crypto/builtinVerifier.ts`. Signing stays
28
- * opt-in, and apps that never adopt it carry no curve arithmetic.
29
- *
30
- * THE POLICY, which is the important part:
31
- *
32
- * - A bundle carrying a signature, with a configured public key and a
33
- * verifier: verified. Failure REJECTS the install. This is the goal state.
34
- * - A bundle carrying a signature, with a public key configured, but no
35
- * verifier available: REJECT. The app asked for signature enforcement by
36
- * configuring a key; silently installing unverified code because a helper is
37
- * missing would turn a security feature into a placebo.
38
- * - No public key configured: skip, and say so once. Signing is opt-in per app,
39
- * and an app that has not adopted it must keep working.
40
- *
41
- * The failure mode is the whole design: an app that opts in cannot be
42
- * accidentally downgraded to unverified installs.
43
- */
44
-
45
- /** Verifies a detached ed25519 signature. Supplied by the host app. */
46
-
47
- let warnedNotConfigured = false;
48
-
49
- /**
50
- * Decide whether a bundle may be staged.
51
- *
52
- * Returns a structured outcome rather than a boolean so the caller can emit a
53
- * precise telemetry reason — "we shipped unverified code because no key was
54
- * configured" and "we refused because the signature was forged" are very
55
- * different operational events and must not collapse into one.
56
- */
57
- export async function verifyBundleSignature(bundleSha256, signature, config) {
58
- const rawKey = config?.publicKey;
59
-
60
- // Normalize to a list. A single non-empty string is the pre-rotation config
61
- // shape and behaves exactly as before; an array pins several keys at once.
62
- const keys = (Array.isArray(rawKey) ? rawKey : rawKey ? [rawKey] : []).filter(k => typeof k === 'string' && k.trim().length > 0);
63
-
64
- // Signing not adopted by this app — nothing to enforce. Parity note: a
65
- // falsy single value (undefined, '') has always meant "not configured" and
66
- // still does.
67
- if (rawKey === undefined || rawKey === '' || rawKey === null) {
68
- if (!warnedNotConfigured) {
69
- warnedNotConfigured = true;
70
- logger.warn('[OTA] No signing public key configured — bundles are accepted on SHA-256 ' + 'integrity alone. Configure `ota.publicSigningKey` to enforce authenticity.');
71
- }
72
- return {
73
- ok: true,
74
- reason: 'not_configured'
75
- };
76
- }
77
-
78
- // The host PROVIDED a key config that resolves to zero usable keys — an
79
- // empty array, or an array of blank strings. That is a broken attempt to
80
- // enable signing, not an absence of one, and the two must not collapse:
81
- // treating `publicSigningKeys: []` as "not configured" would let a config
82
- // mistake silently downgrade an app to unverified installs. Fail closed.
83
- if (keys.length === 0) {
84
- logger.error('[OTA] Signing key config resolves to zero usable keys (empty array or blank ' + 'strings) — refusing to stage. Remove the config to disable signing, or pin ' + 'at least one real key.');
85
- return {
86
- ok: false,
87
- reason: 'no_keys'
88
- };
89
- }
90
-
91
- // The app opted in, so a bundle without a signature is a refusal, not a pass.
92
- if (!signature) {
93
- logger.error('[OTA] Bundle has no signature but a signing key is configured — refusing to stage.');
94
- return {
95
- ok: false,
96
- reason: 'missing_signature'
97
- };
98
- }
99
-
100
- // Resolution order, and the order matters:
101
- // 1. A host-supplied verifier ALWAYS wins. A team with its own crypto
102
- // policy must be able to override whatever the SDK would otherwise pick.
103
- // 2. Otherwise the NATIVE verifier (CryptoKit / Android 13+ platform
104
- // ed25519). Preferred over the JS one because it runs outside the
105
- // bundle it protects and needs no optional peers; on OS levels without
106
- // ed25519 it delegates to the builtin internally.
107
- // 3. Otherwise the built-in @noble verifier, if the optional peers are
108
- // installed. This is the path that removes ~100 lines of hand-written
109
- // crypto plumbing from every app that adopts signing.
110
- // 4. Otherwise reject, because a configured key is a request for
111
- // enforcement and quietly installing unverified code would turn a
112
- // security feature into a placebo.
113
- const verifier = typeof config?.verifier === 'function' ? config.verifier : getNativeVerifier() ?? getBuiltinVerifier();
114
- if (!verifier) {
115
- logger.error('[OTA] A signing key is configured but no signature verifier is available — ' + 'refusing to stage. Either install the optional peers ' + '`@noble/ed25519` and `@noble/hashes` (the SDK then verifies with no ' + 'further code), or provide your own `ota.verifySignature`.');
116
- return {
117
- ok: false,
118
- reason: 'no_verifier'
119
- };
120
- }
121
-
122
- // Try every pinned key; any single match accepts. The verifier contract is
123
- // unchanged (one key per call) so host-supplied verifiers keep working.
124
- //
125
- // Outcome labelling when nothing matched, and why it matters: if at least
126
- // one verifier call RAN TO COMPLETION and said no, the bundle failed
127
- // verification — 'invalid_signature'. Only when EVERY call threw is the
128
- // machinery itself suspect — 'verifier_threw'. Collapsing those (e.g. by
129
- // letting the last key's throw win) would point an investigation at the
130
- // host's verifier when the actual event was a forged bundle, or vice versa.
131
- let sawThrow = false;
132
- let sawCompletion = false;
133
- for (const key of keys) {
134
- try {
135
- const valid = await verifier({
136
- messageHex: bundleSha256,
137
- signature,
138
- publicKey: key
139
- });
140
- sawCompletion = true;
141
- if (valid) return {
142
- ok: true,
143
- reason: 'verified'
144
- };
145
- } catch (err) {
146
- // A throwing verifier is treated as a failed verification for this key,
147
- // never as a pass — but the remaining keys still get their chance.
148
- sawThrow = true;
149
- logger.error(`[OTA] Signature verifier threw: ${err?.message ?? err}`);
150
- }
151
- }
152
- if (sawThrow && !sawCompletion) {
153
- return {
154
- ok: false,
155
- reason: 'verifier_threw'
156
- };
157
- }
158
- logger.error(keys.length > 1 ? `[OTA] Bundle signature is INVALID under all ${keys.length} pinned keys — refusing to stage.` : '[OTA] Bundle signature is INVALID — refusing to stage.');
159
- return {
160
- ok: false,
161
- reason: 'invalid_signature'
162
- };
163
- }
164
-
165
- /** Test seam — resets the once-per-process "not configured" warning. */
166
- export function _resetSignatureWarnings() {
167
- warnedNotConfigured = false;
168
- }
169
- //# sourceMappingURL=signature.js.map
@@ -1,53 +0,0 @@
1
- /**
2
- * Built-in ed25519 verifier, assembled from OPTIONAL peer dependencies.
3
- *
4
- * WHAT THIS REPLACES. Pinning `ota.publicSigningKey` used to require the host
5
- * app to write its own `verifySignature` — roughly a hundred lines of hex and
6
- * base64 decoding wrapped around a crypto library. That put the single most
7
- * security-critical operation in the product, the one deciding whether remote
8
- * code is authentic before it executes, in code the SDK could neither test nor
9
- * audit. Three failure modes all landed on the app author:
10
- *
11
- * - A verifier that always returns `true` silently disables the feature, and
12
- * nothing can detect it.
13
- * - @noble's hash-provider property moved between major versions, so a
14
- * routine dependency bump made every update fail closed with no signal
15
- * pointing at the cause.
16
- * - Hand-written base64 decoders tend to omit base64url, so a signature
17
- * containing `-` or `_` is rejected as a forgery.
18
- *
19
- * All three are now the SDK's problem, which is where they belong. A host that
20
- * installs `@noble/ed25519` and `@noble/hashes` gets verification by pinning a
21
- * key and writing no code at all.
22
- *
23
- * WHY OPTIONAL AND NOT A HARD DEPENDENCY. Most apps never adopt bundle
24
- * signing, and they should not carry curve arithmetic they will not run. The
25
- * `verifySignature` hook remains supported and still WINS over this, for teams
26
- * with their own crypto policy or a native implementation to delegate to.
27
- *
28
- * WHAT THIS DOES NOT FIX. Verification still happens in JS, inside the very
29
- * bundle it protects. An attacker who lands one malicious bundle by other means
30
- * can neuter the check for every update after it. Closing that needs native
31
- * verification — CryptoKit on iOS (13.4+, already the deployment target) and
32
- * `Signature.getInstance("Ed25519")` on Android API 33+, with a fallback below.
33
- */
34
- import type { SignatureVerifier } from '../signature';
35
- /**
36
- * Decode a detached signature that may arrive hex- or base64-encoded.
37
- *
38
- * Tries the unambiguous case first: 128 hex characters is exactly 64 bytes and
39
- * cannot be anything else. Otherwise base64, then hex as a last resort. Only a
40
- * result of exactly 64 bytes is accepted, so a string that decodes under the
41
- * wrong scheme is rejected rather than fed to the curve code as garbage.
42
- */
43
- export declare function decodeSignature(sig: string): Uint8Array | null;
44
- /**
45
- * The built-in verifier, or null when the optional deps are not installed.
46
- * Resolution is cached, including the negative result — a missing dependency
47
- * does not become present at runtime, and retrying the require on every update
48
- * check would be pure overhead.
49
- */
50
- export declare function getBuiltinVerifier(): SignatureVerifier | null;
51
- /** Test seam — clears the cached resolution. */
52
- export declare function _resetBuiltinVerifier(): void;
53
- //# sourceMappingURL=builtinVerifier.d.ts.map
@@ -1,30 +0,0 @@
1
- /**
2
- * Isolated lazy loader for the OPTIONAL `@noble/ed25519` dep.
3
- *
4
- * WHY ITS OWN FILE: Metro's dependency collector emits a dependency map one
5
- * entry short for a module containing TWO OR MORE different-string inline
6
- * `require()` calls, so the require indices desync and `_dependencyMap[N]`
7
- * reads `undefined` ("Requiring unknown module 'undefined'"). Exactly ONE
8
- * inline require per module avoids the miscount. `loadSha512` is a sibling file
9
- * for the same reason — see `push/adapters/loadNotifee.ts`, which hit this first.
10
- *
11
- * The string must still be STATICALLY resolvable at bundle time; hosts that do
12
- * not install it stub it via `withScaleBun`.
13
- */
14
- /** The subset of the @noble/ed25519 surface this SDK uses. */
15
- export interface NobleEd25519 {
16
- verifyAsync?: (sig: Uint8Array, msg: Uint8Array, pub: Uint8Array) => Promise<boolean>;
17
- verify?: (sig: Uint8Array, msg: Uint8Array, pub: Uint8Array) => boolean;
18
- /** v3 shape: `ed.hashes.sha512` */
19
- hashes?: {
20
- sha512?: unknown;
21
- sha512Async?: unknown;
22
- };
23
- /** v2 shape: `ed.etc.sha512Sync` */
24
- etc?: {
25
- sha512Sync?: unknown;
26
- sha512Async?: unknown;
27
- };
28
- }
29
- export declare function loadEd25519(): NobleEd25519 | null;
30
- //# sourceMappingURL=loadEd25519.d.ts.map
@@ -1,15 +0,0 @@
1
- /**
2
- * Isolated lazy loader for the OPTIONAL `@noble/hashes` SHA-512.
3
- *
4
- * Separate file from `loadEd25519` because Metro miscounts a module holding two
5
- * different-string inline `require()` calls — see the comment there.
6
- *
7
- * ed25519 is defined in terms of SHA-512, and React Native has no native
8
- * SHA-512, so @noble/ed25519 cannot verify anything until a hash provider is
9
- * wired into it. Which property it expects differs by major version, so the
10
- * wiring lives in `builtinVerifier`, not here; this module only obtains the
11
- * function.
12
- */
13
- export type Sha512Fn = (msg: Uint8Array) => Uint8Array;
14
- export declare function loadSha512(): Sha512Fn | null;
15
- //# sourceMappingURL=loadSha512.d.ts.map
@@ -1,35 +0,0 @@
1
- /**
2
- * Native ed25519 verifier — platform crypto wrapped as a SignatureVerifier.
3
- *
4
- * WHY IT EXISTS. The built-in @noble verifier runs in JS, inside the very
5
- * bundle it protects: an attacker who lands one malicious bundle can neuter a
6
- * JS check for every update after it. Platform crypto (CryptoKit on iOS,
7
- * Android's conscrypt on API 33+) sits outside the bundle's reach — and using
8
- * it also drops the runtime dependency on the optional @noble peers wherever
9
- * the OS provides ed25519.
10
- *
11
- * WHAT IT DOES NOT FIX, stated plainly: the ORCHESTRATION still lives in JS.
12
- * A hostile bundle can skip calling any verifier and drive the native staging
13
- * methods directly. Moving the crypto native shrinks the attack surface (no
14
- * more tampering with a bundled crypto lib to flip a verdict) but the full
15
- * close needs native-ENFORCED staging with a natively-pinned key — an
16
- * architectural change tracked separately, not smuggled into this one.
17
- *
18
- * VERDICT CONTRACT (mirrors the spec): the native side resolves
19
- * 'valid' | 'invalid' | 'unavailable'. 'invalid' is a definitive NO.
20
- * 'unavailable' (Android < 33, malformed input, machinery failure) means this
21
- * source cannot answer — the composed verifier below then delegates to the
22
- * @noble builtin, and when that is absent too it THROWS, which the policy
23
- * layer converts to a rejection. Every path that cannot verify refuses.
24
- */
25
- import type { SignatureVerifier } from '../signature';
26
- /**
27
- * The native-first verifier, or null when the native module (or its
28
- * verifyEd25519 method — an app running new JS against an old binary) is
29
- * absent. Cached like the builtin: module availability does not change at
30
- * runtime, and this is consulted on every update check.
31
- */
32
- export declare function getNativeVerifier(): SignatureVerifier | null;
33
- /** Test seam — clears the cached resolution. */
34
- export declare function _resetNativeVerifier(): void;
35
- //# sourceMappingURL=nativeVerifier.d.ts.map
@@ -1,81 +0,0 @@
1
- /**
2
- * Bundle signature verification (OTA-03).
3
- *
4
- * WHAT WAS WRONG. `ota-signing-key.service.ts` documents the intended model
5
- * exactly: the CLI generates an ed25519 keypair locally, uploads only the public
6
- * half, and "the device verifies every bundle's signature before install and
7
- * rejects forgeries." Two of those three steps did not exist. The CLI had no
8
- * signing code, and this SDK received `bundle.signature` in the check response
9
- * and never read it. The only client-side check was SHA-256 against a hash
10
- * delivered in the same response as the URL — which defends against a corrupted
11
- * download and nothing else. On a platform whose entire purpose is remote code
12
- * delivery, that is the control that matters most.
13
- *
14
- * WHY IT IS SHAPED LIKE THIS. React Native has no built-in ed25519, so the
15
- * verification primitive has to come from somewhere. It is resolved in order:
16
- * a host-supplied verifier, else the built-in one assembled from the OPTIONAL
17
- * `@noble/ed25519` + `@noble/hashes` peers, else nothing — and the SDK's job is
18
- * to decide, unambiguously, what happens in that last case.
19
- *
20
- * The built-in path exists because requiring every adopter to hand-write a
21
- * verifier put the most security-critical operation in the product in code the
22
- * SDK could neither test nor audit, and made a routine dependency bump able to
23
- * silently stop all updates. See `crypto/builtinVerifier.ts`. Signing stays
24
- * opt-in, and apps that never adopt it carry no curve arithmetic.
25
- *
26
- * THE POLICY, which is the important part:
27
- *
28
- * - A bundle carrying a signature, with a configured public key and a
29
- * verifier: verified. Failure REJECTS the install. This is the goal state.
30
- * - A bundle carrying a signature, with a public key configured, but no
31
- * verifier available: REJECT. The app asked for signature enforcement by
32
- * configuring a key; silently installing unverified code because a helper is
33
- * missing would turn a security feature into a placebo.
34
- * - No public key configured: skip, and say so once. Signing is opt-in per app,
35
- * and an app that has not adopted it must keep working.
36
- *
37
- * The failure mode is the whole design: an app that opts in cannot be
38
- * accidentally downgraded to unverified installs.
39
- */
40
- /** Verifies a detached ed25519 signature. Supplied by the host app. */
41
- export type SignatureVerifier = (input: {
42
- /** Hex-encoded SHA-256 of the bundle — what the signature is over. */
43
- messageHex: string;
44
- /** Hex or base64 detached signature from the check response. */
45
- signature: string;
46
- /** The app's configured ed25519 public key. */
47
- publicKey: string;
48
- }) => boolean | Promise<boolean>;
49
- export interface SignatureConfig {
50
- /**
51
- * ed25519 public key(s) shipped in the app binary. Absent = signing not
52
- * adopted. An ARRAY pins several keys at once and a bundle is accepted when
53
- * ANY of them verifies — this is what makes key rotation possible without an
54
- * app-store release: ship a build pinning [old, new], start signing with new,
55
- * drop old from the next build. With a single pinnable key, losing the
56
- * private key meant no OTA capability until a new binary cleared review —
57
- * the exact emergency OTA exists to solve.
58
- */
59
- publicKey?: string | string[];
60
- /** Host-provided ed25519 verification function. */
61
- verifier?: SignatureVerifier;
62
- }
63
- export type SignatureOutcome = {
64
- ok: true;
65
- reason: 'verified' | 'not_configured';
66
- } | {
67
- ok: false;
68
- reason: 'no_verifier' | 'invalid_signature' | 'missing_signature' | 'verifier_threw' | 'no_keys';
69
- };
70
- /**
71
- * Decide whether a bundle may be staged.
72
- *
73
- * Returns a structured outcome rather than a boolean so the caller can emit a
74
- * precise telemetry reason — "we shipped unverified code because no key was
75
- * configured" and "we refused because the signature was forged" are very
76
- * different operational events and must not collapse into one.
77
- */
78
- export declare function verifyBundleSignature(bundleSha256: string, signature: string | undefined, config: SignatureConfig | undefined): Promise<SignatureOutcome>;
79
- /** Test seam — resets the once-per-process "not configured" warning. */
80
- export declare function _resetSignatureWarnings(): void;
81
- //# sourceMappingURL=signature.d.ts.map
@@ -1,257 +0,0 @@
1
- /**
2
- * Built-in ed25519 verifier, assembled from OPTIONAL peer dependencies.
3
- *
4
- * WHAT THIS REPLACES. Pinning `ota.publicSigningKey` used to require the host
5
- * app to write its own `verifySignature` — roughly a hundred lines of hex and
6
- * base64 decoding wrapped around a crypto library. That put the single most
7
- * security-critical operation in the product, the one deciding whether remote
8
- * code is authentic before it executes, in code the SDK could neither test nor
9
- * audit. Three failure modes all landed on the app author:
10
- *
11
- * - A verifier that always returns `true` silently disables the feature, and
12
- * nothing can detect it.
13
- * - @noble's hash-provider property moved between major versions, so a
14
- * routine dependency bump made every update fail closed with no signal
15
- * pointing at the cause.
16
- * - Hand-written base64 decoders tend to omit base64url, so a signature
17
- * containing `-` or `_` is rejected as a forgery.
18
- *
19
- * All three are now the SDK's problem, which is where they belong. A host that
20
- * installs `@noble/ed25519` and `@noble/hashes` gets verification by pinning a
21
- * key and writing no code at all.
22
- *
23
- * WHY OPTIONAL AND NOT A HARD DEPENDENCY. Most apps never adopt bundle
24
- * signing, and they should not carry curve arithmetic they will not run. The
25
- * `verifySignature` hook remains supported and still WINS over this, for teams
26
- * with their own crypto policy or a native implementation to delegate to.
27
- *
28
- * WHAT THIS DOES NOT FIX. Verification still happens in JS, inside the very
29
- * bundle it protects. An attacker who lands one malicious bundle by other means
30
- * can neuter the check for every update after it. Closing that needs native
31
- * verification — CryptoKit on iOS (13.4+, already the deployment target) and
32
- * `Signature.getInstance("Ed25519")` on Android API 33+, with a fallback below.
33
- */
34
-
35
- import { loadEd25519, type NobleEd25519 } from './loadEd25519';
36
- import { loadSha512, type Sha512Fn } from './loadSha512';
37
- import { logger } from '../../../core/logger/internalLogger';
38
- import type { SignatureVerifier } from '../signature';
39
-
40
- const ED25519_SIGNATURE_BYTES = 64;
41
- const ED25519_PUBLIC_KEY_BYTES = 32;
42
-
43
- /** Hex → bytes. Returns null rather than throwing; callers treat null as "reject". */
44
- function hexToBytes(hex: string): Uint8Array | null {
45
- const clean = hex.trim().toLowerCase().replace(/^0x/, '');
46
- if (clean.length === 0 || clean.length % 2 !== 0 || /[^0-9a-f]/.test(clean)) return null;
47
- const out = new Uint8Array(clean.length / 2);
48
- for (let i = 0; i < out.length; i++) {
49
- out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
50
- }
51
- return out;
52
- }
53
-
54
- const B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
55
-
56
- /**
57
- * Base64 → bytes, accepting base64url as well.
58
- *
59
- * The `-`/`_` handling is the point. A verifier that omits it rejects a
60
- * perfectly valid signature as a forgery the moment the signing side emits
61
- * base64url, and the resulting symptom — updates silently stop installing —
62
- * looks nothing like a decoding bug.
63
- */
64
- function base64ToBytes(b64: string): Uint8Array | null {
65
- const s = b64.trim().replace(/-/g, '+').replace(/_/g, '/').replace(/=+$/, '');
66
- if (s.length === 0) return null;
67
- const out: number[] = [];
68
- let buffer = 0;
69
- let bits = 0;
70
- for (const ch of s) {
71
- const idx = B64_ALPHABET.indexOf(ch);
72
- if (idx === -1) return null;
73
- buffer = (buffer << 6) | idx;
74
- bits += 6;
75
- if (bits >= 8) {
76
- bits -= 8;
77
- out.push((buffer >> bits) & 0xff);
78
- }
79
- }
80
- return Uint8Array.from(out);
81
- }
82
-
83
- /**
84
- * Decode a detached signature that may arrive hex- or base64-encoded.
85
- *
86
- * Tries the unambiguous case first: 128 hex characters is exactly 64 bytes and
87
- * cannot be anything else. Otherwise base64, then hex as a last resort. Only a
88
- * result of exactly 64 bytes is accepted, so a string that decodes under the
89
- * wrong scheme is rejected rather than fed to the curve code as garbage.
90
- */
91
- export function decodeSignature(sig: string): Uint8Array | null {
92
- const trimmed = sig.trim();
93
- if (/^(0x)?[0-9a-fA-F]{128}$/.test(trimmed)) {
94
- const hex = hexToBytes(trimmed);
95
- if (hex && hex.length === ED25519_SIGNATURE_BYTES) return hex;
96
- }
97
- const b64 = base64ToBytes(trimmed);
98
- if (b64 && b64.length === ED25519_SIGNATURE_BYTES) return b64;
99
- const hex = hexToBytes(trimmed);
100
- if (hex && hex.length === ED25519_SIGNATURE_BYTES) return hex;
101
- return null;
102
- }
103
-
104
- /**
105
- * Give @noble its SHA-512 provider.
106
- *
107
- * The property it reads moved between major versions — v2 wants
108
- * `etc.sha512Sync`, v3 wants `hashes.sha512` — so both are populated when
109
- * present. Getting this wrong does not fail loudly: verification simply throws
110
- * from inside the curve code and the outer catch converts it to "rejected",
111
- * which is indistinguishable from an actual forgery. That ambiguity is exactly
112
- * what made this a bad thing to ask app authors to maintain.
113
- */
114
- function wireSha512(ed: NobleEd25519, sha512: Sha512Fn): boolean {
115
- // VARIADIC ON PURPOSE. @noble calls its hash provider with several byte
116
- // arrays to be hashed as one concatenated message. A wrapper taking a
117
- // single argument hashes only the first, silently produces the wrong
118
- // digest, and every genuine signature is then reported as INVALID — no
119
- // error, no exception, just an authenticity check that always says no.
120
- // Verified against a real CLI-produced signature; the single-argument form
121
- // failed every positive case while passing every negative one, which is
122
- // the most misleading shape a bug of this kind can take.
123
- const concat = (parts: Uint8Array[]): Uint8Array => {
124
- if (parts.length === 1) return parts[0];
125
- let total = 0;
126
- for (const p of parts) total += p.length;
127
- const out = new Uint8Array(total);
128
- let off = 0;
129
- for (const p of parts) {
130
- out.set(p, off);
131
- off += p.length;
132
- }
133
- return out;
134
- };
135
- const sync = (...msgs: Uint8Array[]) => sha512(concat(msgs));
136
- const asyncFn = async (...msgs: Uint8Array[]) => sha512(concat(msgs));
137
-
138
- // EACH BRANCH GETS ITS OWN try. Wrapping both in one is a trap, and it cost
139
- // an afternoon: under v3 the `hashes` assignment succeeds and the `etc`
140
- // object is FROZEN, so the v2-shaped assignment throws "object is not
141
- // extensible" — discarding a verifier that was, by then, already correctly
142
- // wired. Only "neither shape took" means unavailable.
143
- let wired = false;
144
-
145
- try {
146
- if (ed.hashes && typeof ed.hashes === 'object') {
147
- if (typeof ed.hashes.sha512 !== 'function') ed.hashes.sha512 = sync;
148
- if (typeof ed.hashes.sha512Async !== 'function') ed.hashes.sha512Async = asyncFn;
149
- wired = typeof ed.hashes.sha512 === 'function';
150
- }
151
- } catch {
152
- /* frozen or absent — try the other shape */
153
- }
154
-
155
- try {
156
- if (ed.etc && typeof ed.etc === 'object') {
157
- if (typeof ed.etc.sha512Sync !== 'function') ed.etc.sha512Sync = sync;
158
- if (typeof ed.etc.sha512Async !== 'function') ed.etc.sha512Async = asyncFn;
159
- wired = wired || typeof ed.etc.sha512Sync === 'function';
160
- }
161
- } catch {
162
- /* frozen or absent — `wired` already records whether the other shape took */
163
- }
164
-
165
- return wired;
166
- }
167
-
168
- let resolved: SignatureVerifier | null | undefined;
169
-
170
- /**
171
- * The built-in verifier, or null when the optional deps are not installed.
172
- * Resolution is cached, including the negative result — a missing dependency
173
- * does not become present at runtime, and retrying the require on every update
174
- * check would be pure overhead.
175
- */
176
- export function getBuiltinVerifier(): SignatureVerifier | null {
177
- if (resolved !== undefined) return resolved;
178
-
179
- const ed = loadEd25519();
180
- const sha512 = loadSha512();
181
-
182
- if (!ed || !sha512) {
183
- resolved = null;
184
- return null;
185
- }
186
-
187
- if (!wireSha512(ed, sha512)) {
188
- // A shape neither branch matched — a @noble major this SDK predates.
189
- // Report "no verifier", which is actionable, rather than letting every
190
- // update fail deep inside the curve code with an opaque error.
191
- logger.error(
192
- '[OTA] @noble/ed25519 is installed but its SHA-512 provider could not be ' +
193
- 'wired (unrecognised version shape). Supply `ota.verifySignature` ' +
194
- 'yourself, or pin a @noble/ed25519 version this SDK supports.',
195
- );
196
- resolved = null;
197
- return null;
198
- }
199
-
200
- resolved = async ({ messageHex, signature, publicKey }) => {
201
- const message = hexToBytes(messageHex);
202
- const pub = hexToBytes(publicKey);
203
- const sig = decodeSignature(signature);
204
-
205
- if (!message) {
206
- logger.error('[OTA] Bundle SHA-256 is not valid hex — refusing to stage.');
207
- return false;
208
- }
209
- if (!pub || pub.length !== ED25519_PUBLIC_KEY_BYTES) {
210
- // Worth naming precisely: a mistyped or wrongly-encoded key would
211
- // otherwise present as "every update is a forgery".
212
- logger.error(
213
- `[OTA] ota.publicSigningKey must be ${ED25519_PUBLIC_KEY_BYTES} bytes of hex ` +
214
- `(${ED25519_PUBLIC_KEY_BYTES * 2} characters) — got ` +
215
- `${pub ? `${pub.length} bytes` : 'a value that is not hex'}.`,
216
- );
217
- return false;
218
- }
219
- if (!sig) {
220
- logger.error('[OTA] Bundle signature is not a 64-byte hex/base64 value.');
221
- return false;
222
- }
223
-
224
- // @noble THROWS rather than returning false for bytes that are not a
225
- // well-formed point — which is most forgeries, since a tampered
226
- // signature usually decodes to garbage rather than to a valid-but-wrong
227
- // point. Swallowing that here is deliberate: from the SDK's side a
228
- // malformed signature IS a rejection, and letting it escape would
229
- // surface as `verifier_threw`, the outcome reserved for a HOST verifier
230
- // misbehaving. Conflating "this bundle is forged" with "your verifier
231
- // is broken" would point every investigation in the wrong direction.
232
- try {
233
- // SYNC FIRST, and this is not a style preference. @noble's
234
- // `verifyAsync` reaches for `crypto.subtle` — "crypto.subtle must be
235
- // defined, consider polyfill" — which React Native does not provide
236
- // and neither does a plain Node test environment. The sync `verify`
237
- // uses the SHA-512 provider wired above and needs no WebCrypto, so
238
- // it is the path that actually works on a phone. Verification is a
239
- // few hundred microseconds over a 32-byte digest, once per update
240
- // check, so there is nothing to gain from the async form anyway.
241
- if (typeof ed.verify === 'function') return ed.verify(sig, message, pub);
242
- if (typeof ed.verifyAsync === 'function') return await ed.verifyAsync(sig, message, pub);
243
- return false;
244
- } catch {
245
- return false;
246
- }
247
- };
248
-
249
- __DEV__ &&
250
- logger.debug('[OTA] Using built-in ed25519 verifier (@noble/ed25519 detected).');
251
- return resolved;
252
- }
253
-
254
- /** Test seam — clears the cached resolution. */
255
- export function _resetBuiltinVerifier(): void {
256
- resolved = undefined;
257
- }