@onekeyfe/react-native-bundle-crypto 3.0.157-alpha.253 → 3.0.157-alpha.255

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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  react-native-bundle-crypto
4
4
 
5
+ [Native behavior specification](docs/SPEC.md)
6
+
5
7
  ## Installation
6
8
 
7
9
  ```sh
@@ -18,6 +18,7 @@ Pod::Spec.new do |s|
18
18
  "ios/**/*.{m,mm}",
19
19
  "cpp/**/*.{hpp,cpp}",
20
20
  ]
21
+ s.exclude_files = "ios/tests/**/*"
21
22
 
22
23
  # Vendored Gopenpgp framework: GPG cleartext/detached signature verification.
23
24
  # This is the single in-tree copy used by bundle-crypto and by modules that
@@ -32,4 +33,8 @@ Pod::Spec.new do |s|
32
33
  add_nitrogen_files(s)
33
34
 
34
35
  install_modules_dependencies(s)
36
+
37
+ s.test_spec "Tests" do |test_spec|
38
+ test_spec.source_files = "ios/tests/**/*.swift"
39
+ end
35
40
  end
@@ -133,4 +133,6 @@ dependencies {
133
133
  // react-native-app-update (the modules this security logic was migrated from).
134
134
  implementation "org.bouncycastle:bcpg-jdk15to18:1.78.1"
135
135
  implementation "org.bouncycastle:bcprov-jdk15to18:1.78.1"
136
+
137
+ testImplementation "junit:junit:4.13.2"
136
138
  }
@@ -391,8 +391,14 @@ object BundleCryptoCore {
391
391
  return false
392
392
  }
393
393
  val jsBundleDir = dir.absolutePath + "/"
394
+ val resolvedDir = try { dir.canonicalFile.toPath() } catch (e: Exception) { return false }
394
395
  val expected = HashMap<String, String>()
395
- for (entry in entries) expected[entry.relativePath] = entry.sha256
396
+ for (entry in entries) {
397
+ if (entry.relativePath.isEmpty() || File(entry.relativePath).isAbsolute) return false
398
+ val resolvedFile = try { File(dir, entry.relativePath).canonicalFile.toPath() } catch (e: Exception) { return false }
399
+ if (resolvedFile == resolvedDir || !resolvedFile.startsWith(resolvedDir)) return false
400
+ expected[entry.relativePath] = entry.sha256
401
+ }
396
402
 
397
403
  if (!validateFilesRecursive(dir, expected, jsBundleDir)) return false
398
404
 
@@ -0,0 +1,10 @@
1
+ package com.margelo.nitro.nativelogger
2
+
3
+ // Host-JVM test fixture: keep the production crypto core and BouncyCastle real
4
+ // while avoiding the production logger's Android/Nitro JNI initialization.
5
+ object OneKeyLog {
6
+ @JvmStatic
7
+ fun error(tag: String, message: String) = Unit
8
+ @JvmStatic
9
+ fun info(tag: String, message: String) = Unit
10
+ }
@@ -0,0 +1,135 @@
1
+ package com.margelo.nitro.reactnativebundlecrypto
2
+
3
+ import java.io.File
4
+ import java.nio.file.Files
5
+ import org.junit.Assert.assertEquals
6
+ import org.junit.Assert.assertFalse
7
+ import org.junit.Assert.assertNull
8
+ import org.junit.Assert.assertTrue
9
+ import org.junit.Rule
10
+ import org.junit.Test
11
+ import org.junit.rules.TemporaryFolder
12
+
13
+ class BundleCryptoCoreTest {
14
+ @get:Rule val temporaryFolder = TemporaryFolder()
15
+
16
+ private val expectedSignedHash = "2ada9c871104fc40649fa3de67a7d8e33faadc18e9abd587e8bb85be0a003eba"
17
+
18
+ // Existing release-key test vector, also used by BundleUpdate.testVerification.
19
+ private val signedMetadata = """-----BEGIN PGP SIGNED MESSAGE-----
20
+ Hash: SHA256
21
+
22
+ {
23
+ "fileName": "metadata.json",
24
+ "sha256": "2ada9c871104fc40649fa3de67a7d8e33faadc18e9abd587e8bb85be0a003eba",
25
+ "size": 158590,
26
+ "generatedAt": "2025-09-19T07:49:13.000Z"
27
+ }
28
+ -----BEGIN PGP SIGNATURE-----
29
+
30
+ iQJCBAEBCAAsFiEE62iuVE8f3YzSZGJPs2mmepC/OHsFAmjNJ1IOHGRldkBvbmVr
31
+ ZXkuc28ACgkQs2mmepC/OHs6Rw/9FKHl5aNsE7V0IsFf/l+h16BYKFwVsL69alMk
32
+ CFLna8oUn0+tyECF6wKBKw5pHo5YR27o2pJfYbAER6dygDF6WTZ1lZdf5QcBMjGA
33
+ LCeXC0hzUBzSSOH4bKBTa3fHp//HdSV1F2OnkymbXqYN7WXvuQPLZ0nV6aU88hCk
34
+ HgFifcvkXAnWKoosUtj0Bban/YBRyvmQ5C2akxUPEkr4Yck1QXwzJeNRd7wMXHjH
35
+ JFK6lJcuABiB8wpJDXJkFzKs29pvHIK2B2vdOjU2rQzKOUwaKHofDi5C4+JitT2b
36
+ 2pSeYP3PAxXYw6XDOmKTOiC7fPnfLjtcPjNYNFCezVKZT6LKvZW9obnW8Q9LNJ4W
37
+ okMPgHObkabv3OqUaTA9QNVfI/X9nvggzlPnaKDUrDWTf7n3vlrdexugkLtV/tJA
38
+ uguPlI5hY7Ue5OW7ckWP46hfmq1+UaIdeUY7dEO+rPZDz6KcArpaRwBiLPBhneIr
39
+ /X3KuMzS272YbPbavgCZGN9xJR5kZsEQE5HhPCbr6Nf0qDnh+X8mg0tAB/U6F+ZE
40
+ o90sJL1ssIaYvST+VWVaGRr4V5nMDcgHzWSF9Q/wm22zxe4alDaBdvOlUseW0iaM
41
+ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ
42
+ 91Js0rQ=
43
+ =A/Ii
44
+ -----END PGP SIGNATURE-----"""
45
+
46
+ @Test
47
+ fun signedMetadataRejectsChangedBodyAndUnsignedInput() {
48
+ val tampered = signedMetadata.replace("158590", "158591")
49
+ val rejected = BundleCryptoCore.verifyGpgCleartext(tampered)
50
+ assertFalse(rejected.valid)
51
+ assertNull(rejected.sha256)
52
+ assertEquals("SIGNATURE_INVALID", rejected.reason)
53
+
54
+ val unsigned = BundleCryptoCore.verifyGpgCleartext("{\"sha256\":\"$expectedSignedHash\"}")
55
+ assertFalse(unsigned.valid)
56
+ assertNull(unsigned.sha256)
57
+ assertEquals("NOT_PGP_SIGNED_MESSAGE", unsigned.reason)
58
+ }
59
+
60
+ @Test
61
+ fun validSignatureWithWrongAscBodyIsRejectedAfterVerification() {
62
+ val result = BundleCryptoCore.verifyDetachedAsc(signedMetadata)
63
+ assertFalse(result.valid)
64
+ assertNull(result.sha256)
65
+ assertEquals("SHA256_TOKEN_INVALID", result.reason)
66
+ }
67
+
68
+ @Test
69
+ fun directoryHashesRejectChangedMissingAndUnlistedFiles() {
70
+ val bundle = temporaryFolder.newFolder("bundle")
71
+ val nested = File(bundle, "nested/bundle").apply { mkdirs() }
72
+ val file = File(nested, "index.js").apply { writeText("abc") }
73
+ File(bundle, "metadata.json").writeText("ignored")
74
+
75
+ val hashes = BundleCryptoCore.hashDir(bundle.absolutePath)
76
+ assertEquals(1, hashes.size)
77
+ assertEquals("nested/bundle/index.js", hashes.single().relativePath)
78
+ assertEquals("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", hashes.single().sha256)
79
+ assertTrue(BundleCryptoCore.verifyDirAgainstHashes(bundle.absolutePath, hashes))
80
+
81
+ file.writeText("changed")
82
+ assertFalse(BundleCryptoCore.verifyDirAgainstHashes(bundle.absolutePath, hashes))
83
+ file.delete()
84
+ assertFalse(BundleCryptoCore.verifyDirAgainstHashes(bundle.absolutePath, hashes))
85
+
86
+ file.writeText("abc")
87
+ File(bundle, "evil-metadata.json").writeText("unlisted")
88
+ assertFalse(BundleCryptoCore.verifyDirAgainstHashes(bundle.absolutePath, hashes))
89
+ }
90
+
91
+ @Test
92
+ fun directoryHashesRejectExpectedPathsOutsideBundle() {
93
+ val root = temporaryFolder.newFolder("root")
94
+ val bundle = File(root, "bundle").apply { mkdirs() }
95
+ val outside = File(root, "outside.txt").apply { writeText("outside") }
96
+ val wrongHash = "0".repeat(64)
97
+
98
+ assertFalse(
99
+ BundleCryptoCore.verifyDirAgainstHashes(
100
+ bundle.absolutePath,
101
+ listOf(BundleCryptoCore.DirHash("../outside.txt", wrongHash))
102
+ )
103
+ )
104
+ assertFalse(
105
+ BundleCryptoCore.verifyDirAgainstHashes(
106
+ bundle.absolutePath,
107
+ listOf(BundleCryptoCore.DirHash(outside.absolutePath, wrongHash))
108
+ )
109
+ )
110
+ Files.createSymbolicLink(File(bundle, "link.txt").toPath(), outside.toPath())
111
+ assertFalse(
112
+ BundleCryptoCore.verifyDirAgainstHashes(
113
+ bundle.absolutePath,
114
+ listOf(BundleCryptoCore.DirHash("link.txt", wrongHash))
115
+ )
116
+ )
117
+ assertFalse(
118
+ BundleCryptoCore.verifyDirAgainstHashes(
119
+ bundle.absolutePath,
120
+ listOf(BundleCryptoCore.DirHash("", wrongHash))
121
+ )
122
+ )
123
+ }
124
+
125
+ @Test
126
+ fun extractedPathSafetyRejectsSymlinks() {
127
+ val bundle = temporaryFolder.newFolder("extracted")
128
+ val outside = temporaryFolder.newFile("outside.txt").apply { writeText("outside") }
129
+ File(bundle, "index.js").writeText("inside")
130
+ assertTrue(BundleCryptoCore.validateExtractedPathSafety(bundle.absolutePath))
131
+
132
+ Files.createSymbolicLink(File(bundle, "escape").toPath(), outside.toPath())
133
+ assertFalse(BundleCryptoCore.validateExtractedPathSafety(bundle.absolutePath))
134
+ }
135
+ }
@@ -411,9 +411,17 @@ public enum BundleCryptoCore {
411
411
  }
412
412
  // Normalize the directory prefix so relative paths match.
413
413
  let normalizedDir = (dirPath as NSString).hasSuffix("/") ? dirPath : dirPath + "/"
414
+ let resolvedDir = (dirPath as NSString).resolvingSymlinksInPath
415
+ let resolvedPrefix = resolvedDir == "/" ? "/" : resolvedDir + "/"
414
416
 
415
417
  var expected: [String: String] = [:]
416
- for entry in entries { expected[entry.relativePath] = entry.sha256 }
418
+ for entry in entries {
419
+ guard !entry.relativePath.isEmpty,
420
+ !(entry.relativePath as NSString).isAbsolutePath else { return false }
421
+ let resolvedFile = ((normalizedDir + entry.relativePath) as NSString).resolvingSymlinksInPath
422
+ guard resolvedFile.hasPrefix(resolvedPrefix) else { return false }
423
+ expected[entry.relativePath] = entry.sha256
424
+ }
417
425
 
418
426
  guard let enumerator = fm.enumerator(atPath: dirPath) else { return false }
419
427
  while let file = enumerator.nextObject() as? String {
@@ -0,0 +1,136 @@
1
+ import Foundation
2
+ import ReactNativeBundleCrypto
3
+ import XCTest
4
+
5
+ final class BundleCryptoCoreTests: XCTestCase {
6
+ private let expectedSignedHash = "2ada9c871104fc40649fa3de67a7d8e33faadc18e9abd587e8bb85be0a003eba"
7
+
8
+ // Existing release-key test vector, also used by BundleUpdate.testVerification.
9
+ private let signedMetadata = """
10
+ -----BEGIN PGP SIGNED MESSAGE-----
11
+ Hash: SHA256
12
+
13
+ {
14
+ "fileName": "metadata.json",
15
+ "sha256": "2ada9c871104fc40649fa3de67a7d8e33faadc18e9abd587e8bb85be0a003eba",
16
+ "size": 158590,
17
+ "generatedAt": "2025-09-19T07:49:13.000Z"
18
+ }
19
+ -----BEGIN PGP SIGNATURE-----
20
+
21
+ iQJCBAEBCAAsFiEE62iuVE8f3YzSZGJPs2mmepC/OHsFAmjNJ1IOHGRldkBvbmVr
22
+ ZXkuc28ACgkQs2mmepC/OHs6Rw/9FKHl5aNsE7V0IsFf/l+h16BYKFwVsL69alMk
23
+ CFLna8oUn0+tyECF6wKBKw5pHo5YR27o2pJfYbAER6dygDF6WTZ1lZdf5QcBMjGA
24
+ LCeXC0hzUBzSSOH4bKBTa3fHp//HdSV1F2OnkymbXqYN7WXvuQPLZ0nV6aU88hCk
25
+ HgFifcvkXAnWKoosUtj0Bban/YBRyvmQ5C2akxUPEkr4Yck1QXwzJeNRd7wMXHjH
26
+ JFK6lJcuABiB8wpJDXJkFzKs29pvHIK2B2vdOjU2rQzKOUwaKHofDi5C4+JitT2b
27
+ 2pSeYP3PAxXYw6XDOmKTOiC7fPnfLjtcPjNYNFCezVKZT6LKvZW9obnW8Q9LNJ4W
28
+ okMPgHObkabv3OqUaTA9QNVfI/X9nvggzlPnaKDUrDWTf7n3vlrdexugkLtV/tJA
29
+ uguPlI5hY7Ue5OW7ckWP46hfmq1+UaIdeUY7dEO+rPZDz6KcArpaRwBiLPBhneIr
30
+ /X3KuMzS272YbPbavgCZGN9xJR5kZsEQE5HhPCbr6Nf0qDnh+X8mg0tAB/U6F+ZE
31
+ o90sJL1ssIaYvST+VWVaGRr4V5nMDcgHzWSF9Q/wm22zxe4alDaBdvOlUseW0iaM
32
+ n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ
33
+ 91Js0rQ=
34
+ =A/Ii
35
+ -----END PGP SIGNATURE-----
36
+ """
37
+
38
+ func testSignedMetadataAcceptsReleaseKeyAndRejectsTampering() {
39
+ let verified = BundleCryptoCore.verifyGpgCleartext(signedMetadata)
40
+ XCTAssertTrue(verified.valid)
41
+ XCTAssertEqual(verified.sha256, expectedSignedHash)
42
+ XCTAssertNil(verified.reason)
43
+
44
+ let tampered = signedMetadata.replacingOccurrences(of: "158590", with: "158591")
45
+ let rejected = BundleCryptoCore.verifyGpgCleartext(tampered)
46
+ XCTAssertFalse(rejected.valid)
47
+ XCTAssertNil(rejected.sha256)
48
+ XCTAssertNotNil(rejected.reason)
49
+
50
+ let unsigned = BundleCryptoCore.verifyGpgCleartext("{\"sha256\":\"\(expectedSignedHash)\"}")
51
+ XCTAssertFalse(unsigned.valid)
52
+ XCTAssertNil(unsigned.sha256)
53
+ XCTAssertEqual(unsigned.reason, "NOT_PGP_SIGNED_MESSAGE")
54
+ }
55
+
56
+ func testDirectoryHashesRejectChangedMissingAndUnlistedFiles() throws {
57
+ try withTemporaryDirectory { root in
58
+ let bundle = root.appendingPathComponent("bundle", isDirectory: true)
59
+ let nested = bundle.appendingPathComponent("nested/bundle", isDirectory: true)
60
+ try FileManager.default.createDirectory(at: nested, withIntermediateDirectories: true)
61
+ let file = nested.appendingPathComponent("index.js")
62
+ try "abc".write(to: file, atomically: true, encoding: .utf8)
63
+ try "ignored".write(to: bundle.appendingPathComponent("metadata.json"), atomically: true, encoding: .utf8)
64
+
65
+ let hashes = try BundleCryptoCore.hashDir(bundle.path)
66
+ XCTAssertEqual(hashes.count, 1)
67
+ XCTAssertEqual(hashes.first?.relativePath, "nested/bundle/index.js")
68
+ XCTAssertEqual(hashes.first?.sha256, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
69
+ XCTAssertTrue(BundleCryptoCore.verifyDirAgainstHashes(dirPath: bundle.path, entries: hashes))
70
+
71
+ try "changed".write(to: file, atomically: true, encoding: .utf8)
72
+ XCTAssertFalse(BundleCryptoCore.verifyDirAgainstHashes(dirPath: bundle.path, entries: hashes))
73
+ try FileManager.default.removeItem(at: file)
74
+ XCTAssertFalse(BundleCryptoCore.verifyDirAgainstHashes(dirPath: bundle.path, entries: hashes))
75
+
76
+ try "abc".write(to: file, atomically: true, encoding: .utf8)
77
+ try "unlisted".write(to: bundle.appendingPathComponent("evil-metadata.json"), atomically: true, encoding: .utf8)
78
+ XCTAssertFalse(BundleCryptoCore.verifyDirAgainstHashes(dirPath: bundle.path, entries: hashes))
79
+ }
80
+ }
81
+
82
+ func testExtractedPathSafetyRejectsSymlinks() throws {
83
+ try withTemporaryDirectory { root in
84
+ let destination = root.appendingPathComponent("bundle", isDirectory: true)
85
+ let outside = root.appendingPathComponent("outside.txt")
86
+ try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)
87
+ try "outside".write(to: outside, atomically: true, encoding: .utf8)
88
+ try "inside".write(to: destination.appendingPathComponent("index.js"), atomically: true, encoding: .utf8)
89
+ XCTAssertTrue(BundleCryptoCore.validateExtractedPathSafety(destination.path))
90
+
91
+ try FileManager.default.createSymbolicLink(
92
+ at: destination.appendingPathComponent("escape"),
93
+ withDestinationURL: outside
94
+ )
95
+ XCTAssertFalse(BundleCryptoCore.validateExtractedPathSafety(destination.path))
96
+ }
97
+ }
98
+
99
+ func testDirectoryHashesRejectExpectedPathsOutsideBundle() throws {
100
+ try withTemporaryDirectory { root in
101
+ let bundle = root.appendingPathComponent("bundle", isDirectory: true)
102
+ let outside = root.appendingPathComponent("outside.txt")
103
+ try FileManager.default.createDirectory(at: bundle, withIntermediateDirectories: true)
104
+ try "outside".write(to: outside, atomically: true, encoding: .utf8)
105
+ let wrongHash = String(repeating: "0", count: 64)
106
+
107
+ XCTAssertFalse(BundleCryptoCore.verifyDirAgainstHashes(
108
+ dirPath: bundle.path,
109
+ entries: [.init(relativePath: "../outside.txt", sha256: wrongHash)]
110
+ ))
111
+ XCTAssertFalse(BundleCryptoCore.verifyDirAgainstHashes(
112
+ dirPath: bundle.path,
113
+ entries: [.init(relativePath: outside.path, sha256: wrongHash)]
114
+ ))
115
+ try FileManager.default.createSymbolicLink(
116
+ at: bundle.appendingPathComponent("link.txt"),
117
+ withDestinationURL: outside
118
+ )
119
+ XCTAssertFalse(BundleCryptoCore.verifyDirAgainstHashes(
120
+ dirPath: bundle.path,
121
+ entries: [.init(relativePath: "link.txt", sha256: wrongHash)]
122
+ ))
123
+ XCTAssertFalse(BundleCryptoCore.verifyDirAgainstHashes(
124
+ dirPath: bundle.path,
125
+ entries: [.init(relativePath: "", sha256: wrongHash)]
126
+ ))
127
+ }
128
+ }
129
+
130
+ private func withTemporaryDirectory(_ body: (URL) throws -> Void) throws {
131
+ let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
132
+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
133
+ defer { try? FileManager.default.removeItem(at: root) }
134
+ try body(root)
135
+ }
136
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-bundle-crypto",
3
- "version": "3.0.157-alpha.253",
3
+ "version": "3.0.157-alpha.255",
4
4
  "description": "react-native-bundle-crypto",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",