diff --git a/example/react-native/android/app/src/main/java/com/example/MainApplication.kt b/example/react-native/android/app/src/main/java/com/example/MainApplication.kt index 0e672ef5..41f852a5 100644 --- a/example/react-native/android/app/src/main/java/com/example/MainApplication.kt +++ b/example/react-native/android/app/src/main/java/com/example/MainApplication.kt @@ -6,6 +6,7 @@ import com.facebook.react.ReactApplication import com.facebook.react.ReactHost import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost +import com.margelo.nitro.nativelogger.OneKeyLog class MainApplication : Application(), ReactApplication { @@ -22,6 +23,7 @@ class MainApplication : Application(), ReactApplication { override fun onCreate() { super.onCreate() + OneKeyLog.info("App", "Application started") loadReactNative(this) } } diff --git a/example/react-native/ios/Example-Bridging-header.h b/example/react-native/ios/Example-Bridging-header.h index c83335ff..d058d3a6 100644 --- a/example/react-native/ios/Example-Bridging-header.h +++ b/example/react-native/ios/Example-Bridging-header.h @@ -1 +1,2 @@ #import "BackgroundRunnerModule.h" +#import "OneKeyLogBridge.h" diff --git a/example/react-native/ios/OneKeyLogBridge.h b/example/react-native/ios/OneKeyLogBridge.h new file mode 100644 index 00000000..c6e9ebbc --- /dev/null +++ b/example/react-native/ios/OneKeyLogBridge.h @@ -0,0 +1,5 @@ +#import + +@interface OneKeyLogBridge : NSObject ++ (void)info:(NSString *)tag message:(NSString *)message; +@end diff --git a/example/react-native/ios/OneKeyLogBridge.m b/example/react-native/ios/OneKeyLogBridge.m new file mode 100644 index 00000000..1e5dbdc9 --- /dev/null +++ b/example/react-native/ios/OneKeyLogBridge.m @@ -0,0 +1,23 @@ +#import "OneKeyLogBridge.h" + +// OneKeyLog is an @objc public Swift class in ReactNativeNativeLogger pod. +// We use forward declaration + performSelector to avoid importing the +// ReactNativeNativeLogger module (which fails due to C++ headers in NitroModules). +@implementation OneKeyLogBridge + ++ (void)info:(NSString *)tag message:(NSString *)message { + Class cls = NSClassFromString(@"ReactNativeNativeLogger.OneKeyLog"); + if (!cls) { + cls = NSClassFromString(@"OneKeyLog"); + } + if (cls) { + SEL sel = NSSelectorFromString(@"info::"); + if ([cls respondsToSelector:sel]) { + typedef void (*InfoFunc)(id, SEL, NSString *, NSString *); + InfoFunc func = (InfoFunc)[cls methodForSelector:sel]; + func(cls, sel, tag, message); + } + } +} + +@end diff --git a/example/react-native/ios/Podfile.lock b/example/react-native/ios/Podfile.lock index 710732f2..a0e9642f 100644 --- a/example/react-native/ios/Podfile.lock +++ b/example/react-native/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - BackgroundThread (1.1.17): + - BackgroundThread (1.1.22): - boost - DoubleConversion - fast_float @@ -25,10 +25,11 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - ReactNativeNativeLogger - SocketRocket - Yoga - boost (1.84.0) - - CloudKitModule (1.1.17): + - CloudKitModule (1.1.22): - boost - DoubleConversion - fast_float @@ -56,8 +57,12 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - ReactNativeNativeLogger - SocketRocket - Yoga + - CocoaLumberjack/Core (3.9.0) + - CocoaLumberjack/Swift (3.9.0): + - CocoaLumberjack/Core - DoubleConversion (1.1.6) - fast_float (8.0.0) - FBLazyVector (0.83.0) @@ -66,7 +71,7 @@ PODS: - hermes-engine (0.14.0): - hermes-engine/Pre-built (= 0.14.0) - hermes-engine/Pre-built (0.14.0) - - KeychainModule (1.1.17): + - KeychainModule (1.1.22): - boost - DoubleConversion - fast_float @@ -94,6 +99,41 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - ReactNativeNativeLogger + - SocketRocket + - Yoga + - MMKV (2.2.4): + - MMKVCore (~> 2.2.4) + - MMKVCore (2.2.4) + - NitroMmkv (4.1.2): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - MMKVCore (= 2.2.4) + - NitroModules + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core - SocketRocket - Yoga - NitroModules (0.33.2): @@ -2653,7 +2693,102 @@ PODS: - React-perflogger (= 0.83.0) - React-utils (= 0.83.0) - SocketRocket - - ReactNativeCheckBiometricAuthChanged (1.1.17): + - ReactNativeAppUpdate (1.1.22): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - NitroModules + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeNativeLogger + - SocketRocket + - Yoga + - ReactNativeBundleUpdate (1.1.22): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - MMKV (~> 2.2) + - NitroModules + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeNativeLogger + - SocketRocket + - SSZipArchive (~> 2.4) + - Yoga + - ReactNativeCheckBiometricAuthChanged (1.1.22): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - NitroModules + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeNativeLogger + - SocketRocket + - Yoga + - ReactNativeDeviceUtils (1.1.22): - boost - DoubleConversion - fast_float @@ -2681,9 +2816,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeDeviceUtils (1.1.17): + - ReactNativeGetRandomValues (1.1.22): - boost - DoubleConversion - fast_float @@ -2711,15 +2847,46 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - ReactNativeNativeLogger - SocketRocket - Yoga - - ReactNativeGetRandomValues (1.1.17): + - ReactNativeLiteCard (1.1.22): - boost - DoubleConversion - fast_float - fmt - glog - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeNativeLogger + - SocketRocket + - Yoga + - ReactNativeNativeLogger (1.1.22): + - boost + - CocoaLumberjack/Swift (~> 3.8) + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine - NitroModules - RCT-Folly - RCT-Folly/Fabric @@ -2743,17 +2910,50 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - ReactNativeLiteCard (1.1.17): + - ReactNativePerfMemory (1.1.22): - boost - DoubleConversion - fast_float - fmt - glog - hermes-engine + - NitroModules - RCT-Folly - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety + - React-callinvoker + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeNativeLogger + - SocketRocket + - Yoga + - ReactNativeSplashScreen (1.1.22): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - NitroModules + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-callinvoker - React-Core - React-debug - React-Fabric @@ -2769,9 +2969,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - ReactNativeNativeLogger - SocketRocket - Yoga - - Skeleton (1.1.17): + - Skeleton (1.1.22): - boost - DoubleConversion - fast_float @@ -2802,6 +3003,7 @@ PODS: - SocketRocket - Yoga - SocketRocket (0.7.1) + - SSZipArchive (2.4.3) - Yoga (0.0.0) DEPENDENCIES: @@ -2815,6 +3017,7 @@ DEPENDENCIES: - glog (from `../../../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - "KeychainModule (from `../../../node_modules/@onekeyfe/react-native-keychain-module`)" + - NitroMmkv (from `../../../node_modules/react-native-mmkv`) - NitroModules (from `../../../node_modules/react-native-nitro-modules`) - RCT-Folly (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTDeprecation (from `../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) @@ -2887,17 +3090,26 @@ DEPENDENCIES: - ReactAppDependencyProvider (from `build/generated/ios/ReactAppDependencyProvider`) - ReactCodegen (from `build/generated/ios/ReactCodegen`) - ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`) + - "ReactNativeAppUpdate (from `../../../node_modules/@onekeyfe/react-native-app-update`)" + - "ReactNativeBundleUpdate (from `../../../node_modules/@onekeyfe/react-native-bundle-update`)" - "ReactNativeCheckBiometricAuthChanged (from `../../../node_modules/@onekeyfe/react-native-check-biometric-auth-changed`)" - "ReactNativeDeviceUtils (from `../../../node_modules/@onekeyfe/react-native-device-utils`)" - "ReactNativeGetRandomValues (from `../../../node_modules/@onekeyfe/react-native-get-random-values`)" - "ReactNativeLiteCard (from `../../../node_modules/@onekeyfe/react-native-lite-card`)" + - "ReactNativeNativeLogger (from `../../../node_modules/@onekeyfe/react-native-native-logger`)" + - "ReactNativePerfMemory (from `../../../node_modules/@onekeyfe/react-native-perf-memory`)" + - "ReactNativeSplashScreen (from `../../../node_modules/@onekeyfe/react-native-splash-screen`)" - "Skeleton (from `../../../node_modules/@onekeyfe/react-native-skeleton`)" - SocketRocket (~> 0.7.1) - Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: trunk: + - CocoaLumberjack + - MMKV + - MMKVCore - SocketRocket + - SSZipArchive EXTERNAL SOURCES: BackgroundThread: @@ -2921,6 +3133,8 @@ EXTERNAL SOURCES: :tag: hermes-v0.14.0 KeychainModule: :path: "../../../node_modules/@onekeyfe/react-native-keychain-module" + NitroMmkv: + :path: "../../../node_modules/react-native-mmkv" NitroModules: :path: "../../../node_modules/react-native-nitro-modules" RCT-Folly: @@ -3063,6 +3277,10 @@ EXTERNAL SOURCES: :path: build/generated/ios/ReactCodegen ReactCommon: :path: "../../../node_modules/react-native/ReactCommon" + ReactNativeAppUpdate: + :path: "../../../node_modules/@onekeyfe/react-native-app-update" + ReactNativeBundleUpdate: + :path: "../../../node_modules/@onekeyfe/react-native-bundle-update" ReactNativeCheckBiometricAuthChanged: :path: "../../../node_modules/@onekeyfe/react-native-check-biometric-auth-changed" ReactNativeDeviceUtils: @@ -3071,22 +3289,32 @@ EXTERNAL SOURCES: :path: "../../../node_modules/@onekeyfe/react-native-get-random-values" ReactNativeLiteCard: :path: "../../../node_modules/@onekeyfe/react-native-lite-card" + ReactNativeNativeLogger: + :path: "../../../node_modules/@onekeyfe/react-native-native-logger" + ReactNativePerfMemory: + :path: "../../../node_modules/@onekeyfe/react-native-perf-memory" + ReactNativeSplashScreen: + :path: "../../../node_modules/@onekeyfe/react-native-splash-screen" Skeleton: :path: "../../../node_modules/@onekeyfe/react-native-skeleton" Yoga: :path: "../../../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - BackgroundThread: 22875378c238bbb3ae8e61c7aa8d0ebab67fe37c + BackgroundThread: 50a88f1224f90f9eb0323007606174c2d598ec33 boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - CloudKitModule: b2e3a96047478ada6a970ded4effb371ba3fbd99 + CloudKitModule: 830ac4cca52e3c3367c4513602ebb30ff7706ee8 + CocoaLumberjack: 5644158777912b7de7469fa881f8a3f259c2512a DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 FBLazyVector: a293a88992c4c33f0aee184acab0b64a08ff9458 fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 70fdc9d0bb0d8532e0411dcb21e53ce5a160960a - KeychainModule: 4e4e8187e6b2ddbff6e2b6117e530ab5e50c3a5c + KeychainModule: bdf0536c565b6cac099db1dad2fa09df41d8bae0 + MMKV: 1a8e7dbce7f9cad02c52e1b1091d07bd843aefaf + MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df + NitroMmkv: 0be91455465952f2b943f753b9ee7df028d89e5c NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3 RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: 2b70c6e3abe00396cefd8913efbf6a2db01a2b36 @@ -3158,12 +3386,18 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: ebcf3a78dc1bcdf054c9e8d309244bade6b31568 ReactCodegen: 554b421c45b7df35ac791da1b734335470b55fcc ReactCommon: 424cc34cf5055d69a3dcf02f3436481afb8b0f6f - ReactNativeCheckBiometricAuthChanged: 994b2c696a4c50dce9840c80944872ac3e9b24fd - ReactNativeDeviceUtils: b1312bad7e4899541d1cd0b5dc4b7db3ab6038f2 - ReactNativeGetRandomValues: 84ed71f8e120d8019be1e1b623e4e3c9c78a88d9 - ReactNativeLiteCard: 790a5dee05a01b83aa130c928645a74477c18279 - Skeleton: b8a7888e09e33a9772ee87dd68db0dd64f46e832 + ReactNativeAppUpdate: 8863fe2542205494752de15e9cac03a049b4db45 + ReactNativeBundleUpdate: 951bd08df70ec2fe6370b551d84d0f029fa836a1 + ReactNativeCheckBiometricAuthChanged: 066177a1de3ba717c6163d677550fdff8e69d729 + ReactNativeDeviceUtils: 0f00441313fe80e2849ee649bb7b30b81c605a65 + ReactNativeGetRandomValues: 124cf34dd5b4ae9726d263c934fbf89fd0575f50 + ReactNativeLiteCard: 1b074c5a82836feb0f29d6cb29d2662f2f6b2913 + ReactNativeNativeLogger: ace28b1932a6434463509a03e421a89892b4376a + ReactNativePerfMemory: 7d42fae97335ec64f2e6c9e95f5b04be5e0c3dbc + ReactNativeSplashScreen: 95e9a951e23c723d5f242f4d1ec2353158bb026b + Skeleton: 700f5403c7079e1190b5ab5942fb619f19dad315 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 + SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef Yoga: 6ca93c8c13f56baeec55eb608577619b17a4d64e PODFILE CHECKSUM: 56b37b8a08f6427bcf7d82875a56b3f462979e26 diff --git a/example/react-native/ios/example.xcodeproj/project.pbxproj b/example/react-native/ios/example.xcodeproj/project.pbxproj index a5aabe11..8f86577c 100644 --- a/example/react-native/ios/example.xcodeproj/project.pbxproj +++ b/example/react-native/ios/example.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 074590982EF13928004098A9 /* BackgroundRunnerModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 074590972EF13928004098A9 /* BackgroundRunnerModule.m */; }; + 0773E5D82F53404600E76F7A /* OneKeyLogBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 0773E5D72F53404600E76F7A /* OneKeyLogBridge.m */; }; 07F464D79E31412F737D9E2F /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; }; 0C80B921A6F3F58F76C31292 /* libPods-example.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-example.a */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; @@ -19,6 +20,8 @@ 074590942EF1390B004098A9 /* Example-Bridging-header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Example-Bridging-header.h"; sourceTree = ""; }; 074590962EF13928004098A9 /* BackgroundRunnerModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BackgroundRunnerModule.h; sourceTree = ""; }; 074590972EF13928004098A9 /* BackgroundRunnerModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BackgroundRunnerModule.m; sourceTree = ""; }; + 0773E5D62F53404600E76F7A /* OneKeyLogBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneKeyLogBridge.h; sourceTree = ""; }; + 0773E5D72F53404600E76F7A /* OneKeyLogBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneKeyLogBridge.m; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = example/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = example/Info.plist; sourceTree = ""; }; @@ -54,6 +57,8 @@ 074590942EF1390B004098A9 /* Example-Bridging-header.h */, 074590962EF13928004098A9 /* BackgroundRunnerModule.h */, 074590972EF13928004098A9 /* BackgroundRunnerModule.m */, + 0773E5D62F53404600E76F7A /* OneKeyLogBridge.h */, + 0773E5D72F53404600E76F7A /* OneKeyLogBridge.m */, ); name = example; sourceTree = ""; @@ -255,6 +260,7 @@ files = ( 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */, 074590982EF13928004098A9 /* BackgroundRunnerModule.m in Sources */, + 0773E5D82F53404600E76F7A /* OneKeyLogBridge.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/example/react-native/ios/example/AppDelegate.swift b/example/react-native/ios/example/AppDelegate.swift index eb35e352..5ff0433c 100644 --- a/example/react-native/ios/example/AppDelegate.swift +++ b/example/react-native/ios/example/AppDelegate.swift @@ -14,6 +14,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { + OneKeyLogBridge.info("App", message: "Application started") + DispatchQueue.global(qos: .userInitiated).async { // BackgroundRunnerModule.startBackgroundRunner() } diff --git a/example/react-native/ios/example/PrivacyInfo.xcprivacy b/example/react-native/ios/example/PrivacyInfo.xcprivacy index 41b8317f..7852c8c5 100644 --- a/example/react-native/ios/example/PrivacyInfo.xcprivacy +++ b/example/react-native/ios/example/PrivacyInfo.xcprivacy @@ -10,6 +10,15 @@ NSPrivacyAccessedAPITypeReasons C617.1 + 0A2A.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + E174.1 diff --git a/example/react-native/package.json b/example/react-native/package.json index 861b125f..6ebe9ae5 100644 --- a/example/react-native/package.json +++ b/example/react-native/package.json @@ -11,18 +11,24 @@ "test": "jest" }, "dependencies": { + "@onekeyfe/react-native-app-update": "workspace:*", "@onekeyfe/react-native-background-thread": "workspace:*", + "@onekeyfe/react-native-bundle-update": "workspace:*", "@onekeyfe/react-native-check-biometric-auth-changed": "workspace:*", "@onekeyfe/react-native-cloud-kit-module": "workspace:*", "@onekeyfe/react-native-device-utils": "workspace:*", "@onekeyfe/react-native-get-random-values": "workspace:*", "@onekeyfe/react-native-keychain-module": "workspace:*", "@onekeyfe/react-native-lite-card": "workspace:*", + "@onekeyfe/react-native-native-logger": "workspace:*", + "@onekeyfe/react-native-perf-memory": "workspace:*", "@onekeyfe/react-native-skeleton": "workspace:*", + "@onekeyfe/react-native-splash-screen": "workspace:*", "@react-native/new-app-screen": "0.83.0", "fast-base64-decode": "*", "react": "19.2.0", "react-native": "0.83.0", + "react-native-mmkv": "^4.1.2", "react-native-safe-area-context": "^5.5.2" }, "devDependencies": { diff --git a/example/react-native/pages/AppUpdateTestPage.tsx b/example/react-native/pages/AppUpdateTestPage.tsx new file mode 100644 index 00000000..5fb83e16 --- /dev/null +++ b/example/react-native/pages/AppUpdateTestPage.tsx @@ -0,0 +1,743 @@ +import { useState, useRef, useEffect, useCallback } from 'react'; +import { + View, + Text, + StyleSheet, + Alert, + TouchableOpacity, + Animated, + ActivityIndicator, + Platform, +} from 'react-native'; +import { TestPageBase, TestButton, TestInput, TestResult } from './TestPageBase'; +import { ReactNativeAppUpdate } from '@onekeyfe/react-native-app-update'; +import type { DownloadEvent } from '@onekeyfe/react-native-app-update'; + +// --- Types --- + +type StepStatus = 'pending' | 'active' | 'completed' | 'error'; + +interface StepState { + status: StepStatus; + errorMessage?: string; +} + +interface WorkflowState { + download: StepState; + downloadAsc: StepState; + verifyAsc: StepState; + verify: StepState; + install: StepState; + downloadProgress: number; +} + +type StepId = 'download' | 'downloadAsc' | 'verifyAsc' | 'verify' | 'install'; + +// --- Color Palette --- + +const C = { + cardBg: '#1c1c1e', + cardBgElevated: '#2c2c2e', + surfaceBg: '#0a0a0a', + accentCyan: '#00d4aa', + accentCyanDim: 'rgba(0, 212, 170, 0.15)', + accentBlue: '#007AFF', + errorRed: '#ff453a', + textPrimary: '#ffffff', + textSecondary: '#8e8e93', + textTertiary: '#636366', + border: '#38383a', +}; + +const mono = Platform.OS === 'ios' ? 'Menlo' : 'monospace'; + +// --- Sub-components --- + +function StepRow({ + stepNumber, + label, + status, + errorMessage, + onAction, + actionLabel, + disabled, + children, +}: { + stepNumber: number; + label: string; + status: StepStatus; + errorMessage?: string; + onAction: () => void; + actionLabel: string; + disabled: boolean; + children?: React.ReactNode; +}) { + const btnLabel = + status === 'active' ? '...' : status === 'error' ? 'Retry' : actionLabel; + + return ( + + + + {status === 'completed' ? ( + {'✓'} + ) : status === 'error' ? ( + ! + ) : status === 'active' ? ( + + ) : ( + {stepNumber} + )} + + + + + {label} + + {status === 'active' && ( + In progress... + )} + {status === 'completed' && ( + + Completed + + )} + {status === 'error' && errorMessage && ( + + {errorMessage} + + )} + + + + + {btnLabel} + + + + {children} + + ); +} + +function StepConnector({ active }: { active: boolean }) { + return ( + + + + ); +} + +function ProgressBar({ + progress, + percentage, +}: { + progress: Animated.Value; + percentage: number; +}) { + const barWidth = progress.interpolate({ + inputRange: [0, 1], + outputRange: ['0%', '100%'], + }); + + return ( + + + + + {`${Math.round(percentage)}%`} + + ); +} + +// --- Main Component --- + +interface AppUpdateTestPageProps { + onGoHome: () => void; + safeAreaInsets: any; +} + +const INITIAL_WORKFLOW: WorkflowState = { + download: { status: 'pending' }, + downloadAsc: { status: 'pending' }, + verifyAsc: { status: 'pending' }, + verify: { status: 'pending' }, + install: { status: 'pending' }, + downloadProgress: 0, +}; + +export function AppUpdateTestPage({ + onGoHome, + safeAreaInsets, +}: AppUpdateTestPageProps) { + // --- Workflow state --- + const [workflow, setWorkflow] = useState(INITIAL_WORKFLOW); + const progressAnim = useRef(new Animated.Value(0)).current; + const listenerIdRef = useRef(null); + + // --- Input state --- + const [downloadUrl, setDownloadUrl] = useState( + 'https://web.onekey-asset.com/app-monorepo/v6.0.0/OneKey-Wallet-6.0.0-android.apk', + ); + + // --- Utility state --- + const [utilExpanded, setUtilExpanded] = useState(false); + const [utilResult, setUtilResult] = useState(null); + const [utilError, setUtilError] = useState(null); + + // --- Cleanup listener on unmount --- + useEffect(() => { + return () => { + if (listenerIdRef.current !== null) { + ReactNativeAppUpdate.removeDownloadListener(listenerIdRef.current); + } + }; + }, []); + + // --- Helpers --- + const updateStep = useCallback( + (step: StepId, update: Partial) => { + setWorkflow((prev) => ({ + ...prev, + [step]: { ...prev[step], ...update }, + })); + }, + [], + ); + + const resetWorkflow = useCallback(() => { + setWorkflow(INITIAL_WORKFLOW); + progressAnim.setValue(0); + }, [progressAnim]); + + const clearUtil = () => { + setUtilResult(null); + setUtilError(null); + }; + + // --- Step Handlers --- + + const handleDownload = useCallback(async () => { + if (!downloadUrl) { + updateStep('download', { + status: 'error', + errorMessage: 'Please enter a download URL', + }); + return; + } + + // Reset all + setWorkflow({ + ...INITIAL_WORKFLOW, + download: { status: 'active' }, + }); + progressAnim.setValue(0); + + // Register listener for progress + const lid = ReactNativeAppUpdate.addDownloadListener( + (event: DownloadEvent) => { + if (event.type === 'update/downloading') { + setWorkflow((prev) => ({ + ...prev, + downloadProgress: event.progress, + })); + Animated.timing(progressAnim, { + toValue: event.progress / 100, + duration: 200, + useNativeDriver: false, + }).start(); + } + }, + ); + listenerIdRef.current = lid; + + try { + await ReactNativeAppUpdate.downloadAPK({ + downloadUrl, + notificationTitle: 'Downloading Update', + fileSize: 183670673, + }); + + setWorkflow((prev) => ({ + ...prev, + download: { status: 'completed' }, + downloadProgress: 100, + })); + Animated.timing(progressAnim, { + toValue: 1, + duration: 300, + useNativeDriver: false, + }).start(); + } catch (err) { + updateStep('download', { + status: 'error', + errorMessage: err instanceof Error ? err.message : 'Download failed', + }); + setWorkflow((prev) => ({ ...prev, downloadProgress: 0 })); + } finally { + if (listenerIdRef.current !== null) { + ReactNativeAppUpdate.removeDownloadListener(listenerIdRef.current); + listenerIdRef.current = null; + } + } + }, [downloadUrl, progressAnim, updateStep]); + + const handleDownloadAsc = useCallback(async () => { + if (!downloadUrl) { + updateStep('downloadAsc', { + status: 'error', + errorMessage: 'Please enter a download URL', + }); + return; + } + updateStep('downloadAsc', { status: 'active' }); + try { + await ReactNativeAppUpdate.downloadASC({ downloadUrl }); + updateStep('downloadAsc', { status: 'completed' }); + } catch (err) { + updateStep('downloadAsc', { + status: 'error', + errorMessage: + err instanceof Error ? err.message : 'ASC download failed', + }); + } + }, [downloadUrl, updateStep]); + + const handleVerifyAsc = useCallback(async () => { + if (!downloadUrl) { + updateStep('verifyAsc', { + status: 'error', + errorMessage: 'Please enter a download URL', + }); + return; + } + updateStep('verifyAsc', { status: 'active' }); + try { + await ReactNativeAppUpdate.verifyASC({ downloadUrl }); + updateStep('verifyAsc', { status: 'completed' }); + } catch (err) { + updateStep('verifyAsc', { + status: 'error', + errorMessage: + err instanceof Error ? err.message : 'ASC verification failed', + }); + } + }, [downloadUrl, updateStep]); + + const handleVerify = useCallback(async () => { + if (!downloadUrl) { + updateStep('verify', { + status: 'error', + errorMessage: 'Please enter a download URL', + }); + return; + } + updateStep('verify', { status: 'active' }); + try { + await ReactNativeAppUpdate.verifyAPK({ downloadUrl }); + updateStep('verify', { status: 'completed' }); + } catch (err) { + updateStep('verify', { + status: 'error', + errorMessage: + err instanceof Error ? err.message : 'Verification failed', + }); + } + }, [downloadUrl, updateStep]); + + const handleInstall = useCallback(async () => { + if (!downloadUrl) { + updateStep('install', { + status: 'error', + errorMessage: 'Please enter a download URL', + }); + return; + } + updateStep('install', { status: 'active' }); + try { + await ReactNativeAppUpdate.installAPK({ downloadUrl }); + updateStep('install', { status: 'completed' }); + } catch (err) { + updateStep('install', { + status: 'error', + errorMessage: + err instanceof Error ? err.message : 'Installation failed', + }); + } + }, [downloadUrl, updateStep]); + + // --- Utility handlers --- + + const utilClearCache = async () => { + clearUtil(); + try { + await ReactNativeAppUpdate.clearCache(); + setUtilResult({ cacheCleared: true }); + } catch (err) { + setUtilError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + + const utilRegisterListener = () => { + clearUtil(); + try { + if (listenerIdRef.current !== null) { + Alert.alert('Info', 'Listener already registered'); + return; + } + const id = ReactNativeAppUpdate.addDownloadListener((event) => { + setUtilResult({ + downloadEvent: event, + timestamp: new Date().toLocaleTimeString(), + }); + }); + listenerIdRef.current = id; + setUtilResult({ listenerRegistered: true, listenerId: id }); + } catch (err) { + setUtilError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + + // --- Render --- + + const showProgress = + workflow.download.status === 'active' || + workflow.download.status === 'completed'; + + return ( + + {/* URL Input */} + + APK PARAMETERS + + + + {/* Pipeline */} + + UPDATE PIPELINE + + + {showProgress && ( + + )} + + + + + + + + + + + + + + + + + + + + {/* Reset */} + + Reset Pipeline + + + {/* Platform Info */} + + PLATFORM INFO + + {Platform.OS === 'ios' + ? 'iOS: All APK operations are no-ops on this platform.\nApp updates are handled through the App Store.' + : 'Android: APK download, verification, and installation\nare supported on this platform.'} + + + + {/* Utilities */} + setUtilExpanded(!utilExpanded)} + activeOpacity={0.7} + > + UTILITIES & DEBUG + {utilExpanded ? '▲' : '▼'} + + {utilExpanded && ( + + + + + + )} + + ); +} + +// --- Styles --- + +const s = StyleSheet.create({ + // Card + card: { + backgroundColor: C.cardBg, + borderRadius: 12, + padding: 16, + marginBottom: 14, + borderWidth: 1, + borderColor: C.border, + }, + cardTitle: { + fontSize: 11, + fontWeight: '700', + color: C.textSecondary, + letterSpacing: 1.5, + marginBottom: 12, + }, + + // Steps + stepRow: { + marginBottom: 4, + }, + stepHeader: { + flexDirection: 'row', + alignItems: 'center', + }, + stepCircle: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: C.cardBgElevated, + borderWidth: 2, + borderColor: C.border, + alignItems: 'center', + justifyContent: 'center', + }, + stepCircleActive: { + borderColor: C.accentCyan, + backgroundColor: C.accentCyanDim, + }, + stepCircleCompleted: { + borderColor: C.accentCyan, + backgroundColor: C.accentCyan, + }, + stepCircleError: { + borderColor: C.errorRed, + backgroundColor: 'rgba(255,69,58,0.15)', + }, + stepNum: { + fontSize: 14, + fontWeight: '700', + color: C.textTertiary, + }, + stepCheck: { + fontSize: 16, + fontWeight: '700', + color: '#000', + }, + stepBang: { + fontSize: 16, + fontWeight: '700', + color: C.errorRed, + }, + stepInfo: { + flex: 1, + marginLeft: 12, + }, + stepLabel: { + fontSize: 15, + fontWeight: '600', + color: C.textPrimary, + }, + stepLabelDim: { + color: C.textTertiary, + }, + stepSub: { + fontSize: 12, + color: C.accentCyan, + marginTop: 2, + }, + stepBtn: { + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 8, + backgroundColor: C.accentBlue, + minWidth: 80, + alignItems: 'center', + }, + stepBtnDisabled: { + backgroundColor: C.cardBgElevated, + }, + stepBtnDone: { + backgroundColor: 'transparent', + borderWidth: 1, + borderColor: C.accentCyan, + }, + stepBtnText: { + fontSize: 13, + fontWeight: '600', + color: '#fff', + }, + stepBtnTextDisabled: { + color: C.textTertiary, + }, + + // Connector + connectorWrap: { + paddingLeft: 15, + height: 24, + justifyContent: 'center', + }, + connectorLine: { + width: 2, + height: '100%', + backgroundColor: C.border, + }, + connectorLineActive: { + backgroundColor: C.accentCyan, + }, + + // Progress + progressWrap: { + marginTop: 10, + marginLeft: 44, + }, + progressTrack: { + height: 6, + backgroundColor: C.surfaceBg, + borderRadius: 3, + overflow: 'hidden', + }, + progressFill: { + height: '100%', + backgroundColor: C.accentCyan, + borderRadius: 3, + }, + progressText: { + fontSize: 11, + color: C.textSecondary, + marginTop: 4, + fontFamily: mono, + }, + + // Reset + resetBtn: { + alignSelf: 'center', + paddingHorizontal: 20, + paddingVertical: 10, + borderRadius: 8, + borderWidth: 1, + borderColor: C.border, + marginBottom: 14, + }, + resetBtnText: { + fontSize: 13, + fontWeight: '500', + color: C.textSecondary, + }, + + // Info text + infoText: { + fontSize: 11, + color: C.textTertiary, + fontFamily: mono, + backgroundColor: C.cardBgElevated, + padding: 10, + borderRadius: 6, + lineHeight: 16, + }, + + // Utilities + utilHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 2, + }, + chevron: { + fontSize: 12, + color: C.textSecondary, + }, +}); diff --git a/example/react-native/pages/BundleUpdateTestPage.tsx b/example/react-native/pages/BundleUpdateTestPage.tsx new file mode 100644 index 00000000..59a7b914 --- /dev/null +++ b/example/react-native/pages/BundleUpdateTestPage.tsx @@ -0,0 +1,923 @@ +import { useState, useRef, useEffect, useCallback } from 'react'; +import { + View, + Text, + StyleSheet, + Alert, + Switch, + TouchableOpacity, + Animated, + ActivityIndicator, + Platform, +} from 'react-native'; +import { TestPageBase, TestButton, TestInput, TestResult } from './TestPageBase'; +import { ReactNativeBundleUpdate } from '@onekeyfe/react-native-bundle-update'; +import type { + BundleDownloadEvent, + BundleDownloadResult, +} from '@onekeyfe/react-native-bundle-update'; +import { createMMKV } from 'react-native-mmkv'; + +const devSettingsMmkv = createMMKV({ id: 'onekey-app-dev-setting' }); +const KEY_DEV_MODE = 'onekey_developer_mode_enabled'; +const KEY_SKIP_GPG = 'onekey_bundle_skip_gpg_verification'; + +// --- Types --- + +type StepStatus = 'pending' | 'active' | 'completed' | 'error'; + +interface StepState { + status: StepStatus; + errorMessage?: string; +} + +interface WorkflowState { + download: StepState; + verify: StepState; + install: StepState; + downloadProgress: number; + downloadResult: BundleDownloadResult | null; +} + +type StepId = 'download' | 'verify' | 'install'; + +// --- Color Palette --- + +const C = { + cardBg: '#1c1c1e', + cardBgElevated: '#2c2c2e', + surfaceBg: '#0a0a0a', + accentCyan: '#00d4aa', + accentCyanDim: 'rgba(0, 212, 170, 0.15)', + accentBlue: '#007AFF', + errorRed: '#ff453a', + textPrimary: '#ffffff', + textSecondary: '#8e8e93', + textTertiary: '#636366', + border: '#38383a', +}; + +const mono = Platform.OS === 'ios' ? 'Menlo' : 'monospace'; + +// --- Sub-components --- + +function StepRow({ + stepNumber, + label, + status, + errorMessage, + onAction, + actionLabel, + disabled, + children, +}: { + stepNumber: number; + label: string; + status: StepStatus; + errorMessage?: string; + onAction: () => void; + actionLabel: string; + disabled: boolean; + children?: React.ReactNode; +}) { + const btnLabel = + status === 'active' ? '...' : status === 'error' ? 'Retry' : actionLabel; + + return ( + + + + {status === 'completed' ? ( + {'✓'} + ) : status === 'error' ? ( + ! + ) : status === 'active' ? ( + + ) : ( + {stepNumber} + )} + + + + + {label} + + {status === 'active' && ( + In progress... + )} + {status === 'completed' && ( + + Completed + + )} + {status === 'error' && errorMessage && ( + + {errorMessage} + + )} + + + + + {btnLabel} + + + + {children} + + ); +} + +function StepConnector({ active }: { active: boolean }) { + return ( + + + + ); +} + +function ProgressBar({ + progress, + percentage, +}: { + progress: Animated.Value; + percentage: number; +}) { + const barWidth = progress.interpolate({ + inputRange: [0, 1], + outputRange: ['0%', '100%'], + }); + + return ( + + + + + {`${Math.round(percentage)}%`} + + ); +} + +function ToggleRow({ + label, + subLabel, + value, + onToggle, +}: { + label: string; + subLabel: string; + value: boolean; + onToggle: (v: boolean) => void; +}) { + return ( + + + {label} + {subLabel} + + + + ); +} + +// --- Main Component --- + +interface BundleUpdateTestPageProps { + onGoHome: () => void; + safeAreaInsets: any; +} + +const INITIAL_WORKFLOW: WorkflowState = { + download: { status: 'pending' }, + verify: { status: 'pending' }, + install: { status: 'pending' }, + downloadProgress: 0, + downloadResult: null, +}; + +export function BundleUpdateTestPage({ + onGoHome, + safeAreaInsets, +}: BundleUpdateTestPageProps) { + // --- Workflow state --- + const [workflow, setWorkflow] = useState(INITIAL_WORKFLOW); + const progressAnim = useRef(new Animated.Value(0)).current; + const listenerIdRef = useRef(null); + + // --- Input state --- + const defaultParams = Platform.select({ + ios: { + downloadUrl: 'https://uni-test.onekey-asset.com/dashboard/version-update/5016000/upload_1761581017500.0.842474996421545.0.zip', + latestVersion: '5.16.0', + bundleVersion: '200', + fileSize: 51617503, + sha256: 'c0beb980fc113bc21ea510b778933ed488dc685c2216105bd146df8e9f791a3d', + signature: `-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA256 + +{ + "fileName": "metadata.json", + "sha256": "8c71473ccb1c590e8c13642559600eb0c8e2649c9567236e2ac27e79e919a12c", + "size": 23123, + "generatedAt": "2025-10-22T09:50:50.446Z" +} +-----BEGIN PGP SIGNATURE----- + +iQJCBAEBCAAsFiEE62iuVE8f3YzSZGJPs2mmepC/OHsFAmj/ZF0OHGRldkBvbmVr +ZXkuc28ACgkQs2mmepC/OHuVZhAArMmwReTpiw+XoKTw7bwlVrz0OWHfAkdh6lFY +xQpGj+AsY38NKJImrK7IQLhcnTJIwycY0a5eh8Wnqs0sxtmmwwyWQs+RHSwIdlTJ +CLpTUGxowNiD0ldz0LVLjPFqZz3/fYKkpGW1+ejkMdRXBbUrFGTa+XsEd0k3TWj2 +bxFrhy128SpQ1NJ8AXXWRzZaenFAADa5ZEJUMV4Q8sjV+C8OXtVKeW1IDXAvWEzx +x9SWU4HD4ciKYT6yRZ6RuHJ3YXFdIDPMrPXDSPTjcZUnhsadT0qFoRck6ya4uyQP +SNvEge9W9Kcup0XfKkK5SnIRyZeKgW5Zn39W8C5equqmrGy581E6R28KS3KHsE66 +Pf6WmVE/XuAKt5F++TmC6RBZ9PISPdOVhWcPZ74ySsFOUQ0nswMg1GLQ/kfixXIl +8ejFGhzhCRDmxYZ1aEJeMAAQhBuXM5TKtY79TIT9lNlttM0J/hl3rTTVxt9xSsMW +MCduz+A1mdO8T/DPqvpJksOO/YOT4gzHT9OSXNsYdte1QJKHmQdeAfzi/m66Z2/L +1qqTvwH3byXreUAjXwAWZLIbAQJ6zeeIrVKiut7DCJOHE+kGS2vdQiM2NmRFE0hP +qxdzLH784DPCWB36Xd3VZfbUxKOc06+bHlFCEXyylWD3schXV9c8Amz4DoriYIdi +Ni3q+jg= +=BVQy +-----END PGP SIGNATURE-----` + }, + android: { + downloadUrl: 'https://uni.onekey-asset.com/dashboard/version-update/5019002/upload_1767687090638.0.6528223946398171.0.zip', + latestVersion: '5.19.0', + bundleVersion: '2', + fileSize: 55586119, + sha256: 'cdff9d4b37f2940e5ce73141e384f412e1e4e4bf3046b0cdb4d79b0c974de742', + signature: `-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA256 + +{ + "fileName": "metadata.json", + "sha256": "cece2fccea3c3a43e0da7ab803f44e5a4850a8b06f2df06db3e7f16860080a40", + "size": 28919, + "generatedAt": "2026-01-06T05:42:35.407Z", + "appType": "android", + "appVersion": "5.19.2", + "buildNumber": "2026010644", + "bundleVersion": "2" +} +-----BEGIN PGP SIGNATURE----- + +iQJCBAEBCAAsFiEE62iuVE8f3YzSZGJPs2mmepC/OHsFAmlcvy8OHGRldkBvbmVr +ZXkuc28ACgkQs2mmepC/OHtxlA//QEvclfq0X9isJXBHFsRZx+JhfGOao60Sl0rW +m11AU6utvOAXHwxnhtLENuB2cDhhKkDrN582R2QhsdRJngRqWafwuBaVBJx+ErZV +KqvAlTj9hcLACXBw/dOyQ4JwDwm2jwloH4H//eiQdFcp1MT/uiGf3Yu9fonEC6ap +URBiiA7wAPg2o9V6zJchv/CM/xGA9G/I337lR0yAID2Y6Oteu9CftCSGqav4va4O +C94nW/wWQdt+XllY+i46mXxKOOoaIxUMP5K6q1q5CcjZBNm6Pgkay5YEDmbershP +/DeSpHwTk2APOIoaw1JVeKP8HrhIg4iGjmrUkveBoHJrGu1x6FjZ0tGqVJkG7b8f +wuBBHoPlCULbR38eFudv6UBYsJ2Zb2MEwMEC4quBczU6wg4NnOSKFK03kqsStyxG +0F+HShqAPZZrvGUUOMBMWhyxpDmbslkXtQntPhaMM8+NGqegTMiLZQnWkvzCIdoc +EYJOSlAICF/VkFzo8+LSuUgCQbqXF5qnFhcsIdzR8rguFKHC9/8/umlVjua5ilnu +bxULNIeYztMeXB29J8JXpu4efz+v5r9/HsddXlY0wMrmEWNiw+bG+ruT3O9pC9k9 +hGatKzsRToFrdoTaHG6xnKhiVH7MhQHGjvEK5KpyXIvQxy9SCIloAqs0oXrW4Yuz +H3bEFZ8= +=ZjEV +-----END PGP SIGNATURE-----` + } + }) ?? { + downloadUrl: '', + latestVersion: '1.0.0', + bundleVersion: '1', + fileSize: 1, + sha256: '', + signature: '', + }; + const [downloadUrl, setDownloadUrl] = useState(defaultParams.downloadUrl); + + // --- Dev settings --- + const [devModeEnabled, setDevModeEnabled] = useState( + () => devSettingsMmkv.getBoolean(KEY_DEV_MODE) ?? false, + ); + const [skipGPGEnabled, setSkipGPGEnabled] = useState( + () => devSettingsMmkv.getBoolean(KEY_SKIP_GPG) ?? false, + ); + const toggleDevMode = useCallback((v: boolean) => { + devSettingsMmkv.set(KEY_DEV_MODE, v); + setDevModeEnabled(v); + }, []); + const toggleSkipGPG = useCallback((v: boolean) => { + devSettingsMmkv.set(KEY_SKIP_GPG, v); + setSkipGPGEnabled(v); + }, []); + + // --- Utility state --- + const [utilExpanded, setUtilExpanded] = useState(false); + const [utilResult, setUtilResult] = useState(null); + const [utilError, setUtilError] = useState(null); + + // --- Cleanup listener on unmount --- + useEffect(() => { + return () => { + if (listenerIdRef.current !== null) { + ReactNativeBundleUpdate.removeDownloadListener(listenerIdRef.current); + } + }; + }, []); + + // --- Helpers --- + const updateStep = useCallback( + (step: StepId, update: Partial) => { + setWorkflow((prev) => ({ + ...prev, + [step]: { ...prev[step], ...update }, + })); + }, + [], + ); + + const resetWorkflow = useCallback(() => { + setWorkflow(INITIAL_WORKFLOW); + progressAnim.setValue(0); + }, [progressAnim]); + + const clearUtil = () => { + setUtilResult(null); + setUtilError(null); + }; + + // --- Step Handlers --- + + const handleDownload = useCallback(async () => { + if (!downloadUrl) { + updateStep('download', { + status: 'error', + errorMessage: 'Please enter a download URL', + }); + return; + } + + // Reset all + setWorkflow({ + ...INITIAL_WORKFLOW, + download: { status: 'active' }, + }); + progressAnim.setValue(0); + + // Register listener for progress + const lid = ReactNativeBundleUpdate.addDownloadListener( + (event: BundleDownloadEvent) => { + if (event.type === 'update/downloading') { + setWorkflow((prev) => ({ + ...prev, + downloadProgress: event.progress, + })); + Animated.timing(progressAnim, { + toValue: event.progress / 100, + duration: 200, + useNativeDriver: false, + }).start(); + } + }, + ); + listenerIdRef.current = lid; + + try { + const result = await ReactNativeBundleUpdate.downloadBundle({ + downloadUrl: defaultParams.downloadUrl, + latestVersion: defaultParams.latestVersion, + bundleVersion: defaultParams.bundleVersion, + fileSize: defaultParams.fileSize, + sha256: defaultParams.sha256, + }); + + setWorkflow((prev) => ({ + ...prev, + download: { status: 'completed' }, + downloadProgress: 100, + downloadResult: result, + })); + Animated.timing(progressAnim, { + toValue: 1, + duration: 300, + useNativeDriver: false, + }).start(); + } catch (err) { + updateStep('download', { + status: 'error', + errorMessage: err instanceof Error ? err.message : 'Download failed', + }); + setWorkflow((prev) => ({ ...prev, downloadProgress: 0 })); + } finally { + if (listenerIdRef.current !== null) { + ReactNativeBundleUpdate.removeDownloadListener(listenerIdRef.current); + listenerIdRef.current = null; + } + } + }, [defaultParams.bundleVersion, defaultParams.downloadUrl, defaultParams.fileSize, defaultParams.latestVersion, defaultParams.sha256, downloadUrl, progressAnim, updateStep]); + + const handleVerify = useCallback(async () => { + const dr = workflow.downloadResult; + if (!dr) return; + updateStep('verify', { status: 'active' }); + try { + await ReactNativeBundleUpdate.verifyBundleASC({ + downloadedFile: dr.downloadedFile, + sha256: dr.sha256, + latestVersion: dr.latestVersion, + bundleVersion: dr.bundleVersion, + signature: defaultParams.signature + }); + updateStep('verify', { status: 'completed' }); + } catch (err) { + updateStep('verify', { + status: 'error', + errorMessage: + err instanceof Error ? err.message : 'Verification failed', + }); + } + }, [workflow.downloadResult, updateStep, defaultParams.signature]); + + const handleInstall = useCallback(async () => { + const dr = workflow.downloadResult; + if (!dr) return; + updateStep('install', { status: 'active' }); + try { + await ReactNativeBundleUpdate.installBundle({ + downloadedFile: dr.downloadedFile, + latestVersion: dr.latestVersion, + bundleVersion: dr.bundleVersion, + signature: defaultParams.signature, + }); + updateStep('install', { status: 'completed' }); + } catch (err) { + updateStep('install', { + status: 'error', + errorMessage: + err instanceof Error ? err.message : 'Installation failed', + }); + } + }, [workflow.downloadResult, updateStep, defaultParams.signature]); + + // --- Utility handlers --- + + const utilGetWebEmbedPath = () => { + clearUtil(); + try { + setUtilResult({ webEmbedPath: ReactNativeBundleUpdate.getWebEmbedPath() || '(empty)' }); + } catch (err) { + setUtilError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + const utilGetJsBundlePath = async () => { + clearUtil(); + try { + setUtilResult({ jsBundlePath: (await ReactNativeBundleUpdate.getJsBundlePath()) || '(empty)' }); + } catch (err) { + setUtilError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + const utilGetNativeAppVersion = async () => { + clearUtil(); + try { + setUtilResult({ nativeAppVersion: await ReactNativeBundleUpdate.getNativeAppVersion() }); + } catch (err) { + setUtilError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + const utilGetFallbackData = async () => { + clearUtil(); + try { + setUtilResult({ fallbackBundleData: await ReactNativeBundleUpdate.getFallbackUpdateBundleData() }); + } catch (err) { + setUtilError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + const utilListLocal = async () => { + clearUtil(); + try { + setUtilResult({ localBundles: await ReactNativeBundleUpdate.listLocalBundles() }); + } catch (err) { + setUtilError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + const utilListAscFiles = async () => { + clearUtil(); + try { + setUtilResult({ ascFiles: await ReactNativeBundleUpdate.listAscFiles() }); + } catch (err) { + setUtilError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + const utilTestVerification = async () => { + clearUtil(); + try { + setUtilResult({ verificationTest: await ReactNativeBundleUpdate.testVerification() }); + } catch (err) { + setUtilError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + const utilClearBundle = async () => { + clearUtil(); + try { + await ReactNativeBundleUpdate.clearBundle(); + setUtilResult({ cleared: true }); + } catch (err) { + setUtilError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + const utilClearAll = () => { + Alert.alert('Confirm', 'Clear ALL JS bundle data?', [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Clear All', + style: 'destructive', + onPress: async () => { + clearUtil(); + try { + setUtilResult({ clearAll: await ReactNativeBundleUpdate.clearAllJSBundleData() }); + } catch (err) { + setUtilError(err instanceof Error ? err.message : 'Unknown error'); + } + }, + }, + ]); + }; + const utilVerifyAllLocal = async () => { + clearUtil(); + try { + const bundles = await ReactNativeBundleUpdate.listLocalBundles(); + if (bundles.length === 0) { + setUtilResult({ message: 'No local bundles found', bundles: [] }); + return; + } + const results: Record = {}; + for (const b of bundles) { + try { + await ReactNativeBundleUpdate.verifyExtractedBundle(b.appVersion, b.bundleVersion); + results[`${b.appVersion}-${b.bundleVersion}`] = 'OK'; + } catch (err) { + results[`${b.appVersion}-${b.bundleVersion}`] = + err instanceof Error ? err.message : 'error'; + } + } + setUtilResult({ verifyResults: results }); + } catch (err) { + setUtilError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + + // --- Render --- + + const showProgress = + workflow.download.status === 'active' || + workflow.download.status === 'completed'; + + return ( + + {/* URL Input */} + + BUNDLE URL + + + + {/* Pipeline */} + + UPDATE PIPELINE + + + {showProgress && ( + + )} + + + + + + + + + + + + {/* Reset */} + + Reset Pipeline + + + {/* Dev Settings */} + + DEVELOPER SETTINGS + + + + Both switches must be ON to skip GPG.{'\n'}Check native logs for:{'\n'} + {' '}GPG check: devSettings=X, skipGPGToggle=Y, skipGPG=Z + + + + {/* Utilities */} + setUtilExpanded(!utilExpanded)} + activeOpacity={0.7} + > + UTILITIES & DEBUG + {utilExpanded ? '▲' : '▼'} + + {utilExpanded && ( + + + + + + + + + + + + + + )} + + ); +} + +// --- Styles --- + +const s = StyleSheet.create({ + // Card + card: { + backgroundColor: C.cardBg, + borderRadius: 12, + padding: 16, + marginBottom: 14, + borderWidth: 1, + borderColor: C.border, + }, + cardTitle: { + fontSize: 11, + fontWeight: '700', + color: C.textSecondary, + letterSpacing: 1.5, + marginBottom: 12, + }, + + // Steps + stepRow: { + marginBottom: 4, + }, + stepHeader: { + flexDirection: 'row', + alignItems: 'center', + }, + stepCircle: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: C.cardBgElevated, + borderWidth: 2, + borderColor: C.border, + alignItems: 'center', + justifyContent: 'center', + }, + stepCircleActive: { + borderColor: C.accentCyan, + backgroundColor: C.accentCyanDim, + }, + stepCircleCompleted: { + borderColor: C.accentCyan, + backgroundColor: C.accentCyan, + }, + stepCircleError: { + borderColor: C.errorRed, + backgroundColor: 'rgba(255,69,58,0.15)', + }, + stepNum: { + fontSize: 14, + fontWeight: '700', + color: C.textTertiary, + }, + stepCheck: { + fontSize: 16, + fontWeight: '700', + color: '#000', + }, + stepBang: { + fontSize: 16, + fontWeight: '700', + color: C.errorRed, + }, + stepInfo: { + flex: 1, + marginLeft: 12, + }, + stepLabel: { + fontSize: 15, + fontWeight: '600', + color: C.textPrimary, + }, + stepLabelDim: { + color: C.textTertiary, + }, + stepSub: { + fontSize: 12, + color: C.accentCyan, + marginTop: 2, + }, + stepBtn: { + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 8, + backgroundColor: C.accentBlue, + minWidth: 80, + alignItems: 'center', + }, + stepBtnDisabled: { + backgroundColor: C.cardBgElevated, + }, + stepBtnDone: { + backgroundColor: 'transparent', + borderWidth: 1, + borderColor: C.accentCyan, + }, + stepBtnText: { + fontSize: 13, + fontWeight: '600', + color: '#fff', + }, + stepBtnTextDisabled: { + color: C.textTertiary, + }, + + // Connector + connectorWrap: { + paddingLeft: 15, + height: 24, + justifyContent: 'center', + }, + connectorLine: { + width: 2, + height: '100%', + backgroundColor: C.border, + }, + connectorLineActive: { + backgroundColor: C.accentCyan, + }, + + // Progress + progressWrap: { + marginTop: 10, + marginLeft: 44, + }, + progressTrack: { + height: 6, + backgroundColor: C.surfaceBg, + borderRadius: 3, + overflow: 'hidden', + }, + progressFill: { + height: '100%', + backgroundColor: C.accentCyan, + borderRadius: 3, + }, + progressText: { + fontSize: 11, + color: C.textSecondary, + marginTop: 4, + fontFamily: mono, + }, + + // Reset + resetBtn: { + alignSelf: 'center', + paddingHorizontal: 20, + paddingVertical: 10, + borderRadius: 8, + borderWidth: 1, + borderColor: C.border, + marginBottom: 14, + }, + resetBtnText: { + fontSize: 13, + fontWeight: '500', + color: C.textSecondary, + }, + + // Toggle + toggleRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 10, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: C.border, + }, + toggleInfo: { + flex: 1, + marginRight: 12, + }, + toggleLabel: { + fontSize: 14, + fontWeight: '500', + color: C.textPrimary, + }, + toggleKey: { + fontSize: 10, + color: C.textTertiary, + fontFamily: mono, + marginTop: 2, + }, + + // Info text + infoText: { + fontSize: 11, + color: C.textTertiary, + fontFamily: mono, + backgroundColor: C.cardBgElevated, + padding: 10, + borderRadius: 6, + marginTop: 10, + lineHeight: 16, + }, + + // Utilities + utilHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 2, + }, + chevron: { + fontSize: 12, + color: C.textSecondary, + }, +}); diff --git a/example/react-native/pages/DeviceUtilsTestPage.tsx b/example/react-native/pages/DeviceUtilsTestPage.tsx index 64da2e7c..d0f54b13 100644 --- a/example/react-native/pages/DeviceUtilsTestPage.tsx +++ b/example/react-native/pages/DeviceUtilsTestPage.tsx @@ -223,6 +223,96 @@ export function DeviceUtilsTestPage({ onGoHome, safeAreaInsets }: DeviceUtilsTes } }; + // LaunchOptionsManager methods + const testGetLaunchOptions = async () => { + clearResults(); + try { + const options = await deviceUtils.getLaunchOptions(); + setResult({ launchOptions: options }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + + const testClearLaunchOptions = async () => { + clearResults(); + try { + const ok = await deviceUtils.clearLaunchOptions(); + setResult({ clearLaunchOptions: ok }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + + const testGetDeviceToken = async () => { + clearResults(); + try { + const token = await deviceUtils.getDeviceToken(); + setResult({ deviceToken: token || '(empty)' }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + + const testRegisterDeviceToken = async () => { + clearResults(); + try { + const ok = await deviceUtils.registerDeviceToken(); + setResult({ registerDeviceToken: ok }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + + const testGetStartupTime = async () => { + clearResults(); + try { + const time = await deviceUtils.getStartupTime(); + setResult({ startupTime: time, startupTimeMs: `${time} ms` }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + + // WebView & Play Services + const testGetWebViewPackageInfo = async () => { + clearResults(); + try { + const info = await deviceUtils.getCurrentWebViewPackageInfo(); + setResult({ webViewPackageInfo: info }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + + const testIsGooglePlayServicesAvailable = async () => { + clearResults(); + try { + const status = await deviceUtils.isGooglePlayServicesAvailable(); + setResult({ googlePlayServices: status }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + + // ExitModule + const testExitApp = () => { + Alert.alert( + 'Confirm', + 'This will terminate the app. Continue?', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Exit', + style: 'destructive', + onPress: () => { + deviceUtils.exitApp(); + }, + }, + ], + ); + }; + return ( + {/* Launch Options */} + + Launch Options + + + + + + + + + + + + + {/* WebView & Play Services */} + + WebView & Play Services + + + + + + + {/* Exit App */} + + Exit App + + + + {/* Results */} - + {/* Instructions */} Instructions: @@ -392,7 +537,12 @@ export function DeviceUtilsTestPage({ onGoHome, safeAreaInsets }: DeviceUtilsTes • Dual Screen: Works best on Surface Duo or devices with external displays{'\n'} • Spanning: Try rotating device or connecting external display{'\n'} • Callback: Register once, then test spanning changes{'\n'} - • Background: Changes system UI colors (status bar, navigation bar) + • Background: Changes system UI colors (status bar, navigation bar){'\n'} + • Launch Options: Shows how the app was launched (notification, deep link, etc.){'\n'} + • Startup Time: Time in ms from native app start{'\n'} + • WebView: Shows installed WebView package info (Android){'\n'} + • Play Services: Checks Google Play Services availability (Android){'\n'} + • Exit App: Terminates the app process (with confirmation) diff --git a/example/react-native/pages/NativeLoggerTestPage.tsx b/example/react-native/pages/NativeLoggerTestPage.tsx new file mode 100644 index 00000000..da9234c8 --- /dev/null +++ b/example/react-native/pages/NativeLoggerTestPage.tsx @@ -0,0 +1,298 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { View, Text, Alert, StyleSheet, Clipboard, TouchableOpacity } from 'react-native'; +import { TestPageBase, TestButton, TestInput, TestResult } from './TestPageBase'; +import { NativeLogger } from '@onekeyfe/react-native-native-logger'; + +interface NativeLoggerTestPageProps { + onGoHome: () => void; + safeAreaInsets: any; +} + +const LogLevel = { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, +} as const; + +export function NativeLoggerTestPage({ onGoHome, safeAreaInsets }: NativeLoggerTestPageProps) { + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [logFiles, setLogFiles] = useState([]); + const [logDir, setLogDir] = useState(''); + const [customMessage, setCustomMessage] = useState('Hello from NativeLogger'); + + const refreshLogFiles = useCallback(async () => { + try { + const paths = await NativeLogger.getLogFilePaths(); + setLogFiles(paths); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + setError(msg); + } + }, []); + + useEffect(() => { + try { + setLogDir(NativeLogger.getLogDirectory()); + } catch (_) { + // ignore + } + refreshLogFiles(); + }, [refreshLogFiles]); + + const executeOperation = async (operation: () => Promise | any, operationName: string) => { + setIsLoading(true); + setError(null); + setResult(null); + try { + const res = await Promise.resolve(operation()); + setResult(res); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + setError(msg); + Alert.alert('Error', `${operationName} failed: ${msg}`); + } finally { + setIsLoading(false); + } + }; + + const writeAllLevels = async () => { + NativeLogger.write(LogLevel.DEBUG, '[Test] Debug message'); + NativeLogger.write(LogLevel.INFO, '[Test] Info message'); + NativeLogger.write(LogLevel.WARN, '[Test] Warning message'); + NativeLogger.write(LogLevel.ERROR, '[Test] Error message'); + setResult('Wrote 4 log messages (debug, info, warn, error)'); + await refreshLogFiles(); + }; + + const writeCustomMessage = async () => { + if (!customMessage.trim()) { + Alert.alert('Error', 'Please enter a message'); + return; + } + NativeLogger.write(LogLevel.INFO, customMessage); + setResult(`Wrote: ${customMessage}`); + await refreshLogFiles(); + }; + + const writeBatchLogs = async () => { + const count = 50; + for (let i = 0; i < count; i++) { + NativeLogger.write(LogLevel.INFO, `[Batch] Message ${i + 1}/${count}`); + } + setResult(`Wrote ${count} batch log messages`); + await refreshLogFiles(); + }; + + const getLogFilePaths = () => { + executeOperation(async () => { + const paths = await NativeLogger.getLogFilePaths(); + setLogFiles(paths); + return { + count: paths.length, + directory: logDir || 'N/A', + files: paths, + }; + }, 'Get Log File Paths'); + }; + + const deleteLogFiles = () => { + Alert.alert('Confirm', 'Delete all log files?', [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Delete', + style: 'destructive', + onPress: () => { + executeOperation(async () => { + await NativeLogger.deleteLogFiles(); + await refreshLogFiles(); + return 'All log files deleted'; + }, 'Delete Log Files'); + }, + }, + ]); + }; + + return ( + + {/* Log Directory */} + + Log Directory + + + Path: + {logDir ? ( + { + Clipboard.setString(logDir); + Alert.alert('Copied', 'Log directory path copied to clipboard'); + }} + style={styles.copyButton} + > + Copy + + ) : null} + + + {logDir || 'Loading...'} + + + + + Files ({logFiles.length}): + + {logFiles.length === 0 ? ( + No log files yet + ) : ( + logFiles.map((name, index) => ( + + {name} + + )) + )} + + + + {/* Write Logs */} + + Write Logs + + + + + + + + + {/* File Operations */} + + File Operations + + + + + + + + + {/* Documentation */} + + + NativeLogger API:{'\n'} + {'• '}write(level, msg): Write log (0=debug, 1=info, 2=warn, 3=error){'\n'} + {'• '}getLogFilePaths(): Get all log file paths{'\n'} + {'• '}deleteLogFiles(): Delete all log files{'\n'} + {'\n'} + Log File Convention:{'\n'} + {'• '}Active: app-latest.log{'\n'} + {'• '}Archived: app-yyyy-MM-dd.i.log (max 6){'\n'} + {'• '}Max file size: 20 MB, daily rolling{'\n'} + {'• '}Directory: {''}/logs/ + + + + ); +} + +const styles = StyleSheet.create({ + section: { + gap: 5, + }, + sectionTitle: { + fontSize: 16, + fontWeight: '600', + color: '#333', + marginBottom: 5, + }, + dirBox: { + backgroundColor: '#fff', + padding: 12, + borderRadius: 8, + }, + dirHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 4, + }, + dirLabel: { + fontSize: 13, + fontWeight: '600', + color: '#333', + }, + copyButton: { + backgroundColor: '#007AFF', + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 4, + }, + copyButtonText: { + fontSize: 12, + color: '#fff', + fontWeight: '600', + }, + dirPath: { + fontSize: 12, + fontFamily: 'Courier New', + color: '#007AFF', + }, + fileList: { + backgroundColor: '#fff', + padding: 12, + borderRadius: 8, + marginTop: 6, + }, + fileName: { + fontSize: 12, + fontFamily: 'Courier New', + color: '#666', + marginBottom: 2, + }, + noFiles: { + fontSize: 13, + color: '#999', + fontStyle: 'italic', + }, + docSection: { + marginTop: 10, + padding: 15, + backgroundColor: '#fff', + borderRadius: 8, + }, + docText: { + fontSize: 14, + color: '#666', + lineHeight: 20, + }, +}); diff --git a/example/react-native/pages/PerfMemoryTestPage.tsx b/example/react-native/pages/PerfMemoryTestPage.tsx new file mode 100644 index 00000000..2f6828d2 --- /dev/null +++ b/example/react-native/pages/PerfMemoryTestPage.tsx @@ -0,0 +1,132 @@ +import { useState, useRef, useEffect } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { TestPageBase, TestButton, TestResult } from './TestPageBase'; +import { ReactNativePerfMemory } from '@onekeyfe/react-native-perf-memory'; + +interface PerfMemoryTestPageProps { + onGoHome: () => void; + safeAreaInsets: any; +} + +export function PerfMemoryTestPage({ onGoHome, safeAreaInsets }: PerfMemoryTestPageProps) { + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [isPolling, setIsPolling] = useState(false); + const intervalRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, []); + + const clearResults = () => { + setResult(null); + setError(null); + }; + + const testGetMemoryUsage = async () => { + clearResults(); + try { + const memory = await ReactNativePerfMemory.getMemoryUsage(); + setResult({ + rss: memory.rss, + rssMB: `${(memory.rss / 1024 / 1024).toFixed(2)} MB`, + }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + + const togglePolling = () => { + if (isPolling) { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + setIsPolling(false); + } else { + setIsPolling(true); + const poll = async () => { + try { + const memory = await ReactNativePerfMemory.getMemoryUsage(); + setResult({ + rss: memory.rss, + rssMB: `${(memory.rss / 1024 / 1024).toFixed(2)} MB`, + timestamp: new Date().toLocaleTimeString(), + }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + void poll(); + intervalRef.current = setInterval(() => void poll(), 1000); + } + }; + + return ( + + + Memory Usage + + + + + + + + + + Instructions: + + • Get Memory Usage: reads the process RSS (Resident Set Size){'\n'} + • Polling: continuously reads memory every second{'\n'} + • RSS is reported in bytes and converted to MB + + + + ); +} + +const styles = StyleSheet.create({ + section: { + marginBottom: 20, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '600', + color: '#333', + marginBottom: 10, + }, + instructionsContainer: { + backgroundColor: '#fff3cd', + padding: 15, + borderRadius: 8, + marginTop: 20, + borderLeftWidth: 4, + borderLeftColor: '#ffc107', + }, + instructionsTitle: { + fontSize: 16, + fontWeight: '600', + color: '#856404', + marginBottom: 8, + }, + instructionsText: { + fontSize: 14, + color: '#856404', + lineHeight: 20, + }, +}); diff --git a/example/react-native/pages/SplashScreenTestPage.tsx b/example/react-native/pages/SplashScreenTestPage.tsx new file mode 100644 index 00000000..8fc78523 --- /dev/null +++ b/example/react-native/pages/SplashScreenTestPage.tsx @@ -0,0 +1,97 @@ +import { useState } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { TestPageBase, TestButton, TestResult } from './TestPageBase'; +import { ReactNativeSplashScreen } from '@onekeyfe/react-native-splash-screen'; + +interface SplashScreenTestPageProps { + onGoHome: () => void; + safeAreaInsets: any; +} + +export function SplashScreenTestPage({ onGoHome, safeAreaInsets }: SplashScreenTestPageProps) { + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + const clearResults = () => { + setResult(null); + setError(null); + }; + + const testPreventAutoHide = async () => { + clearResults(); + try { + const ok = await ReactNativeSplashScreen.preventAutoHideAsync(); + setResult({ preventAutoHide: ok }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + + const testHide = async () => { + clearResults(); + try { + const ok = await ReactNativeSplashScreen.hideAsync(); + setResult({ hide: ok }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } + }; + + return ( + + + Legacy Splash Screen (Android < 12) + + + + + + + + + Instructions: + + • preventAutoHideAsync: keeps the splash screen visible{'\n'} + • hideAsync: hides the splash screen{'\n'} + • Only active on Android < 12 (older devices){'\n'} + • On iOS, both methods return true (no-op) + + + + ); +} + +const styles = StyleSheet.create({ + section: { + marginBottom: 20, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '600', + color: '#333', + marginBottom: 10, + }, + instructionsContainer: { + backgroundColor: '#fff3cd', + padding: 15, + borderRadius: 8, + marginTop: 20, + borderLeftWidth: 4, + borderLeftColor: '#ffc107', + }, + instructionsTitle: { + fontSize: 16, + fontWeight: '600', + color: '#856404', + marginBottom: 8, + }, + instructionsText: { + fontSize: 14, + color: '#856404', + lineHeight: 20, + }, +}); diff --git a/example/react-native/route.tsx b/example/react-native/route.tsx index e7fe9119..872f1af6 100644 --- a/example/react-native/route.tsx +++ b/example/react-native/route.tsx @@ -8,23 +8,38 @@ import { LiteCardTestPage } from './pages/LiteCardTestPage'; import { GetRandomValuesTestPage } from './pages/GetRandomValuesTestPage'; import { DeviceUtilsTestPage } from './pages/DeviceUtilsTestPage'; import { SkeletonTestPage } from './pages/SkeletonTestPage'; +import { NativeLoggerTestPage } from './pages/NativeLoggerTestPage'; +import { PerfMemoryTestPage } from './pages/PerfMemoryTestPage'; +import { BundleUpdateTestPage } from './pages/BundleUpdateTestPage'; +import { AppUpdateTestPage } from './pages/AppUpdateTestPage'; +import { SplashScreenTestPage } from './pages/SplashScreenTestPage'; -export type RouteScreen = +export type RouteScreen = | 'home' + | 'app-update' | 'background-thread' | 'biometric-auth' + | 'bundle-update' | 'cloud-kit' | 'keychain' | 'lite-card' | 'get-random-values' | 'device-utils' - | 'skeleton'; + | 'native-logger' + | 'perf-memory' + | 'skeleton' + | 'splash-screen'; interface RouterProps { safeAreaInsets: any; } const modules = [ + { + id: 'app-update' as RouteScreen, + name: 'App Update', + description: 'APK download, verification, and installation (Android)', + }, { id: 'background-thread' as RouteScreen, name: 'Background Thread', @@ -35,6 +50,11 @@ const modules = [ name: 'Biometric Auth Changed', description: 'Check if biometric authentication has changed', }, + { + id: 'bundle-update' as RouteScreen, + name: 'Bundle Update', + description: 'JS bundle download, verification, install, and path management', + }, { id: 'cloud-kit' as RouteScreen, name: 'CloudKit Module', @@ -60,11 +80,26 @@ const modules = [ name: 'Get Random Values', description: 'Generate cryptographically secure random values', }, + { + id: 'native-logger' as RouteScreen, + name: 'Native Logger', + description: 'File-based logging with log directory viewer', + }, + { + id: 'perf-memory' as RouteScreen, + name: 'Perf Memory', + description: 'Read process memory usage (RSS) for performance monitoring', + }, { id: 'skeleton' as RouteScreen, name: 'Skeleton View', description: 'Animated skeleton loading components for better UX', }, + { + id: 'splash-screen' as RouteScreen, + name: 'Splash Screen', + description: 'Legacy splash screen for Android < 12', + }, ]; export function Router({ safeAreaInsets }: RouterProps) { @@ -133,10 +168,14 @@ export function Router({ safeAreaInsets }: RouterProps) { }; switch (currentScreen) { + case 'app-update': + return ; case 'background-thread': return ; case 'biometric-auth': return ; + case 'bundle-update': + return ; case 'cloud-kit': return ; case 'device-utils': @@ -147,8 +186,14 @@ export function Router({ safeAreaInsets }: RouterProps) { return ; case 'get-random-values': return ; + case 'native-logger': + return ; + case 'perf-memory': + return ; case 'skeleton': return ; + case 'splash-screen': + return ; default: return renderHomeScreen(); } diff --git a/native-modules/native-logger/.gitignore b/native-modules/native-logger/.gitignore new file mode 100644 index 00000000..357a2f9a --- /dev/null +++ b/native-modules/native-logger/.gitignore @@ -0,0 +1,89 @@ +# OSX +# +.DS_Store + +# XDE +.expo/ + +# VSCode +.vscode/ +jsconfig.json + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace +**/.xcode.env.local + +# Android/IJ +# +.classpath +.cxx +.gradle +.idea +.project +.settings +local.properties +android.iml + +# Cocoapods +# +example/ios/Pods + +# Ruby +example/vendor/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +android/app/libs +android/keystores/debug.keystore + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Expo +.expo/ + +# Turborepo +.turbo/ + +# generated by bob +lib/ + +# React Native Codegen +ios/generated +android/generated + +# React Native Nitro Modules +nitrogen/ + + + diff --git a/native-modules/native-logger/LICENSE b/native-modules/native-logger/LICENSE new file mode 100644 index 00000000..24ee4114 --- /dev/null +++ b/native-modules/native-logger/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 OneKey + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/native-modules/native-logger/ReactNativeNativeLogger.podspec b/native-modules/native-logger/ReactNativeNativeLogger.podspec new file mode 100644 index 00000000..eeda72e2 --- /dev/null +++ b/native-modules/native-logger/ReactNativeNativeLogger.podspec @@ -0,0 +1,29 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "ReactNativeNativeLogger" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => min_ios_version_supported } + s.source = { :git => "https://github.com/OneKeyHQ/app-modules/native-logger.git", :tag => "#{s.version}" } + + s.source_files = [ + "ios/**/*.{swift}", + "cpp/**/*.{hpp,cpp}", + ] + + s.dependency 'React-jsi' + s.dependency 'React-callinvoker' + s.dependency 'CocoaLumberjack/Swift', '~> 3.8' + + load 'nitrogen/generated/ios/ReactNativeNativeLogger+autolinking.rb' + add_nitrogen_files(s) + + install_modules_dependencies(s) +end diff --git a/native-modules/native-logger/android/CMakeLists.txt b/native-modules/native-logger/android/CMakeLists.txt new file mode 100644 index 00000000..6b2ca68e --- /dev/null +++ b/native-modules/native-logger/android/CMakeLists.txt @@ -0,0 +1,24 @@ +project(nativelogger) +cmake_minimum_required(VERSION 3.9.0) + +set(PACKAGE_NAME nativelogger) +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_CXX_STANDARD 20) + +# Define C++ library and add all sources +add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp) + +# Add Nitrogen specs :) +include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/nativelogger+autolinking.cmake) + +# Set up local includes +include_directories("src/main/cpp" "../cpp") + +find_library(LOG_LIB log) + +# Link all libraries together +target_link_libraries( + ${PACKAGE_NAME} + ${LOG_LIB} + android # <-- Android core +) diff --git a/native-modules/native-logger/android/build.gradle b/native-modules/native-logger/android/build.gradle new file mode 100644 index 00000000..6f81ee07 --- /dev/null +++ b/native-modules/native-logger/android/build.gradle @@ -0,0 +1,133 @@ +buildscript { + ext.getExtOrDefault = {name -> + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['NativeLogger_' + name] + } + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:8.7.2" + // noinspection DifferentKotlinGradleVersion + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}" + } +} + +def reactNativeArchitectures() { + def value = rootProject.getProperties().get("reactNativeArchitectures") + return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" +apply from: '../nitrogen/generated/android/nativelogger+autolinking.gradle' + +apply plugin: "com.facebook.react" + +def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["NativeLogger_" + name]).toInteger() +} + +android { + namespace "com.margelo.nitro.nativelogger" + + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + + defaultConfig { + minSdkVersion getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + + externalNativeBuild { + cmake { + cppFlags "-frtti -fexceptions -Wall -fstack-protector-all" + arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" + abiFilters (*reactNativeArchitectures()) + + buildTypes { + debug { + cppFlags "-O1 -g" + } + release { + cppFlags "-O2" + } + } + } + } + } + + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } + } + + packagingOptions { + excludes = [ + "META-INF", + "META-INF/**", + "**/libc++_shared.so", + "**/libfbjni.so", + "**/libjsi.so", + "**/libfolly_json.so", + "**/libfolly_runtime.so", + "**/libglog.so", + "**/libhermes.so", + "**/libhermes-executor-debug.so", + "**/libhermes_executor.so", + "**/libreactnative.so", + "**/libreactnativejni.so", + "**/libturbomodulejsijni.so", + "**/libreact_nativemodule_core.so", + "**/libjscexecutor.so" + ] + } + + buildFeatures { + buildConfig true + prefab true + } + + buildTypes { + release { + minifyEnabled false + consumerProguardFiles 'consumer-rules.pro' + } + } + + lintOptions { + disable "GradleCompatible" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs += [ + "generated/java", + "generated/jni" + ] + } + } +} + +repositories { + mavenCentral() + google() +} + +def kotlin_version = getExtOrDefault("kotlinVersion") + +dependencies { + implementation "com.facebook.react:react-android" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation project(":react-native-nitro-modules") + + // Logging backend — use `api` so consuming modules get SLF4J transitively + api 'org.slf4j:slf4j-api:1.7.33' + api 'com.github.tony19:logback-android:2.0.0' +} diff --git a/native-modules/native-logger/android/consumer-rules.pro b/native-modules/native-logger/android/consumer-rules.pro new file mode 100644 index 00000000..f2bae43b --- /dev/null +++ b/native-modules/native-logger/android/consumer-rules.pro @@ -0,0 +1,5 @@ +# Logback-android: keep classes required by logback XML configuration +-keep class ch.qos.logback.** { *; } +-keep class org.slf4j.** { *; } +-dontwarn ch.qos.logback.** +-dontwarn org.slf4j.** diff --git a/native-modules/native-logger/android/gradle.properties b/native-modules/native-logger/android/gradle.properties new file mode 100644 index 00000000..f1fdab2d --- /dev/null +++ b/native-modules/native-logger/android/gradle.properties @@ -0,0 +1,4 @@ +NativeLogger_kotlinVersion=1.9.25 +NativeLogger_compileSdkVersion=35 +NativeLogger_targetSdkVersion=35 +NativeLogger_minSdkVersion=24 diff --git a/native-modules/native-logger/android/src/main/AndroidManifest.xml b/native-modules/native-logger/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c1d23f13 --- /dev/null +++ b/native-modules/native-logger/android/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/native-modules/native-logger/android/src/main/cpp/cpp-adapter.cpp b/native-modules/native-logger/android/src/main/cpp/cpp-adapter.cpp new file mode 100644 index 00000000..c95da3af --- /dev/null +++ b/native-modules/native-logger/android/src/main/cpp/cpp-adapter.cpp @@ -0,0 +1,6 @@ +#include +#include "nativeloggerOnLoad.hpp" + +extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { + return margelo::nitro::nativelogger::initialize(vm); +} diff --git a/native-modules/native-logger/android/src/main/java/com/margelo/nitro/nativelogger/NativeLogger.kt b/native-modules/native-logger/android/src/main/java/com/margelo/nitro/nativelogger/NativeLogger.kt new file mode 100644 index 00000000..1f4f36e9 --- /dev/null +++ b/native-modules/native-logger/android/src/main/java/com/margelo/nitro/nativelogger/NativeLogger.kt @@ -0,0 +1,106 @@ +package com.margelo.nitro.nativelogger + +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.Promise +import java.io.File + +@DoNotStrip +class NativeLogger : HybridNativeLoggerSpec() { + + companion object { + /** Patterns that should never be written to log files */ + private val sensitivePatterns = listOf( + // Hex-encoded private keys (64 hex chars), with optional 0x prefix + Regex("(?:0x)?[0-9a-fA-F]{64}"), + // WIF private keys (base58, starting with 5, K, or L) + Regex("\\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\\b"), + // Extended keys (xprv/xpub/zprv/zpub/yprv/ypub) + Regex("\\b[xyzXYZ](?:prv|pub)[1-9A-HJ-NP-Za-km-z]{107,108}\\b"), + // BIP39 mnemonic-like sequences (12+ words of 3-8 lowercase letters) + Regex("(?:\\b[a-z]{3,8}\\b[\\s,]+){11,}\\b[a-z]{3,8}\\b"), + // Bearer/API tokens + Regex("(?:Bearer|token[=:]?)\\s*[A-Za-z0-9_.\\-+/=]{20,}"), + // Base64 encoded data that looks like keys (44+ chars) + Regex("(?:eyJ|AAAA)[A-Za-z0-9+/=]{40,}"), + ) + + /** Rate limiting: max messages per second */ + private const val MAX_MESSAGES_PER_SECOND = 100 + @Volatile private var messageCount = 0 + @Volatile private var windowStartMs = System.currentTimeMillis() + private val rateLimitLock = Any() + + private fun isRateLimited(): Boolean { + synchronized(rateLimitLock) { + val now = System.currentTimeMillis() + if (now - windowStartMs >= 1000L) { + windowStartMs = now + messageCount = 0 + } + messageCount++ + return messageCount > MAX_MESSAGES_PER_SECOND + } + } + + fun sanitize(message: String): String { + var result = message + for (pattern in sensitivePatterns) { + result = pattern.replace(result, "[REDACTED]") + } + // Strip newlines to prevent log injection + result = result.replace("\n", " ").replace("\r", " ") + return result + } + } + + override fun write(level: Double, msg: String) { + if (isRateLimited()) return + val sanitized = sanitize(msg) + when (level.toInt()) { + 0 -> OneKeyLog.debug("JS", sanitized) + 1 -> OneKeyLog.info("JS", sanitized) + 2 -> OneKeyLog.warn("JS", sanitized) + 3 -> OneKeyLog.error("JS", sanitized) + else -> OneKeyLog.info("JS", sanitized) + } + } + + override fun getLogDirectory(): String { + return OneKeyLog.logsDirectory + } + + override fun getLogFilePaths(): Promise> { + return Promise.async { + // Flush buffered log data so the active file reflects all written logs + OneKeyLog.flush() + val dir = OneKeyLog.logsDirectory + if (dir.isEmpty()) return@async arrayOf() + val files = File(dir).listFiles { _, name -> name.endsWith(".log") } + if (files == null) { + OneKeyLog.warn("NativeLogger", "Failed to list log directory") + return@async arrayOf() + } + // Return filenames only, not absolute paths + files.sortedBy { it.name } + .map { it.name }.toTypedArray() + } + } + + override fun deleteLogFiles(): Promise { + return Promise.async { + val dir = OneKeyLog.logsDirectory + if (dir.isEmpty()) return@async + val files = File(dir).listFiles { _, name -> name.endsWith(".log") } + if (files == null) { + OneKeyLog.warn("NativeLogger", "Failed to list log directory for deletion") + return@async + } + // Skip the active log file to avoid breaking logback's open file handle + files.filter { it.name != "app-latest.log" }.forEach { file -> + if (!file.delete()) { + OneKeyLog.warn("NativeLogger", "Failed to delete log file") + } + } + } + } +} diff --git a/native-modules/native-logger/android/src/main/java/com/margelo/nitro/nativelogger/NativeLoggerPackage.kt b/native-modules/native-logger/android/src/main/java/com/margelo/nitro/nativelogger/NativeLoggerPackage.kt new file mode 100644 index 00000000..cb3abd32 --- /dev/null +++ b/native-modules/native-logger/android/src/main/java/com/margelo/nitro/nativelogger/NativeLoggerPackage.kt @@ -0,0 +1,24 @@ +package com.margelo.nitro.nativelogger + +import com.facebook.react.BaseReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfoProvider + +class NativeLoggerPackage : BaseReactPackage() { + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return null + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { HashMap() } + } + + companion object { + init { + System.loadLibrary("nativelogger") + } + } +} + + diff --git a/native-modules/native-logger/android/src/main/java/com/margelo/nitro/nativelogger/OneKeyLog.kt b/native-modules/native-logger/android/src/main/java/com/margelo/nitro/nativelogger/OneKeyLog.kt new file mode 100644 index 00000000..96eb8a64 --- /dev/null +++ b/native-modules/native-logger/android/src/main/java/com/margelo/nitro/nativelogger/OneKeyLog.kt @@ -0,0 +1,205 @@ +package com.margelo.nitro.nativelogger + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.LoggerContext +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.rolling.RollingFileAppender +import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy +import ch.qos.logback.core.util.FileSize +import com.margelo.nitro.NitroModules +import java.io.File +import java.nio.charset.Charset +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import java.util.Locale + +object OneKeyLog { + private const val APPENDER_NAME = "OneKeyFileAppender" + private const val LOG_PREFIX = "app" + private const val MAX_MESSAGE_LENGTH = 4096 + private const val MAX_FILE_SIZE = 20L * 1024 * 1024 // 20 MB + private const val MAX_HISTORY = 6 + private const val TOTAL_SIZE_CAP = MAX_FILE_SIZE * MAX_HISTORY + + // Cached value; empty string means context was not yet available (will retry) + @Volatile + private var cachedLogsDir: String? = null + + /** + * Initialise OneKeyLog with an Android Context before NitroModules is ready. + * Call this early in Application.onCreate() so that logs emitted before + * React Native loads are not silently dropped. + */ + @JvmStatic + fun init(context: android.content.Context) { + if (cachedLogsDir == null) { + cachedLogsDir = "${context.cacheDir.absolutePath}/logs" + } + } + + val logsDirectory: String + get() { + cachedLogsDir?.let { return it } + val context = NitroModules.applicationContext + if (context == null) { + android.util.Log.w("OneKeyLog", "applicationContext not yet available") + return "" // Don't cache — allow retry on next access + } + val dir = "${context.cacheDir.absolutePath}/logs" + cachedLogsDir = dir + return dir + } + + // Cached logger; null means not yet initialized (will retry). + // Once successfully initialized, stays non-null. + @Volatile + private var cachedLogger: Logger? = null + + private val logger: Logger? + get() { + cachedLogger?.let { return it } + return synchronized(this) { + cachedLogger?.let { return@synchronized it } + val dir = logsDirectory + if (dir.isEmpty()) return@synchronized null + + File(dir).mkdirs() + + val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext + + val appender = RollingFileAppender().apply { + context = loggerContext + name = APPENDER_NAME + file = "$dir/$LOG_PREFIX-latest.log" + } + + SizeAndTimeBasedRollingPolicy().apply { + context = loggerContext + fileNamePattern = "$dir/$LOG_PREFIX-%d{yyyy-MM-dd}.%i.log" + setMaxFileSize(FileSize(MAX_FILE_SIZE)) + maxHistory = MAX_HISTORY + setTotalSizeCap(FileSize(TOTAL_SIZE_CAP)) + setParent(appender) + start() + appender.rollingPolicy = this + } + + PatternLayoutEncoder().apply { + context = loggerContext + charset = Charset.forName("UTF-8") + pattern = "%msg%n" + start() + appender.encoder = this + } + + appender.start() + + // Attach appender to a dedicated named logger only (not ROOT) + // to avoid capturing third-party library SLF4J output + val onekeyLogger = loggerContext.getLogger("OneKey") + onekeyLogger.level = Level.DEBUG + onekeyLogger.getAppender(APPENDER_NAME)?.stop() + onekeyLogger.detachAppender(APPENDER_NAME) + onekeyLogger.addAppender(appender) + onekeyLogger.isAdditive = false // Do not propagate to ROOT logger + + cachedLogger = onekeyLogger + onekeyLogger + } + } + + // DateTimeFormatter is immutable and thread-safe (unlike SimpleDateFormat) + private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss", Locale.US) + + private fun truncate(message: String): String { + return if (message.length > MAX_MESSAGE_LENGTH) { + message.substring(0, MAX_MESSAGE_LENGTH) + "...(truncated)" + } else { + message + } + } + + private fun sanitizeForLog(str: String): String { + return str.replace("\n", " ").replace("\r", " ") + } + + /** Sanitize sensitive data from all log messages (native and JS) */ + private val nativeSensitivePatterns = listOf( + Regex("(?:0x)?[0-9a-fA-F]{64}"), + Regex("\\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\\b"), + Regex("\\b[xyzXYZ](?:prv|pub)[1-9A-HJ-NP-Za-km-z]{107,108}\\b"), + Regex("(?:\\b[a-z]{3,8}\\b[\\s,]+){11,}\\b[a-z]{3,8}\\b"), + Regex("(?:Bearer|token[=:]?)\\s*[A-Za-z0-9_.\\-+/=]{20,}"), + Regex("(?:eyJ|AAAA)[A-Za-z0-9+/=]{40,}"), + ) + + private fun sanitizeSensitive(message: String): String { + var result = message + for (pattern in nativeSensitivePatterns) { + result = pattern.replace(result, "[REDACTED]") + } + return result + } + + private fun formatMessage(tag: String, level: String, message: String): String { + val safeTag = sanitizeForLog(tag.take(64)) + val safeMessage = sanitizeSensitive(sanitizeForLog(message)) + if (safeTag == "JS") { + return truncate(safeMessage) + } + val time = LocalTime.now().format(timeFormatter) + return truncate("$time | $level : [$safeTag] $safeMessage") + } + + private fun log(tag: String, level: String, message: String, androidLogLevel: Int) { + val formatted = formatMessage(tag, level, message) + val l = logger + if (l != null) { + when (androidLogLevel) { + android.util.Log.DEBUG -> l.debug(formatted) + android.util.Log.INFO -> l.info(formatted) + android.util.Log.WARN -> l.warn(formatted) + android.util.Log.ERROR -> l.error(formatted) + } + } else { + // Fallback to android.util.Log when file logger is unavailable + // Use formatted (sanitized) message, not raw message + android.util.Log.println(androidLogLevel, "OneKey/${tag.take(64)}", formatted) + } + } + + /** + * Flush all buffered log data to disk. + * Call before reading log files to ensure the active file is up-to-date. + */ + @JvmStatic + fun flush() { + val loggerContext = LoggerFactory.getILoggerFactory() + if (loggerContext is LoggerContext) { + val appender = loggerContext.getLogger("OneKey") + ?.getAppender(APPENDER_NAME) + if (appender is RollingFileAppender<*>) { + try { + appender.outputStream?.flush() + } catch (_: Exception) { + // Ignore flush errors + } + } + } + } + + @JvmStatic + fun debug(tag: String, message: String) { log(tag, "DEBUG", message, android.util.Log.DEBUG) } + + @JvmStatic + fun info(tag: String, message: String) { log(tag, "INFO", message, android.util.Log.INFO) } + + @JvmStatic + fun warn(tag: String, message: String) { log(tag, "WARN", message, android.util.Log.WARN) } + + @JvmStatic + fun error(tag: String, message: String) { log(tag, "ERROR", message, android.util.Log.ERROR) } +} diff --git a/native-modules/native-logger/android/src/main/java/com/margelo/nitro/nativelogger/OneKeyLogInitProvider.kt b/native-modules/native-logger/android/src/main/java/com/margelo/nitro/nativelogger/OneKeyLogInitProvider.kt new file mode 100644 index 00000000..7820aa35 --- /dev/null +++ b/native-modules/native-logger/android/src/main/java/com/margelo/nitro/nativelogger/OneKeyLogInitProvider.kt @@ -0,0 +1,27 @@ +package com.margelo.nitro.nativelogger + +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.net.Uri + +/** + * Auto-initialises [OneKeyLog] before Application.onCreate(). + * + * ContentProvider.onCreate() is invoked by the system between + * Application.attachBaseContext() and Application.onCreate(), + * so the logger is ready for the earliest app-level code. + */ +class OneKeyLogInitProvider : ContentProvider() { + + override fun onCreate(): Boolean { + context?.let { OneKeyLog.init(it) } + return true + } + + override fun query(uri: Uri, proj: Array?, sel: String?, selArgs: Array?, sort: String?): Cursor? = null + override fun getType(uri: Uri): String? = null + override fun insert(uri: Uri, values: ContentValues?): Uri? = null + override fun delete(uri: Uri, sel: String?, selArgs: Array?): Int = 0 + override fun update(uri: Uri, values: ContentValues?, sel: String?, selArgs: Array?): Int = 0 +} diff --git a/native-modules/native-logger/babel.config.js b/native-modules/native-logger/babel.config.js new file mode 100644 index 00000000..06a81f19 --- /dev/null +++ b/native-modules/native-logger/babel.config.js @@ -0,0 +1,13 @@ +module.exports = { + presets: ['@react-native/babel-preset'], + plugins: [ + [ + 'module-resolver', + { + alias: { + 'native-logger': './src/index', + }, + }, + ], + ], +}; diff --git a/native-modules/native-logger/eslint.config.mjs b/native-modules/native-logger/eslint.config.mjs new file mode 100644 index 00000000..3416cf5c --- /dev/null +++ b/native-modules/native-logger/eslint.config.mjs @@ -0,0 +1,5 @@ +import { createEslintConfig } from '@react-native/eslint-config'; + +export default createEslintConfig({ + extends: ['@react-native/eslint-config'], +}); diff --git a/native-modules/native-logger/ios/NativeLogger.swift b/native-modules/native-logger/ios/NativeLogger.swift new file mode 100644 index 00000000..00851802 --- /dev/null +++ b/native-modules/native-logger/ios/NativeLogger.swift @@ -0,0 +1,115 @@ +import NitroModules +import CocoaLumberjack + +class NativeLogger: HybridNativeLoggerSpec { + + /// Patterns that should never be written to log files + private static let sensitivePatterns: [NSRegularExpression] = { + let patterns = [ + // Hex-encoded private keys (64 hex chars), with optional 0x prefix + "(?:0x)?[0-9a-fA-F]{64}", + // WIF private keys (base58, starting with 5, K, or L) + "\\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\\b", + // Extended keys (xprv/xpub/zprv/zpub/yprv/ypub) + "\\b[xyzXYZ](?:prv|pub)[1-9A-HJ-NP-Za-km-z]{107,108}\\b", + // BIP39 mnemonic-like sequences (12+ words of 3-8 lowercase letters) + "(?:\\b[a-z]{3,8}\\b[\\s,]+){11,}\\b[a-z]{3,8}\\b", + // Bearer/API tokens + "(?:Bearer|token[=:]?)\\s*[A-Za-z0-9_.\\-+/=]{20,}", + // Base64 encoded data that looks like keys (44+ chars) + "(?:eyJ|AAAA)[A-Za-z0-9+/=]{40,}", + ] + return patterns.compactMap { try? NSRegularExpression(pattern: $0) } + }() + + /// Rate limiting: max messages per second + private static let maxMessagesPerSecond = 100 + private static var messageCount = 0 + private static var windowStart = Date() + private static let rateLimitLock = NSLock() + + private static func isRateLimited() -> Bool { + rateLimitLock.lock() + defer { rateLimitLock.unlock() } + let now = Date() + if now.timeIntervalSince(windowStart) >= 1.0 { + windowStart = now + messageCount = 0 + } + messageCount += 1 + return messageCount > maxMessagesPerSecond + } + + private static func sanitize(_ message: String) -> String { + var result = message + for regex in sensitivePatterns { + result = regex.stringByReplacingMatches( + in: result, + range: NSRange(result.startIndex..., in: result), + withTemplate: "[REDACTED]" + ) + } + // Strip newlines to prevent log injection + result = result.replacingOccurrences(of: "\n", with: " ") + result = result.replacingOccurrences(of: "\r", with: " ") + return result + } + + func write(level: Double, msg: String) { + if NativeLogger.isRateLimited() { return } + let sanitized = NativeLogger.sanitize(msg) + switch Int(level) { + case 0: OneKeyLog.debug("JS", sanitized) + case 1: OneKeyLog.info("JS", sanitized) + case 2: OneKeyLog.warn("JS", sanitized) + case 3: OneKeyLog.error("JS", sanitized) + default: OneKeyLog.info("JS", sanitized) + } + } + + func getLogDirectory() throws -> String { + return OneKeyLog.logsDirectory + } + + func getLogFilePaths() throws -> Promise<[String]> { + return Promise.async { + // Flush buffered log data so the active file reflects all written logs + DDLog.flushLog() + let dir = OneKeyLog.logsDirectory + let fm = FileManager.default + let files: [String] + do { + files = try fm.contentsOfDirectory(atPath: dir) + } catch { + OneKeyLog.warn("NativeLogger", "Failed to list log directory") + return [] + } + // Return filenames only, not absolute paths + return files + .filter { $0.hasSuffix(".log") } + .sorted() + } + } + + func deleteLogFiles() throws -> Promise { + return Promise.async { + let dir = OneKeyLog.logsDirectory + let fm = FileManager.default + let files: [String] + do { + files = try fm.contentsOfDirectory(atPath: dir) + } catch { + OneKeyLog.warn("NativeLogger", "Failed to list log directory for deletion") + return + } + // Skip the active log file to avoid breaking CocoaLumberjack's open file handle + for file in files where file.hasSuffix(".log") && file != "app-latest.log" { + do { + try fm.removeItem(atPath: "\(dir)/\(file)") + } catch { + OneKeyLog.warn("NativeLogger", "Failed to delete log file") + } + } + } + } +} diff --git a/native-modules/native-logger/ios/OneKeyLog.swift b/native-modules/native-logger/ios/OneKeyLog.swift new file mode 100644 index 00000000..3d173442 --- /dev/null +++ b/native-modules/native-logger/ios/OneKeyLog.swift @@ -0,0 +1,202 @@ +import CocoaLumberjack + +private let ddLogLevel: DDLogLevel = .debug + +private class OneKeyLogFileManager: DDLogFileManagerDefault { + private static let logPrefix = "app" + private static let latestFileName = "\(logPrefix)-latest.log" + private static let dateFormatterLock = NSLock() + private static let _dateFormatter: DateFormatter = { + let fmt = DateFormatter() + fmt.dateFormat = "yyyy-MM-dd" + fmt.locale = Locale(identifier: "en_US_POSIX") + return fmt + }() + + private static func formattedDate(_ date: Date) -> String { + dateFormatterLock.lock() + defer { dateFormatterLock.unlock() } + return _dateFormatter.string(from: date) + } + + /// Always write to app-latest.log (matches Android behavior) + override var newLogFileName: String { + return Self.latestFileName + } + + override func isLogFile(withName fileName: String) -> Bool { + return fileName.hasPrefix(Self.logPrefix) && fileName.hasSuffix(".log") + } + + /// When rolled, rename app-latest.log → app-{yyyy-MM-dd}.{i}.log (matches Android pattern) + override func didArchiveLogFile(atPath logFilePath: String, wasRolled: Bool) { + guard wasRolled else { + super.didArchiveLogFile(atPath: logFilePath, wasRolled: wasRolled) + return + } + + let dir = (logFilePath as NSString).deletingLastPathComponent + let dateStr = Self.formattedDate(Date()) + let fm = FileManager.default + + // Find next available index, retry on move failure to handle TOCTOU race + var index = 0 + var archivedPath = logFilePath + var moved = false + while !moved && index < 1000 { + archivedPath = "\(dir)/\(Self.logPrefix)-\(dateStr).\(index).log" + if fm.fileExists(atPath: archivedPath) { + index += 1 + continue + } + do { + try fm.moveItem(atPath: logFilePath, toPath: archivedPath) + moved = true + } catch { + // Another thread may have created this file; try next index + index += 1 + } + } + if !moved { + NSLog("[OneKeyLog] Failed to archive log file after 1000 attempts: %@", logFilePath) + super.didArchiveLogFile(atPath: logFilePath, wasRolled: wasRolled) + } else { + // Pass the new path so the super class can find the file for cleanup + super.didArchiveLogFile(atPath: archivedPath, wasRolled: wasRolled) + } + } +} + +@objc public class OneKeyLog: NSObject { + + private static let maxMessageLength = 4096 + + private static let configured: Bool = { + let logsDir = logsDirectory + + // Ensure logs directory exists (matches Android's File(dir).mkdirs()) + try? FileManager.default.createDirectory( + atPath: logsDir, withIntermediateDirectories: true + ) + + // Apply file protection: files inaccessible while device is locked before first unlock + try? FileManager.default.setAttributes( + [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], + ofItemAtPath: logsDir + ) + + let fileManager = OneKeyLogFileManager(logsDirectory: logsDir) + // NOTE: DDLogFileManagerDefault.maximumNumberOfLogFiles counts ALL log files + // including the current active file (app-latest.log). + // Set to 7 = 1 active + 6 archived, matching Android MAX_HISTORY=6. + fileManager.maximumNumberOfLogFiles = 7 + + let logger = DDFileLogger(logFileManager: fileManager) + logger.rollingFrequency = 86400 // daily rolling + logger.maximumFileSize = 20_971_520 // 20 MB + logger.logFormatter = OneKeyLogFormatter() + + DDLog.add(logger) + return true + }() + + // Use a lock to protect DateFormatter (not thread-safe per Apple docs) + private static let timeFormatterLock = NSLock() + private static let _timeFormatter: DateFormatter = { + let fmt = DateFormatter() + fmt.dateFormat = "HH:mm:ss" + fmt.locale = Locale(identifier: "en_US_POSIX") + return fmt + }() + + private static func formattedTime(_ date: Date) -> String { + timeFormatterLock.lock() + defer { timeFormatterLock.unlock() } + return _timeFormatter.string(from: date) + } + + private static func truncate(_ message: String) -> String { + if message.count > maxMessageLength { + return String(message.prefix(maxMessageLength)) + "...(truncated)" + } + return message + } + + private static func sanitizeForLog(_ str: String) -> String { + return str.replacingOccurrences(of: "\n", with: " ") + .replacingOccurrences(of: "\r", with: " ") + } + + /// Sanitize sensitive data from native-side log messages + private static let nativeSensitivePatterns: [NSRegularExpression] = { + let patterns = [ + "(?:0x)?[0-9a-fA-F]{64}", + "\\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\\b", + "\\b[xyzXYZ](?:prv|pub)[1-9A-HJ-NP-Za-km-z]{107,108}\\b", + "(?:\\b[a-z]{3,8}\\b[\\s,]+){11,}\\b[a-z]{3,8}\\b", + "(?:Bearer|token[=:]?)\\s*[A-Za-z0-9_.\\-+/=]{20,}", + "(?:eyJ|AAAA)[A-Za-z0-9+/=]{40,}", + ] + return patterns.compactMap { try? NSRegularExpression(pattern: $0) } + }() + + private static func sanitizeSensitive(_ message: String) -> String { + var result = message + for regex in nativeSensitivePatterns { + result = regex.stringByReplacingMatches( + in: result, + range: NSRange(result.startIndex..., in: result), + withTemplate: "[REDACTED]" + ) + } + return result + } + + private static func formatMessage(_ tag: String, _ level: String, _ message: String) -> String { + let safeTag = sanitizeForLog(String(tag.prefix(64))) + let safeMessage = sanitizeSensitive(sanitizeForLog(message)) + if safeTag == "JS" { + return truncate(safeMessage) + } + let time = formattedTime(Date()) + return truncate("\(time) | \(level) : [\(safeTag)] \(safeMessage)") + } + + @objc public static func debug(_ tag: String, _ message: String) { + _ = configured + DDLogDebug(formatMessage(tag, "DEBUG", message)) + } + + @objc public static func info(_ tag: String, _ message: String) { + _ = configured + DDLogInfo(formatMessage(tag, "INFO", message)) + } + + @objc public static func warn(_ tag: String, _ message: String) { + _ = configured + DDLogWarn(formatMessage(tag, "WARN", message)) + } + + @objc public static func error(_ tag: String, _ message: String) { + _ = configured + DDLogError(formatMessage(tag, "ERROR", message)) + } + + /// Returns the logs directory path (for getLogFilePaths / deleteLogFiles) + static var logsDirectory: String { + guard let cacheDir = NSSearchPathForDirectoriesInDomains( + .cachesDirectory, .userDomainMask, true + ).first else { + // Fallback to tmp directory if Caches is somehow unavailable + return (NSTemporaryDirectory() as NSString).appendingPathComponent("logs") + } + return (cacheDir as NSString).appendingPathComponent("logs") + } +} + +/// Formatter that outputs message only (matches Android's `%msg%n` pattern) +private class OneKeyLogFormatter: NSObject, DDLogFormatter { + func format(message logMessage: DDLogMessage) -> String? { + return logMessage.message + } +} diff --git a/native-modules/native-logger/lefthook.yml b/native-modules/native-logger/lefthook.yml new file mode 100644 index 00000000..89766b1f --- /dev/null +++ b/native-modules/native-logger/lefthook.yml @@ -0,0 +1,10 @@ +pre-commit: + parallel: true + commands: + lint: + glob: "*.{js,ts,tsx}" + run: yarn lint {staged_files} + typecheck: + files: git diff --cached --name-only --diff-filter=ACMRTUXB + glob: "*.{js,ts,tsx}" + run: yarn typecheck diff --git a/native-modules/native-logger/nitro.json b/native-modules/native-logger/nitro.json new file mode 100644 index 00000000..065ae6cd --- /dev/null +++ b/native-modules/native-logger/nitro.json @@ -0,0 +1,17 @@ +{ + "cxxNamespace": ["nativelogger"], + "ios": { + "iosModuleName": "ReactNativeNativeLogger" + }, + "android": { + "androidNamespace": ["nativelogger"], + "androidCxxLibName": "nativelogger" + }, + "autolinking": { + "NativeLogger": { + "swift": "NativeLogger", + "kotlin": "NativeLogger" + } + }, + "ignorePaths": ["node_modules"] +} diff --git a/native-modules/native-logger/package.json b/native-modules/native-logger/package.json new file mode 100644 index 00000000..b9a9174a --- /dev/null +++ b/native-modules/native-logger/package.json @@ -0,0 +1,169 @@ +{ + "name": "@onekeyfe/react-native-native-logger", + "version": "1.1.24", + "description": "react-native-native-logger", + "main": "./lib/module/index.js", + "types": "./lib/typescript/src/index.d.ts", + "exports": { + ".": { + "source": "./src/index.tsx", + "types": "./lib/typescript/src/index.d.ts", + "default": "./lib/module/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "src", + "lib", + "android", + "ios", + "cpp", + "nitrogen", + "nitro.json", + "*.podspec", + "react-native.config.js", + "!ios/build", + "!android/build", + "!android/gradle", + "!android/gradlew", + "!android/gradlew.bat", + "!android/local.properties", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__", + "!**/.*" + ], + "scripts": { + "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", + "prepare": "bob build", + "nitrogen": "nitrogen", + "typecheck": "tsc", + "lint": "eslint \"**/*.{js,ts,tsx}\"", + "test": "jest", + "release": "yarn prepare && npm whoami && npm publish --access public" + }, + "keywords": [ + "react-native", + "ios", + "android" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/OneKeyHQ/app-modules/native-logger.git" + }, + "author": "onekeyfe (https://github.com/OneKeyHQ/app-modules)", + "license": "MIT", + "bugs": { + "url": "https://github.com/OneKeyHQ/app-modules/native-logger/issues" + }, + "homepage": "https://github.com/OneKeyHQ/app-modules/native-logger#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "devDependencies": { + "@commitlint/config-conventional": "^19.8.1", + "@eslint/compat": "^1.3.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.35.0", + "@react-native/babel-preset": "0.83.0", + "@react-native/eslint-config": "0.83.0", + "@release-it/conventional-changelog": "^10.0.1", + "@types/jest": "^29.5.14", + "@types/react": "^19.2.0", + "commitlint": "^19.8.1", + "del-cli": "^6.0.0", + "eslint": "^9.35.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "jest": "^29.7.0", + "lefthook": "^2.0.3", + "nitrogen": "0.31.10", + "prettier": "^2.8.8", + "react": "19.2.0", + "react-native": "0.83.0", + "react-native-builder-bob": "^0.40.13", + "react-native-nitro-modules": "0.33.2", + "release-it": "^19.0.4", + "turbo": "^2.5.6", + "typescript": "^5.9.2" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-nitro-modules": "0.33.2" + }, + "react-native-builder-bob": { + "source": "src", + "output": "lib", + "targets": [ + [ + "custom", + { + "script": "nitrogen", + "clean": "nitrogen/" + } + ], + [ + "module", + { + "esm": true + } + ], + [ + "typescript", + { + "project": "tsconfig.build.json" + } + ] + ] + }, + "prettier": { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + }, + "jest": { + "preset": "react-native", + "modulePathIgnorePatterns": [ + "/example/node_modules", + "/lib/" + ] + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "release-it": { + "git": { + "commitMessage": "chore: release ${version}", + "tagName": "v${version}" + }, + "npm": { + "publish": true + }, + "github": { + "release": true + }, + "plugins": { + "@release-it/conventional-changelog": { + "preset": { + "name": "angular" + } + } + } + }, + "create-react-native-library": { + "type": "nitro-module", + "languages": "kotlin-swift", + "tools": [ + "eslint", + "jest", + "lefthook", + "release-it" + ], + "version": "0.56.0" + } +} diff --git a/native-modules/native-logger/src/NativeLogger.nitro.ts b/native-modules/native-logger/src/NativeLogger.nitro.ts new file mode 100644 index 00000000..7de21f2f --- /dev/null +++ b/native-modules/native-logger/src/NativeLogger.nitro.ts @@ -0,0 +1,9 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +export interface NativeLogger + extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { + write(level: number, msg: string): void; + getLogDirectory(): string; + getLogFilePaths(): Promise; + deleteLogFiles(): Promise; +} diff --git a/native-modules/native-logger/src/index.tsx b/native-modules/native-logger/src/index.tsx new file mode 100644 index 00000000..14a8d068 --- /dev/null +++ b/native-modules/native-logger/src/index.tsx @@ -0,0 +1,8 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import type { NativeLogger as NativeLoggerType } from './NativeLogger.nitro'; + +const NativeLoggerHybridObject = + NitroModules.createHybridObject('NativeLogger'); + +export const NativeLogger = NativeLoggerHybridObject; +export type * from './NativeLogger.nitro'; diff --git a/native-modules/native-logger/tsconfig.build.json b/native-modules/native-logger/tsconfig.build.json new file mode 100644 index 00000000..45777014 --- /dev/null +++ b/native-modules/native-logger/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "declarationMap": true + }, + "exclude": ["**/__tests__/**/*", "**/__fixtures__/**/*", "**/__mocks__/**/*"] +} diff --git a/native-modules/native-logger/tsconfig.json b/native-modules/native-logger/tsconfig.json new file mode 100644 index 00000000..689ee37f --- /dev/null +++ b/native-modules/native-logger/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "rootDir": ".", + "paths": { + "native-logger": ["./src/index"] + }, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "customConditions": ["react-native-strict-api"], + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "lib": ["ESNext"], + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext", + "verbatimModuleSyntax": true + } +} diff --git a/native-modules/native-logger/turbo.json b/native-modules/native-logger/turbo.json new file mode 100644 index 00000000..08b9676e --- /dev/null +++ b/native-modules/native-logger/turbo.json @@ -0,0 +1,17 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "outputs": [ + "lib/**", + "nitrogen/**" + ], + "inputs": [ + "src/**", + "android/src/**", + "ios/**", + "cpp/**" + ] + } + } +} diff --git a/native-modules/react-native-app-update/.gitignore b/native-modules/react-native-app-update/.gitignore new file mode 100644 index 00000000..357a2f9a --- /dev/null +++ b/native-modules/react-native-app-update/.gitignore @@ -0,0 +1,89 @@ +# OSX +# +.DS_Store + +# XDE +.expo/ + +# VSCode +.vscode/ +jsconfig.json + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace +**/.xcode.env.local + +# Android/IJ +# +.classpath +.cxx +.gradle +.idea +.project +.settings +local.properties +android.iml + +# Cocoapods +# +example/ios/Pods + +# Ruby +example/vendor/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +android/app/libs +android/keystores/debug.keystore + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Expo +.expo/ + +# Turborepo +.turbo/ + +# generated by bob +lib/ + +# React Native Codegen +ios/generated +android/generated + +# React Native Nitro Modules +nitrogen/ + + + diff --git a/native-modules/react-native-app-update/LICENSE b/native-modules/react-native-app-update/LICENSE new file mode 100644 index 00000000..24ee4114 --- /dev/null +++ b/native-modules/react-native-app-update/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 OneKey + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/native-modules/react-native-app-update/ReactNativeAppUpdate.podspec b/native-modules/react-native-app-update/ReactNativeAppUpdate.podspec new file mode 100644 index 00000000..f829b3a1 --- /dev/null +++ b/native-modules/react-native-app-update/ReactNativeAppUpdate.podspec @@ -0,0 +1,30 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "ReactNativeAppUpdate" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => min_ios_version_supported } + s.source = { :git => "https://github.com/OneKeyHQ/app-modules/react-native-app-update.git", :tag => "#{s.version}" } + + s.source_files = [ + "ios/**/*.{swift}", + "ios/**/*.{m,mm}", + "cpp/**/*.{hpp,cpp}", + ] + + s.dependency 'React-jsi' + s.dependency 'React-callinvoker' + s.dependency 'ReactNativeNativeLogger' + + load 'nitrogen/generated/ios/ReactNativeAppUpdate+autolinking.rb' + add_nitrogen_files(s) + + install_modules_dependencies(s) +end diff --git a/native-modules/react-native-app-update/android/CMakeLists.txt b/native-modules/react-native-app-update/android/CMakeLists.txt new file mode 100644 index 00000000..2c5f10f7 --- /dev/null +++ b/native-modules/react-native-app-update/android/CMakeLists.txt @@ -0,0 +1,24 @@ +project(reactnativeappupdate) +cmake_minimum_required(VERSION 3.9.0) + +set(PACKAGE_NAME reactnativeappupdate) +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_CXX_STANDARD 20) + +# Define C++ library and add all sources +add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp) + +# Add Nitrogen specs :) +include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/reactnativeappupdate+autolinking.cmake) + +# Set up local includes +include_directories("src/main/cpp" "../cpp") + +find_library(LOG_LIB log) + +# Link all libraries together +target_link_libraries( + ${PACKAGE_NAME} + ${LOG_LIB} + android # <-- Android core +) diff --git a/native-modules/react-native-app-update/android/build.gradle b/native-modules/react-native-app-update/android/build.gradle new file mode 100644 index 00000000..8308dedb --- /dev/null +++ b/native-modules/react-native-app-update/android/build.gradle @@ -0,0 +1,141 @@ +buildscript { + ext.getExtOrDefault = {name -> + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['ReactNativeAppUpdate_' + name] + } + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:8.7.2" + // noinspection DifferentKotlinGradleVersion + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}" + } +} + +def reactNativeArchitectures() { + def value = rootProject.getProperties().get("reactNativeArchitectures") + return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" +apply from: '../nitrogen/generated/android/reactnativeappupdate+autolinking.gradle' + +apply plugin: "com.facebook.react" + +def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ReactNativeAppUpdate_" + name]).toInteger() +} + +android { + namespace "com.margelo.nitro.reactnativeappupdate" + + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + + defaultConfig { + minSdkVersion getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + + externalNativeBuild { + cmake { + cppFlags "-frtti -fexceptions -Wall -fstack-protector-all" + arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" + abiFilters (*reactNativeArchitectures()) + + buildTypes { + debug { + cppFlags "-O1 -g" + } + release { + cppFlags "-O2" + } + } + } + } + } + + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } + } + + packagingOptions { + excludes = [ + "META-INF", + "META-INF/**", + "**/libc++_shared.so", + "**/libfbjni.so", + "**/libjsi.so", + "**/libfolly_json.so", + "**/libfolly_runtime.so", + "**/libglog.so", + "**/libhermes.so", + "**/libhermes-executor-debug.so", + "**/libhermes_executor.so", + "**/libreactnative.so", + "**/libreactnativejni.so", + "**/libturbomodulejsijni.so", + "**/libreact_nativemodule_core.so", + "**/libjscexecutor.so" + ] + } + + buildFeatures { + buildConfig true + prefab true + } + + buildTypes { + release { + minifyEnabled false + } + } + + lintOptions { + disable "GradleCompatible" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs += [ + "generated/java", + "generated/jni" + ] + } + } +} + +repositories { + mavenCentral() + google() +} + +def kotlin_version = getExtOrDefault("kotlinVersion") + +dependencies { + implementation "com.facebook.react:react-android" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation project(":react-native-nitro-modules") + + implementation project(":onekeyfe_react-native-native-logger") + + implementation "com.squareup.okhttp3:okhttp:4.12.0" + implementation "com.squareup.okio:okio:3.9.0" + implementation "androidx.core:core-ktx:1.15.0" + + // BouncyCastle for GPG/PGP signature verification + implementation "org.bouncycastle:bcpg-jdk15to18:1.78.1" + implementation "org.bouncycastle:bcprov-jdk15to18:1.78.1" + + // MMKV for reading DevSettings (compileOnly: provided by the host app via react-native-mmkv) + compileOnly "io.github.zhongwuzw:mmkv:2.2.4" +} diff --git a/native-modules/react-native-app-update/android/gradle.properties b/native-modules/react-native-app-update/android/gradle.properties new file mode 100644 index 00000000..754b01a6 --- /dev/null +++ b/native-modules/react-native-app-update/android/gradle.properties @@ -0,0 +1,4 @@ +ReactNativeAppUpdate_kotlinVersion=1.9.25 +ReactNativeAppUpdate_compileSdkVersion=35 +ReactNativeAppUpdate_targetSdkVersion=35 +ReactNativeAppUpdate_minSdkVersion=24 diff --git a/native-modules/react-native-app-update/android/src/main/AndroidManifest.xml b/native-modules/react-native-app-update/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..9ea9ed59 --- /dev/null +++ b/native-modules/react-native-app-update/android/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/native-modules/react-native-app-update/android/src/main/cpp/cpp-adapter.cpp b/native-modules/react-native-app-update/android/src/main/cpp/cpp-adapter.cpp new file mode 100644 index 00000000..8c646aa7 --- /dev/null +++ b/native-modules/react-native-app-update/android/src/main/cpp/cpp-adapter.cpp @@ -0,0 +1,6 @@ +#include +#include "reactnativeappupdateOnLoad.hpp" + +extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { + return margelo::nitro::reactnativeappupdate::initialize(vm); +} diff --git a/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt b/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt new file mode 100644 index 00000000..dc572e0b --- /dev/null +++ b/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt @@ -0,0 +1,873 @@ +package com.margelo.nitro.reactnativeappupdate + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.FileProvider +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.Promise +import com.margelo.nitro.NitroModules +import com.margelo.nitro.nativelogger.OneKeyLog +import com.tencent.mmkv.MMKV +import okhttp3.OkHttpClient +import okhttp3.Request +import okio.buffer +import okio.sink +import java.io.BufferedInputStream +import java.io.BufferedReader +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.InputStreamReader +import java.security.MessageDigest +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection +import org.bouncycastle.openpgp.PGPSignatureList +import org.bouncycastle.openpgp.PGPUtil +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider +import org.bouncycastle.jce.provider.BouncyCastleProvider + +// OneKey GPG public key for signature verification +private const val GPG_PUBLIC_KEY = """-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGJATGwBEADL1K7b8dzYYzlSsvAGiA8mz042pygB7AAh/uFUycpNQdSzuoDE +VoXq/QsXCOsGkMdFLwlUjarRaxFX6RTV6S51LOlJFRsyGwXiMz08GSNagSafQ0YL +Gi+aoemPh6Ta5jWgYGIUWXavkjJciJYw43ACMdVmIWos94bA41Xm93dq9C3VRpl+ +EjvGAKRUMxJbH8r13TPzPmfN4vdrHLq+us7eKGJpwV/VtD9vVHAi0n48wGRq7DQw +IUDU2mKy3wmjwS38vIIu4yQyeUdl4EqwkCmGzWc7Cv2HlOG6rLcUdTAOMNBBX1IQ +iHKg9Bhh96MXYvBhEL7XHJ96S3+gTHw/LtrccBM+eiDJVHPZn+lw2HqX994DueLV +tAFDS+qf3ieX901IC97PTHsX6ztn9YZQtSGBJO3lEMBdC4ez2B7zUv4bgyfU+KvE +zHFIK9HmDehx3LoDAYc66nhZXyasiu6qGPzuxXu8/4qTY8MnhXJRBkbWz5P84fx1 +/Db5WETLE72on11XLreFWmlJnEWN4UOARrNn1Zxbwl+uxlSJyM+2GTl4yoccG+WR +uOUCmRXTgduHxejPGI1PfsNmFpVefAWBDO7SdnwZb1oUP3AFmhH5CD1GnmLnET+l +/c+7XfFLwgSUVSADBdO3GVS4Cr9ux4nIrHGJCrrroFfM2yvG8AtUVr16PQARAQAB +tCJvbmVrZXlocSBkZXZlbG9wZXIgPGRldkBvbmVrZXkuc28+iQJUBBMBCAA+FiEE +62iuVE8f3YzSZGJPs2mmepC/OHsFAmJATGwCGwMFCQeGH0QFCwkIBwIGFQoJCAsC +BBYCAwECHgECF4AACgkQs2mmepC/OHtgvg//bsWFMln08ZJjf5od/buJua7XYb3L +jWq1H5rdjJva5TP1UuQaDULuCuPqllxb+h+RB7g52yRG/1nCIrpTfveYOVtq/mYE +D12KYAycDwanbmtoUp25gcKqCrlNeSE1EXmPlBzyiNzxJutE1DGlvbY3rbuNZLQi +UTFBG3hk6JgsaXkFCwSmF95uATAaItv8aw6eY7RWv47rXhQch6PBMCir4+a/v7vs +lXxQtcpCqfLtjrloq7wvmD423yJVsUGNEa7/BrwFz6/GP6HrUZc6JgvrieuiBE4n +ttXQFm3dkOfD+67MLMO3dd7nPhxtjVEGi+43UH3/cdtmU4JFX3pyCQpKIlXTEGp2 +wqim561auKsRb1B64qroCwT7aACwH0ZTgQS8rPifG3QM8ta9QheuOsjHLlqjo8jI +fpqe0vKYUlT092joT0o6nT2MzmLmHUW0kDqD9p6JEJEZUZpqcSRE84eMTFNyu966 +xy/rjN2SMJTFzkNXPkwXYrMYoahGez1oZfLzV6SQ0+blNc3aATt9aQW6uaCZtMw1 +ibcfWW9neHVpRtTlMYCoa2reGaBGCv0Nd8pMcyFUQkVaes5cQHkh3r5Dba+YrVvp +l4P8HMbN8/LqAv7eBfj3ylPa/8eEPWVifcum2Y9TqherN1C2JDqWIpH4EsApek3k +NMK6q0lPxXjZ3Pa5Ag0EYkBMbAEQAM1R4N3bBkwKkHeYwsQASevUkHwY4eg6Ncgp +f9NbmJHcEioqXTIv0nHCQbos3P2NhXvDowj4JFkK/ZbpP9yo0p7TI4fckseVSWwI +tiF9l/8OmXvYZMtw3hHcUUZVdJnk0xrqT6ni6hyRFIfbqous6/vpqi0GG7nB/+lU +E5StGN8696ZWRyAX9MmwoRoods3ShNJP0+GCYHfIcG0XRhEDMJph+7mWPlkQUcza +4aEjxOQ4Stwwp+ZL1rXSlyJIPk1S9/FIS/Uw5GgqFJXIf5n+SCVtUZ8lGedEWwe4 +wXsoPFxxOc2Gqw5r4TrJFdgA3MptYebXmb2LGMssXQTM1AQS2LdpnWw44+X1CHvQ +0m4pEw/g2OgeoJPBurVUnu2mU/M+ARZiS4ceAR0pLZN7Yq48p1wr6EOBQdA3Usby +uc17MORG/IjRmjz4SK/luQLXjN+0jwQSoM1kcIHoRk37B8feHjVufJDKlqtw83H1 +uNu6lGwb8MxDgTuuHloDijCDQsn6m7ZKU1qqLDGtdvCUY2ovzuOUS9vv6MAhR86J +kqoU3sOBMeQhnBaTNKU0IjT4M+ERCWQ7MewlzXuPHgyb4xow1SKZny+f+fYXPy9+ +hx4/j5xaKrZKdq5zIo+GRGe4lA088l253nGeLgSnXsbSxqADqKK73d7BXLCVEZHx +f4Sa5JN7ABEBAAGJAjwEGAEIACYWIQTraK5UTx/djNJkYk+zaaZ6kL84ewUCYkBM +bAIbDAUJB4YfRAAKCRCzaaZ6kL84e0UGD/4mVWyGoQC86TyPoU4Pb5r8mynXWmiH +ZGKu2ll8qn3l5Q67OophgbA1I0GTBFsYK2f91ahgs7FEsLrmz/25E8ybcdJipITE +6869nyE1b37jVb3z3BJLYS/4MaNvugNz4VjMHWVAL52glXLN+SJBSNscmWZDKnVn +Rnrn+kBEvOWZgLbi4MpPiNVwm2PGnrtPzudTcg/NS3HOcmJTfG3mrnwwNJybTVAx +txlQPoXUpJQqJjtkPPW+CqosolpRdugQ5zpFSg05iL+vN+CMrVPkk85w87dtsidl +yZl/ZNITrLzym9d2UFVQZY2rRohNdRfx3l4rfXJFLaqQtihRvBIiMKTbUb2V0pd3 +rVLz2Ck3gJqPfPEEmCWS0Nx6rME8m0sOkNyMau3dMUUAs4j2c3pOQmsZRjKo7LAc +7/GahKFhZ2aBCQzvcTES+gPH1Z5HnivkcnUF2gnQV9x7UOr1Q/euKJsxPl5CCZtM +N9GFW10cDxFo7cO5Ch+/BkkkfebuI/4Wa1SQTzawsxTx4eikKwcemgfDsyIqRs2W +62PBrqCzs9Tg19l35sCdmvYsvMadrYFXukHXiUKEpwJMdTLAtjJ+AX84YLwuHi3+ +qZ5okRCqZH+QpSojSScT9H5ze4ZpuP0d8pKycxb8M2RfYdyOtT/eqsZ/1EQPg7kq +P2Q5dClenjjjVA== +=F0np +-----END PGP PUBLIC KEY BLOCK-----""" + +private data class Listener( + val id: Double, + val callback: (DownloadEvent) -> Unit +) + +@DoNotStrip +class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() { + + companion object { + private const val CHANNEL_ID = "updateApp" + private const val NOTIFICATION_ID = 1 + // Use our own BouncyCastle provider instance to avoid Android's stripped-down built-in "BC" + private val bcProvider = BouncyCastleProvider() + } + + private val listeners = CopyOnWriteArrayList() + private val nextListenerId = AtomicLong(1) + private val isDownloading = AtomicBoolean(false) + // downloadThread removed: downloads use coroutine-based Promise.async, not raw threads + + private fun sendEvent(type: String, progress: Int = 0, message: String = "") { + val event = DownloadEvent(type = type, progress = progress.toDouble(), message = message) + for (listener in listeners) { + try { + listener.callback(event) + } catch (e: Exception) { + OneKeyLog.error("AppUpdate", "Error sending event: ${e.message}") + } + } + } + + override fun addDownloadListener(callback: (DownloadEvent) -> Unit): Double { + val id = nextListenerId.getAndIncrement().toDouble() + listeners.add(Listener(id, callback)) + return id + } + + override fun removeDownloadListener(id: Double) { + listeners.removeAll { it.id == id } + } + + private fun getApkCacheDir(): File { + val context = NitroModules.applicationContext + ?: throw SecurityException("Application context unavailable") + val apkDir = File(context.cacheDir, "apks") + if (!apkDir.exists()) apkDir.mkdirs() + return apkDir + } + + /** + * Derive a local file name from a download URL by extracting the last path segment. + * e.g. "https://example.com/path/to/app-1.0.apk" -> "app-1.0.apk" + */ + private fun filePathFromUrl(url: String): String { + val path = Uri.parse(url).lastPathSegment + if (path.isNullOrBlank()) { + throw Exception("Cannot derive file name from URL: $url") + } + return path + } + + private fun buildFile(path: String): File { + val stripped = path.removePrefix("file:///") + val context = NitroModules.applicationContext + ?: throw SecurityException("Application context unavailable, cannot validate file path") + + // Resolve relative paths against cacheDir/apk/ + val file = if (stripped.startsWith("/")) { + File(stripped) + } else { + File(getApkCacheDir(), stripped) + } + + // Validate the resolved path is within the app's cache or files directory + val canonicalPath = file.canonicalPath + val cacheDir = context.cacheDir.canonicalPath + val filesDir = context.filesDir.canonicalPath + val externalCacheDir = context.externalCacheDir?.canonicalPath + val externalFilesDir = context.getExternalFilesDir(null)?.canonicalPath + val allowed = canonicalPath.startsWith(cacheDir + File.separator) || canonicalPath == cacheDir || + canonicalPath.startsWith(filesDir + File.separator) || canonicalPath == filesDir || + (externalCacheDir != null && (canonicalPath.startsWith(externalCacheDir + File.separator) || canonicalPath == externalCacheDir)) || + (externalFilesDir != null && (canonicalPath.startsWith(externalFilesDir + File.separator) || canonicalPath == externalFilesDir)) + if (!allowed) { + OneKeyLog.error("AppUpdate", "buildFile: path outside allowed directories, " + + "input=$path, resolved=$canonicalPath, " + + "cacheDir=$cacheDir, filesDir=$filesDir, " + + "externalCacheDir=$externalCacheDir, externalFilesDir=$externalFilesDir") + throw SecurityException("Path outside allowed directories: $canonicalPath") + } + OneKeyLog.debug("AppUpdate", "buildFile: input=$path, resolved=$canonicalPath") + return file + } + + private fun bytesToHex(bytes: ByteArray): String { + return bytes.joinToString("") { "%02x".format(it) } + } + + private fun computeSha256(file: File): String { + val digest = MessageDigest.getInstance("SHA-256") + BufferedInputStream(FileInputStream(file)).use { bis -> + val buffer = ByteArray(8192) + var count: Int + while (bis.read(buffer).also { count = it } > 0) { + digest.update(buffer, 0, count) + } + } + return bytesToHex(digest.digest()) + } + + /** + * Download ASC file for the given URL and save to the given path. + * Returns the saved file, or null on failure. + */ + private fun downloadAscFile(ascUrl: String, ascFile: File): File? { + val client = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .followRedirects(false) + .followSslRedirects(false) + .build() + val request = Request.Builder().url(ascUrl).build() + val response = client.newCall(request).execute() + if (!response.isSuccessful) return null + val content = StringBuilder() + val maxAscSize = 10 * 1024 + val body = response.body ?: return null + BufferedReader(InputStreamReader(body.byteStream())).use { reader -> + var line: String? + while (reader.readLine().also { line = it } != null) { + content.append(line).append("\n") + if (content.length > maxAscSize) return null + } + } + val ascContent = content.toString() + if (ascContent.isEmpty()) return null + ascFile.parentFile?.mkdirs() + FileOutputStream(ascFile).use { fos -> fos.write(ascContent.toByteArray()) } + return ascFile + } + + /** + * Verify GPG signature of an ASC file and extract the SHA256 hash. + * Returns the SHA256 hash if signature is valid, null otherwise. + */ + private fun verifyAscAndExtractSha256(ascFile: File): String? { + val ascContent = ascFile.readText() + if (!ascContent.contains("-----BEGIN PGP SIGNED MESSAGE-----")) return null + + val lines = ascContent.lines() + val hashHeaderIdx = lines.indexOfFirst { it.startsWith("Hash:") } + val sigStartIdx = lines.indexOfFirst { it == "-----BEGIN PGP SIGNATURE-----" } + val sigEndIdx = lines.indexOfFirst { it == "-----END PGP SIGNATURE-----" } + if (hashHeaderIdx < 0 || sigStartIdx < 0 || sigEndIdx < 0) return null + + val bodyStartIdx = hashHeaderIdx + 2 + val bodyLines = lines.subList(bodyStartIdx, sigStartIdx) + val cleartextBody = bodyLines.joinToString("\r\n").trimEnd() + val sigBlock = lines.subList(sigStartIdx, sigEndIdx + 1).joinToString("\n") + + // Verify GPG signature + val sigInputStream = PGPUtil.getDecoderStream(sigBlock.byteInputStream()) + val sigFactory = JcaPGPObjectFactory(sigInputStream) + val signatureList = sigFactory.nextObject() + if (signatureList !is PGPSignatureList || signatureList.isEmpty) return null + + val pgpSignature = signatureList[0] + val pubKeyStream = PGPUtil.getDecoderStream(GPG_PUBLIC_KEY.byteInputStream()) + val pgpPubKeyRingCollection = PGPPublicKeyRingCollection(pubKeyStream, JcaKeyFingerprintCalculator()) + val publicKey = pgpPubKeyRingCollection.getPublicKey(pgpSignature.keyID) ?: return null + + pgpSignature.init(JcaPGPContentVerifierBuilderProvider().setProvider(bcProvider), publicKey) + val unescapedLines = cleartextBody.lines().map { line -> + if (line.startsWith("- ")) line.substring(2) else line + } + val dataToVerify = unescapedLines.joinToString("\r\n").toByteArray(Charsets.UTF_8) + pgpSignature.update(dataToVerify) + if (!pgpSignature.verify()) return null + + // Extract SHA256 from verified cleartext + val sha256 = cleartextBody.trim().split("\\s+".toRegex())[0].lowercase() + if (sha256.length != 64 || !sha256.all { it in '0'..'9' || it in 'a'..'f' }) return null + return sha256 + } + + /** Constant-time comparison to prevent timing attacks on hash values */ + private fun secureCompare(a: String, b: String): Boolean { + val aBytes = a.toByteArray(Charsets.UTF_8) + val bBytes = b.toByteArray(Charsets.UTF_8) + if (aBytes.size != bBytes.size) return false + var result = 0 + for (i in aBytes.indices) { + result = result or (aBytes[i].toInt() xor bBytes[i].toInt()) + } + return result == 0 + } + + /** + * Check if an existing APK is valid by verifying GPG signature and SHA256 against the ASC file. + * Downloads ASC if not present. Returns true if APK is valid. + */ + private fun tryVerifyExistingApk(url: String, filePath: String, apkFile: File): Boolean { + return try { + val ascFilePath = "$filePath.SHA256SUMS.asc" + val ascFile = buildFile(ascFilePath) + + if (!ascFile.exists()) { + val ascUrl = "$url.SHA256SUMS.asc" + OneKeyLog.info("AppUpdate", "tryVerifyExistingApk: ASC not found, downloading from $ascUrl") + if (downloadAscFile(ascUrl, ascFile) == null) { + OneKeyLog.warn("AppUpdate", "tryVerifyExistingApk: ASC download failed") + return false + } + OneKeyLog.info("AppUpdate", "tryVerifyExistingApk: ASC downloaded to ${ascFile.absolutePath}") + } + + val expectedSha256 = verifyAscAndExtractSha256(ascFile) + if (expectedSha256 == null) { + OneKeyLog.warn("AppUpdate", "tryVerifyExistingApk: GPG verification or SHA256 extraction failed") + return false + } + + OneKeyLog.info("AppUpdate", "tryVerifyExistingApk: computing SHA256 of existing APK (size=${apkFile.length()})...") + val actualSha256 = computeSha256(apkFile) + + if (secureCompare(actualSha256, expectedSha256)) { + OneKeyLog.info("AppUpdate", "tryVerifyExistingApk: SHA256 matches, APK is valid") + true + } else { + OneKeyLog.warn("AppUpdate", "tryVerifyExistingApk: SHA256 mismatch, expected=${expectedSha256.take(16)}..., got=${actualSha256.take(16)}...") + false + } + } catch (e: Exception) { + OneKeyLog.warn("AppUpdate", "tryVerifyExistingApk: failed: ${e.javaClass.simpleName}: ${e.message}") + false + } + } + + private fun isDebuggable(): Boolean { + val context = NitroModules.applicationContext ?: return false + return (context.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0 + } + + /** Returns true if OneKey developer mode (DevSettings) is enabled via MMKV storage. */ + private fun isDevSettingsEnabled(): Boolean { + return try { + val context = NitroModules.applicationContext ?: return false + MMKV.initialize(context) + val mmkv = MMKV.mmkvWithID("onekey-app-dev-setting") ?: return false + mmkv.decodeBool("onekey_developer_mode_enabled", false) + } catch (e: Exception) { + false + } + } + + /** Returns true if the skip-GPG-verification toggle is enabled via MMKV storage. */ + private fun isSkipGPGEnabled(): Boolean { + return try { + val context = NitroModules.applicationContext ?: return false + MMKV.initialize(context) + val mmkv = MMKV.mmkvWithID("onekey-app-dev-setting") ?: return false + mmkv.decodeBool("onekey_bundle_skip_gpg_verification", false) + } catch (e: Exception) { + false + } + } + + override fun downloadAPK(params: AppUpdateDownloadParams): Promise { + return Promise.async { + if (isDownloading.getAndSet(true)) { + OneKeyLog.warn("AppUpdate", "downloadAPK: rejected, already downloading") + throw Exception("Download already in progress") + } + + try { + val url = params.downloadUrl + val filePath = filePathFromUrl(url) + val notificationTitle = params.notificationTitle + val fileSize = params.fileSize.toLong() + + OneKeyLog.info("AppUpdate", "downloadAPK: url=$url, filePath=$filePath, fileSize=$fileSize") + + val context = NitroModules.applicationContext + ?: throw Exception("Application context unavailable") + + val notifyManager = NotificationManagerCompat.from(context) + val builder = NotificationCompat.Builder(context, CHANNEL_ID) + .setContentTitle(notificationTitle) + .setContentText("") + .setOngoing(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setSmallIcon(android.R.drawable.stat_sys_download) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, "updateApp", NotificationManager.IMPORTANCE_DEFAULT + ) + notifyManager.createNotificationChannel(channel) + } + + if (!url.startsWith("https://")) { + OneKeyLog.error("AppUpdate", "downloadAPK: URL is not HTTPS: $url") + throw Exception("Download URL must use HTTPS") + } + + val downloadedFile = buildFile(filePath) + if (downloadedFile.exists()) { + OneKeyLog.info("AppUpdate", "downloadAPK: existing APK found (size=${downloadedFile.length()}), verifying...") + if (tryVerifyExistingApk(url, filePath, downloadedFile)) { + OneKeyLog.info("AppUpdate", "downloadAPK: existing APK is valid, skipping download") + sendEvent("update/downloaded") + return@async + } + OneKeyLog.info("AppUpdate", "downloadAPK: existing APK invalid, deleting and re-downloading...") + downloadedFile.delete() + } + + val client = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .followRedirects(false) + .followSslRedirects(false) + .build() + val request = Request.Builder().url(url).build() + val response = client.newCall(request).execute() + + if (!response.isSuccessful) { + OneKeyLog.error("AppUpdate", "downloadAPK: HTTP error, statusCode=${response.code}") + sendEvent("update/error", message = response.code.toString()) + throw Exception(response.code.toString()) + } + + val body = response.body ?: throw Exception("Empty response body") + val contentLength = if (fileSize > 0) fileSize else body.contentLength() + OneKeyLog.info("AppUpdate", "downloadAPK: HTTP 200, contentLength=$contentLength, starting download...") + val source = body.source() + val sink = downloadedFile.sink().buffer() + val sinkBuffer = sink.buffer + + var totalBytesRead = 0L + val bufferSize = 8 * 1024L + sendEvent("update/start") + var prevProgress = 0 + + try { + while (true) { + val bytesRead = source.read(sinkBuffer, bufferSize) + if (bytesRead == -1L) break + sink.emit() + totalBytesRead += bytesRead + if (contentLength > 0) { + val progress = ((totalBytesRead * 100) / contentLength).toInt() + if (prevProgress != progress) { + sendEvent("update/downloading", progress = progress) + OneKeyLog.info("AppUpdate", "download progress: $progress%") + builder.setProgress(100, progress, false) + if (ActivityCompat.checkSelfPermission( + context, android.Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + notifyManager.notify(NOTIFICATION_ID, builder.build()) + } + prevProgress = progress + } + } + } + } finally { + sink.flush() + sink.close() + source.close() + } + + OneKeyLog.info("AppUpdate", "Download completed") + sendEvent("update/downloaded") + + notifyManager.cancel(NOTIFICATION_ID) + builder.setContentText("") + .setProgress(0, 0, false) + .setOngoing(false) + .setAutoCancel(true) + if (ActivityCompat.checkSelfPermission( + context, android.Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + notifyManager.notify(NOTIFICATION_ID, builder.build()) + } + } catch (e: Exception) { + OneKeyLog.error("AppUpdate", "downloadAPK: failed: ${e.javaClass.simpleName}: ${e.message}") + sendEvent("update/error", message = "${e.javaClass.simpleName}: ${e.message}") + throw e + } finally { + isDownloading.set(false) + } + } + } + + override fun downloadASC(params: AppUpdateFileParams): Promise { + return Promise.async { + val url = params.downloadUrl + val filePath = filePathFromUrl(url) + + OneKeyLog.info("AppUpdate", "downloadASC: url=$url, filePath=$filePath") + + if (!url.startsWith("https://")) { + OneKeyLog.error("AppUpdate", "downloadASC: URL is not HTTPS: $url") + throw Exception("Download URL must use HTTPS") + } + + val ascFileUrl = "$url.SHA256SUMS.asc" + val ascFilePath = "$filePath.SHA256SUMS.asc" + OneKeyLog.info("AppUpdate", "downloadASC: ascFileUrl=$ascFileUrl") + + val client = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .followRedirects(false) + .followSslRedirects(false) + .build() + val request = Request.Builder().url(ascFileUrl).build() + val response = client.newCall(request).execute() + + if (!response.isSuccessful) { + OneKeyLog.error("AppUpdate", "downloadASC: HTTP error, statusCode=${response.code}") + throw Exception(response.code.toString()) + } + + OneKeyLog.info("AppUpdate", "downloadASC: HTTP 200, reading ASC content...") + + val content = StringBuilder() + val maxAscSize = 10 * 1024 // 10 KB max for ASC files + val body = response.body ?: throw Exception("Empty ASC response body") + BufferedReader(InputStreamReader(body.byteStream())).use { reader -> + var line: String? + while (reader.readLine().also { line = it } != null) { + content.append(line).append("\n") + if (content.length > maxAscSize) { + OneKeyLog.error("AppUpdate", "downloadASC: ASC file exceeds max size ($maxAscSize bytes)") + throw Exception("ASC file exceeds maximum allowed size") + } + } + } + + val ascContent = content.toString() + if (ascContent.isEmpty()) { + OneKeyLog.error("AppUpdate", "downloadASC: ASC content is empty") + throw Exception("Empty ASC file") + } + + OneKeyLog.info("AppUpdate", "downloadASC: ASC content size=${ascContent.length} bytes") + + val ascFile = buildFile(ascFilePath) + if (ascFile.exists()) { + OneKeyLog.info("AppUpdate", "downloadASC: existing ASC file found, deleting...") + ascFile.delete() + } + FileOutputStream(ascFile).use { fos -> + fos.write(ascContent.toByteArray()) + } + OneKeyLog.info("AppUpdate", "downloadASC: saved ASC file to $ascFilePath") + } + } + + override fun verifyASC(params: AppUpdateFileParams): Promise { + return Promise.async { + val filePath = filePathFromUrl(params.downloadUrl) + OneKeyLog.info("AppUpdate", "verifyASC: filePath=$filePath") + + // Skip GPG verification only when both DevSettings and skip-GPG toggle are enabled + val devSettings = isDevSettingsEnabled() + val skipGPGToggle = isSkipGPGEnabled() + OneKeyLog.info("AppUpdate", "verifyASC: GPG check: devSettings=$devSettings, skipGPGToggle=$skipGPGToggle") + if (devSettings && skipGPGToggle) { + OneKeyLog.warn("AppUpdate", "verifyASC: GPG verification skipped (DevSettings + skip-GPG enabled)") + return@async + } + + try { + + val ascFilePath = "$filePath.SHA256SUMS.asc" + val ascFile = buildFile(ascFilePath) + if (!ascFile.exists()) { + OneKeyLog.error("AppUpdate", "verifyASC: ASC file not found at $ascFilePath") + throw Exception("ASC file not found") + } + + val ascContent = ascFile.readText() + OneKeyLog.info("AppUpdate", "verifyASC: ASC file loaded, size=${ascContent.length} bytes") + + if (!ascContent.contains("-----BEGIN PGP SIGNED MESSAGE-----")) { + OneKeyLog.error("AppUpdate", "verifyASC: ASC file missing PGP signed message header") + throw Exception("ASC file does not contain a PGP signed message") + } + + // Parse the cleartext signed message + val lines = ascContent.lines() + val hashHeaderIdx = lines.indexOfFirst { it.startsWith("Hash:") } + val sigStartIdx = lines.indexOfFirst { it == "-----BEGIN PGP SIGNATURE-----" } + val sigEndIdx = lines.indexOfFirst { it == "-----END PGP SIGNATURE-----" } + + if (hashHeaderIdx < 0 || sigStartIdx < 0 || sigEndIdx < 0) { + OneKeyLog.error("AppUpdate", "verifyASC: invalid cleartext format (hashHeader=$hashHeaderIdx, sigStart=$sigStartIdx, sigEnd=$sigEndIdx)") + throw Exception("Invalid PGP cleartext signed message format") + } + + OneKeyLog.info("AppUpdate", "verifyASC: parsed cleartext message structure OK") + + val bodyStartIdx = hashHeaderIdx + 2 + val bodyLines = lines.subList(bodyStartIdx, sigStartIdx) + val cleartextBody = bodyLines.joinToString("\r\n").trimEnd() + + val sigBlock = lines.subList(sigStartIdx, sigEndIdx + 1).joinToString("\n") + + // Parse signature + OneKeyLog.info("AppUpdate", "verifyASC: parsing PGP signature...") + val sigInputStream = PGPUtil.getDecoderStream(sigBlock.byteInputStream()) + val sigFactory = JcaPGPObjectFactory(sigInputStream) + val signatureList = sigFactory.nextObject() + + if (signatureList !is PGPSignatureList || signatureList.isEmpty) { + OneKeyLog.error("AppUpdate", "verifyASC: no PGP signature found in ASC file") + throw Exception("No PGP signature found in ASC file") + } + + val pgpSignature = signatureList[0] + val keyId = pgpSignature.keyID + OneKeyLog.info("AppUpdate", "verifyASC: signature keyID=${java.lang.Long.toHexString(keyId).uppercase()}") + + // Parse public key + OneKeyLog.info("AppUpdate", "verifyASC: loading GPG public key...") + val pubKeyStream = PGPUtil.getDecoderStream(GPG_PUBLIC_KEY.byteInputStream()) + val pgpPubKeyRingCollection = PGPPublicKeyRingCollection(pubKeyStream, JcaKeyFingerprintCalculator()) + val publicKey = pgpPubKeyRingCollection.getPublicKey(keyId) + if (publicKey == null) { + OneKeyLog.error("AppUpdate", "verifyASC: GPG public key not found for keyID=${java.lang.Long.toHexString(keyId).uppercase()}") + throw Exception("GPG public key not found for signature verification") + } + OneKeyLog.info("AppUpdate", "verifyASC: public key matched, verifying signature...") + + // Verify signature + pgpSignature.init(JcaPGPContentVerifierBuilderProvider().setProvider(bcProvider), publicKey) + + val unescapedLines = cleartextBody.lines().map { line -> + if (line.startsWith("- ")) line.substring(2) else line + } + val dataToVerify = unescapedLines.joinToString("\r\n").toByteArray(Charsets.UTF_8) + pgpSignature.update(dataToVerify) + + if (!pgpSignature.verify()) { + OneKeyLog.error("AppUpdate", "verifyASC: GPG signature verification FAILED") + throw Exception("GPG signature verification failed for ASC file") + } + OneKeyLog.info("AppUpdate", "verifyASC: GPG signature verified OK") + + // Extract SHA256 from cleartext (format: " \n" or just "") + val sha256 = cleartextBody.trim().split("\\s+".toRegex())[0].lowercase() + OneKeyLog.info("AppUpdate", "verifyASC: extracted SHA256=${sha256.take(16)}...") + + if (sha256.length != 64 || !sha256.all { it in '0'..'9' || it in 'a'..'f' }) { + OneKeyLog.error("AppUpdate", "verifyASC: invalid SHA256 hash format (length=${sha256.length})") + throw Exception("Invalid SHA256 hash format in ASC file") + } + + // Verify APK file SHA256 + val apkFile = buildFile(filePath) + if (!apkFile.exists()) { + OneKeyLog.error("AppUpdate", "verifyASC: APK file not found at $filePath") + throw Exception("APK file not found: $filePath") + } + + OneKeyLog.info("AppUpdate", "verifyASC: computing SHA256 of APK file (size=${apkFile.length()} bytes)...") + val fileSha256 = computeSha256(apkFile) + OneKeyLog.info("AppUpdate", "verifyASC: APK SHA256=${fileSha256.take(16)}...") + + if (!secureCompare(fileSha256, sha256)) { + OneKeyLog.error("AppUpdate", "verifyASC: SHA256 MISMATCH — expected=${sha256.take(16)}..., got=${fileSha256.take(16)}...") + throw Exception("SHA256 mismatch for APK file") + } + + OneKeyLog.info("AppUpdate", "verifyASC: GPG signature + SHA256 verification passed") + + } catch (e: Exception) { + OneKeyLog.error("AppUpdate", "verifyASC: failed: ${e.javaClass.simpleName}: ${e.message}") + throw e + } + } + } + + // Track verified files with their SHA-256 hash to prevent TOCTOU attacks + private data class VerifiedFile(val canonicalPath: String, val sha256: String) + private val verifiedFiles = java.util.Collections.synchronizedMap(mutableMapOf()) + + override fun verifyAPK(params: AppUpdateFileParams): Promise { + return Promise.async { + val filePath = filePathFromUrl(params.downloadUrl) + OneKeyLog.info("AppUpdate", "verifyAPK: filePath=$filePath") + + val file = buildFile(filePath) + if (!file.exists()) { + OneKeyLog.error("AppUpdate", "verifyAPK: APK file not found at $filePath") + throw Exception("NOT_FOUND_PACKAGE") + } + OneKeyLog.info("AppUpdate", "verifyAPK: APK file found, size=${file.length()} bytes") + + val context = NitroModules.applicationContext + ?: throw Exception("Application context unavailable") + val pm = context.packageManager + + // Check package name + OneKeyLog.info("AppUpdate", "verifyAPK: parsing APK package info...") + val info = pm.getPackageArchiveInfo(file.absolutePath, 0) + if (info?.packageName == null) { + OneKeyLog.error("AppUpdate", "verifyAPK: failed to parse APK package info (INVALID_PACKAGE)") + throw Exception("INVALID_PACKAGE") + } + OneKeyLog.info("AppUpdate", "verifyAPK: APK packageName=${info.packageName}, versionName=${info.versionName}, versionCode=${if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info.longVersionCode else @Suppress("DEPRECATION") info.versionCode}") + + val debugBuild = isDebuggable() + + // Check package name + if (info.packageName != context.packageName) { + OneKeyLog.error("AppUpdate", "verifyAPK: package name mismatch — APK=${info.packageName}, installed=${context.packageName}") + if (!debugBuild) throw Exception("PACKAGE_NAME_MISMATCH") + OneKeyLog.warn("AppUpdate", "verifyAPK: DEBUG build — ignoring package name mismatch") + } else { + OneKeyLog.info("AppUpdate", "verifyAPK: package name matches installed app") + } + + // Verify APK signing certificate matches the installed app + OneKeyLog.info("AppUpdate", "verifyAPK: verifying APK signing certificate (API level=${Build.VERSION.SDK_INT})...") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val apkInfo = pm.getPackageArchiveInfo(file.absolutePath, PackageManager.GET_SIGNING_CERTIFICATES) + val installedInfo = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES) + val apkSigners = apkInfo?.signingInfo?.apkContentsSigners + val installedSigners = installedInfo?.signingInfo?.apkContentsSigners + if (apkSigners == null || installedSigners == null) { + OneKeyLog.error("AppUpdate", "verifyAPK: signing info unavailable (apkSigners=${apkSigners != null}, installedSigners=${installedSigners != null})") + if (!debugBuild) throw Exception("SIGNATURE_UNAVAILABLE") + OneKeyLog.warn("AppUpdate", "verifyAPK: DEBUG build — ignoring unavailable signatures") + } else { + OneKeyLog.info("AppUpdate", "verifyAPK: APK signers count=${apkSigners.size}, installed signers count=${installedSigners.size}") + if (apkSigners.toSet() != installedSigners.toSet()) { + OneKeyLog.error("AppUpdate", "verifyAPK: signing certificate MISMATCH") + if (!debugBuild) throw Exception("SIGNATURE_MISMATCH") + OneKeyLog.warn("AppUpdate", "verifyAPK: DEBUG build — ignoring certificate mismatch") + } else { + OneKeyLog.info("AppUpdate", "verifyAPK: signing certificate verified OK") + } + } + } else { + @Suppress("DEPRECATION") + val apkInfo = pm.getPackageArchiveInfo(file.absolutePath, PackageManager.GET_SIGNATURES) + @Suppress("DEPRECATION") + val installedInfo = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES) + val apkSignatures = apkInfo?.signatures + val installedSignatures = installedInfo?.signatures + if (apkSignatures == null || installedSignatures == null) { + OneKeyLog.error("AppUpdate", "verifyAPK: legacy signatures unavailable") + if (!debugBuild) throw Exception("SIGNATURE_UNAVAILABLE") + OneKeyLog.warn("AppUpdate", "verifyAPK: DEBUG build — ignoring unavailable signatures") + } else { + OneKeyLog.info("AppUpdate", "verifyAPK: APK signatures count=${apkSignatures.size}, installed signatures count=${installedSignatures.size}") + if (apkSignatures.toSet() != installedSignatures.toSet()) { + OneKeyLog.error("AppUpdate", "verifyAPK: legacy signing certificate MISMATCH") + if (!debugBuild) throw Exception("SIGNATURE_MISMATCH") + OneKeyLog.warn("AppUpdate", "verifyAPK: DEBUG build — ignoring certificate mismatch") + } else { + OneKeyLog.info("AppUpdate", "verifyAPK: signing certificate verified OK") + } + } + } + + // Compute SHA-256 hash of the verified file for TOCTOU protection + OneKeyLog.info("AppUpdate", "verifyAPK: computing SHA-256 hash for TOCTOU protection...") + val fileHash = computeSha256(file) + verifiedFiles[file.canonicalPath] = fileHash + OneKeyLog.info("AppUpdate", "verifyAPK: APK verified successfully, hash=${fileHash.take(16)}..., stored for install verification") + } + } + + override fun installAPK(params: AppUpdateFileParams): Promise { + return Promise.async { + val filePath = filePathFromUrl(params.downloadUrl) + OneKeyLog.info("AppUpdate", "installAPK: filePath=$filePath") + + val context = NitroModules.applicationContext + ?: throw Exception("Application context unavailable") + val file = buildFile(filePath) + if (!file.exists()) { + OneKeyLog.error("AppUpdate", "installAPK: APK file not found at $filePath") + throw Exception("NOT_FOUND_PACKAGE") + } + OneKeyLog.info("AppUpdate", "installAPK: APK file found, size=${file.length()} bytes") + + // Ensure verifyAPK was called before installation + val debugBuild = isDebuggable() + val expectedHash = verifiedFiles.remove(file.canonicalPath) + if (expectedHash == null) { + OneKeyLog.error("AppUpdate", "installAPK: APK was not verified before installation (no hash in verifiedFiles)") + if (!debugBuild) throw Exception("APK must be verified before installation") + OneKeyLog.warn("AppUpdate", "installAPK: DEBUG build — ignoring missing verification") + } + + // Re-verify file hash to prevent TOCTOU attacks (file swapped after verification) + if (expectedHash != null) { + OneKeyLog.info("AppUpdate", "installAPK: expected hash=${expectedHash.take(16)}..., re-verifying TOCTOU...") + val currentHash = computeSha256(file) + if (currentHash != expectedHash) { + OneKeyLog.error("AppUpdate", "installAPK: TOCTOU check FAILED — file was modified after verification (current=${currentHash.take(16)}..., expected=${expectedHash.take(16)}...)") + if (!debugBuild) throw Exception("APK file was modified after verification") + OneKeyLog.warn("AppUpdate", "installAPK: DEBUG build — ignoring TOCTOU mismatch") + } else { + OneKeyLog.info("AppUpdate", "installAPK: TOCTOU check passed, hash matches") + } + } + + // Parse APK info for logging + val pm = context.packageManager + val apkInfo = pm.getPackageArchiveInfo(file.absolutePath, 0) + if (apkInfo != null) { + val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) apkInfo.longVersionCode else @Suppress("DEPRECATION") apkInfo.versionCode.toLong() + OneKeyLog.info("AppUpdate", "installAPK: APK package=${apkInfo.packageName}, versionName=${apkInfo.versionName}, versionCode=$versionCode") + } + + OneKeyLog.info("AppUpdate", "installAPK: creating install intent (API level=${Build.VERSION.SDK_INT})...") + val intent = Intent(Intent.ACTION_VIEW) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val apkUri = FileProvider.getUriForFile( + context, + "${context.packageName}.appupdate.fileprovider", + file + ) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.setDataAndType(apkUri, "application/vnd.android.package-archive") + OneKeyLog.info("AppUpdate", "installAPK: using FileProvider URI=$apkUri") + } else { + intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive") + OneKeyLog.info("AppUpdate", "installAPK: using file URI") + } + context.startActivity(intent) + OneKeyLog.info("AppUpdate", "installAPK: install intent launched, awaiting user confirmation") + } + } + + override fun clearCache(): Promise { + return Promise.async { + OneKeyLog.info("AppUpdate", "clearCache: starting cleanup...") + isDownloading.set(false) + val verifiedCount = verifiedFiles.size + verifiedFiles.clear() + OneKeyLog.info("AppUpdate", "clearCache: reset download state, cleared $verifiedCount verified file entries") + + // Clean up downloaded APK and ASC files from cacheDir/apks/ directory + val context = NitroModules.applicationContext + if (context != null) { + val apkDir = File(context.cacheDir, "apks") + if (apkDir.exists()) { + val filesToDelete = apkDir.listFiles() ?: emptyArray() + OneKeyLog.info("AppUpdate", "clearCache: found ${filesToDelete.size} cached file(s) to delete in ${apkDir.absolutePath}") + var deletedCount = 0 + filesToDelete.forEach { file -> + val size = file.length() + if (file.delete()) { + OneKeyLog.debug("AppUpdate", "clearCache: deleted ${file.name} (${size} bytes)") + deletedCount++ + } else { + OneKeyLog.warn("AppUpdate", "clearCache: failed to delete ${file.name}") + } + } + OneKeyLog.info("AppUpdate", "clearCache: completed, deleted $deletedCount/${filesToDelete.size} files") + } else { + OneKeyLog.info("AppUpdate", "clearCache: apks cache directory does not exist, nothing to clean") + } + } else { + OneKeyLog.warn("AppUpdate", "clearCache: application context unavailable, skipping file cleanup") + } + } + } +} diff --git a/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdatePackage.kt b/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdatePackage.kt new file mode 100644 index 00000000..854d77ea --- /dev/null +++ b/native-modules/react-native-app-update/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdatePackage.kt @@ -0,0 +1,24 @@ +package com.margelo.nitro.reactnativeappupdate + +import com.facebook.react.BaseReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfoProvider + +class ReactNativeAppUpdatePackage : BaseReactPackage() { + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return null + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { HashMap() } + } + + companion object { + init { + System.loadLibrary("reactnativeappupdate") + } + } +} + + diff --git a/native-modules/react-native-app-update/android/src/main/res/xml/app_update_file_paths.xml b/native-modules/react-native-app-update/android/src/main/res/xml/app_update_file_paths.xml new file mode 100644 index 00000000..4e3516ee --- /dev/null +++ b/native-modules/react-native-app-update/android/src/main/res/xml/app_update_file_paths.xml @@ -0,0 +1,4 @@ + + + + diff --git a/native-modules/react-native-app-update/babel.config.js b/native-modules/react-native-app-update/babel.config.js new file mode 100644 index 00000000..12aaf61f --- /dev/null +++ b/native-modules/react-native-app-update/babel.config.js @@ -0,0 +1,13 @@ +module.exports = { + presets: ['@react-native/babel-preset'], + plugins: [ + [ + 'module-resolver', + { + alias: { + 'react-native-app-update': './src/index', + }, + }, + ], + ], +}; diff --git a/native-modules/react-native-app-update/eslint.config.mjs b/native-modules/react-native-app-update/eslint.config.mjs new file mode 100644 index 00000000..3416cf5c --- /dev/null +++ b/native-modules/react-native-app-update/eslint.config.mjs @@ -0,0 +1,5 @@ +import { createEslintConfig } from '@react-native/eslint-config'; + +export default createEslintConfig({ + extends: ['@react-native/eslint-config'], +}); diff --git a/native-modules/react-native-app-update/ios/ReactNativeAppUpdate.swift b/native-modules/react-native-app-update/ios/ReactNativeAppUpdate.swift new file mode 100644 index 00000000..17e9e366 --- /dev/null +++ b/native-modules/react-native-app-update/ios/ReactNativeAppUpdate.swift @@ -0,0 +1,45 @@ +import NitroModules +import ReactNativeNativeLogger + +class ReactNativeAppUpdate: HybridReactNativeAppUpdateSpec { + + private var nextListenerId: Int64 = 1 + + func downloadAPK(params: AppUpdateDownloadParams) throws -> Promise { + OneKeyLog.debug("AppUpdate", "downloadAPK not available on iOS") + return Promise.resolved(withResult: ()) + } + + func downloadASC(params: AppUpdateFileParams) throws -> Promise { + OneKeyLog.debug("AppUpdate", "downloadASC not available on iOS") + return Promise.resolved(withResult: ()) + } + + func verifyASC(params: AppUpdateFileParams) throws -> Promise { + OneKeyLog.debug("AppUpdate", "verifyASC not available on iOS") + return Promise.resolved(withResult: ()) + } + + func verifyAPK(params: AppUpdateFileParams) throws -> Promise { + OneKeyLog.debug("AppUpdate", "verifyAPK not available on iOS") + return Promise.resolved(withResult: ()) + } + + func installAPK(params: AppUpdateFileParams) throws -> Promise { + OneKeyLog.debug("AppUpdate", "installAPK not available on iOS") + return Promise.resolved(withResult: ()) + } + + func clearCache() throws -> Promise { + return Promise.resolved(withResult: ()) + } + + func addDownloadListener(callback: @escaping (DownloadEvent) -> Void) throws -> Double { + let id = nextListenerId + nextListenerId += 1 + return Double(id) + } + + func removeDownloadListener(id: Double) throws { + } +} diff --git a/native-modules/react-native-app-update/lefthook.yml b/native-modules/react-native-app-update/lefthook.yml new file mode 100644 index 00000000..89766b1f --- /dev/null +++ b/native-modules/react-native-app-update/lefthook.yml @@ -0,0 +1,10 @@ +pre-commit: + parallel: true + commands: + lint: + glob: "*.{js,ts,tsx}" + run: yarn lint {staged_files} + typecheck: + files: git diff --cached --name-only --diff-filter=ACMRTUXB + glob: "*.{js,ts,tsx}" + run: yarn typecheck diff --git a/native-modules/react-native-app-update/nitro.json b/native-modules/react-native-app-update/nitro.json new file mode 100644 index 00000000..4e8fb8a9 --- /dev/null +++ b/native-modules/react-native-app-update/nitro.json @@ -0,0 +1,17 @@ +{ + "cxxNamespace": ["reactnativeappupdate"], + "ios": { + "iosModuleName": "ReactNativeAppUpdate" + }, + "android": { + "androidNamespace": ["reactnativeappupdate"], + "androidCxxLibName": "reactnativeappupdate" + }, + "autolinking": { + "ReactNativeAppUpdate": { + "swift": "ReactNativeAppUpdate", + "kotlin": "ReactNativeAppUpdate" + } + }, + "ignorePaths": ["node_modules"] +} diff --git a/native-modules/react-native-app-update/package.json b/native-modules/react-native-app-update/package.json new file mode 100644 index 00000000..109406ab --- /dev/null +++ b/native-modules/react-native-app-update/package.json @@ -0,0 +1,169 @@ +{ + "name": "@onekeyfe/react-native-app-update", + "version": "1.1.24", + "description": "react-native-app-update", + "main": "./lib/module/index.js", + "types": "./lib/typescript/src/index.d.ts", + "exports": { + ".": { + "source": "./src/index.tsx", + "types": "./lib/typescript/src/index.d.ts", + "default": "./lib/module/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "src", + "lib", + "android", + "ios", + "cpp", + "nitrogen", + "nitro.json", + "*.podspec", + "react-native.config.js", + "!ios/build", + "!android/build", + "!android/gradle", + "!android/gradlew", + "!android/gradlew.bat", + "!android/local.properties", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__", + "!**/.*" + ], + "scripts": { + "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", + "prepare": "bob build", + "nitrogen": "nitrogen", + "typecheck": "tsc", + "lint": "eslint \"**/*.{js,ts,tsx}\"", + "test": "jest", + "release": "yarn prepare && npm whoami && npm publish --access public" + }, + "keywords": [ + "react-native", + "ios", + "android" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/OneKeyHQ/app-modules/react-native-app-update.git" + }, + "author": "onekeyfe (https://github.com/OneKeyHQ/app-modules)", + "license": "MIT", + "bugs": { + "url": "https://github.com/OneKeyHQ/app-modules/react-native-app-update/issues" + }, + "homepage": "https://github.com/OneKeyHQ/app-modules/react-native-app-update#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "devDependencies": { + "@commitlint/config-conventional": "^19.8.1", + "@eslint/compat": "^1.3.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.35.0", + "@react-native/babel-preset": "0.83.0", + "@react-native/eslint-config": "0.83.0", + "@release-it/conventional-changelog": "^10.0.1", + "@types/jest": "^29.5.14", + "@types/react": "^19.2.0", + "commitlint": "^19.8.1", + "del-cli": "^6.0.0", + "eslint": "^9.35.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "jest": "^29.7.0", + "lefthook": "^2.0.3", + "nitrogen": "0.31.10", + "prettier": "^2.8.8", + "react": "19.2.0", + "react-native": "0.83.0", + "react-native-builder-bob": "^0.40.13", + "react-native-nitro-modules": "0.33.2", + "release-it": "^19.0.4", + "turbo": "^2.5.6", + "typescript": "^5.9.2" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-nitro-modules": "0.33.2" + }, + "react-native-builder-bob": { + "source": "src", + "output": "lib", + "targets": [ + [ + "custom", + { + "script": "nitrogen", + "clean": "nitrogen/" + } + ], + [ + "module", + { + "esm": true + } + ], + [ + "typescript", + { + "project": "tsconfig.build.json" + } + ] + ] + }, + "prettier": { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + }, + "jest": { + "preset": "react-native", + "modulePathIgnorePatterns": [ + "/example/node_modules", + "/lib/" + ] + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "release-it": { + "git": { + "commitMessage": "chore: release ${version}", + "tagName": "v${version}" + }, + "npm": { + "publish": true + }, + "github": { + "release": true + }, + "plugins": { + "@release-it/conventional-changelog": { + "preset": { + "name": "angular" + } + } + } + }, + "create-react-native-library": { + "type": "nitro-module", + "languages": "kotlin-swift", + "tools": [ + "eslint", + "jest", + "lefthook", + "release-it" + ], + "version": "0.56.0" + } +} diff --git a/native-modules/react-native-app-update/src/ReactNativeAppUpdate.nitro.ts b/native-modules/react-native-app-update/src/ReactNativeAppUpdate.nitro.ts new file mode 100644 index 00000000..2e134a42 --- /dev/null +++ b/native-modules/react-native-app-update/src/ReactNativeAppUpdate.nitro.ts @@ -0,0 +1,30 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +export interface AppUpdateDownloadParams { + downloadUrl: string; + notificationTitle: string; + fileSize: number; +} + +export interface AppUpdateFileParams { + downloadUrl: string; +} + +export interface DownloadEvent { + type: string; + progress: number; + message: string; +} + +export interface ReactNativeAppUpdate + extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { + downloadAPK(params: AppUpdateDownloadParams): Promise; + downloadASC(params: AppUpdateFileParams): Promise; + verifyASC(params: AppUpdateFileParams): Promise; + verifyAPK(params: AppUpdateFileParams): Promise; + installAPK(params: AppUpdateFileParams): Promise; + clearCache(): Promise; + + addDownloadListener(callback: (event: DownloadEvent) => void): number; + removeDownloadListener(id: number): void; +} diff --git a/native-modules/react-native-app-update/src/index.tsx b/native-modules/react-native-app-update/src/index.tsx new file mode 100644 index 00000000..273854ec --- /dev/null +++ b/native-modules/react-native-app-update/src/index.tsx @@ -0,0 +1,8 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import type { ReactNativeAppUpdate as ReactNativeAppUpdateType } from './ReactNativeAppUpdate.nitro'; + +const ReactNativeAppUpdateHybridObject = + NitroModules.createHybridObject('ReactNativeAppUpdate'); + +export const ReactNativeAppUpdate = ReactNativeAppUpdateHybridObject; +export type * from './ReactNativeAppUpdate.nitro'; diff --git a/native-modules/react-native-app-update/tsconfig.build.json b/native-modules/react-native-app-update/tsconfig.build.json new file mode 100644 index 00000000..45777014 --- /dev/null +++ b/native-modules/react-native-app-update/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "declarationMap": true + }, + "exclude": ["**/__tests__/**/*", "**/__fixtures__/**/*", "**/__mocks__/**/*"] +} diff --git a/native-modules/react-native-app-update/tsconfig.json b/native-modules/react-native-app-update/tsconfig.json new file mode 100644 index 00000000..89a63364 --- /dev/null +++ b/native-modules/react-native-app-update/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "rootDir": ".", + "paths": { + "react-native-app-update": ["./src/index"] + }, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "customConditions": ["react-native-strict-api"], + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "lib": ["ESNext"], + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext", + "verbatimModuleSyntax": true + } +} diff --git a/native-modules/react-native-app-update/turbo.json b/native-modules/react-native-app-update/turbo.json new file mode 100644 index 00000000..08b9676e --- /dev/null +++ b/native-modules/react-native-app-update/turbo.json @@ -0,0 +1,17 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "outputs": [ + "lib/**", + "nitrogen/**" + ], + "inputs": [ + "src/**", + "android/src/**", + "ios/**", + "cpp/**" + ] + } + } +} diff --git a/native-modules/react-native-background-thread/BackgroundThread.podspec b/native-modules/react-native-background-thread/BackgroundThread.podspec index 7754237c..c3cf0a71 100644 --- a/native-modules/react-native-background-thread/BackgroundThread.podspec +++ b/native-modules/react-native-background-thread/BackgroundThread.podspec @@ -16,5 +16,7 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,swift,cpp}" s.public_header_files = "ios/**/*.h" + s.dependency 'ReactNativeNativeLogger' + install_modules_dependencies(s) end diff --git a/native-modules/react-native-background-thread/README.md b/native-modules/react-native-background-thread/README.md deleted file mode 100644 index 1dd1b131..00000000 --- a/native-modules/react-native-background-thread/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# react-native-background-thread - -react-native-background-thread - -## Installation - - -```sh -npm install react-native-background-thread -``` - - -## Usage - - -```js -import { multiply } from 'react-native-background-thread'; - -// ... - -const result = multiply(3, 7); -``` - - -## Contributing - -- [Development workflow](CONTRIBUTING.md#development-workflow) -- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request) -- [Code of conduct](CODE_OF_CONDUCT.md) - -## License - -MIT - ---- - -Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) diff --git a/native-modules/react-native-background-thread/ios/BTLogger.h b/native-modules/react-native-background-thread/ios/BTLogger.h new file mode 100644 index 00000000..2a0b5a25 --- /dev/null +++ b/native-modules/react-native-background-thread/ios/BTLogger.h @@ -0,0 +1,16 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Lightweight logging wrapper that dynamically dispatches to OneKeyLog. +/// Avoids `@import ReactNativeNativeLogger` which fails in .mm (Objective-C++) files. +@interface BTLogger : NSObject + ++ (void)debug:(NSString *)message; ++ (void)info:(NSString *)message; ++ (void)warn:(NSString *)message; ++ (void)error:(NSString *)message; + +@end + +NS_ASSUME_NONNULL_END diff --git a/native-modules/react-native-background-thread/ios/BTLogger.m b/native-modules/react-native-background-thread/ios/BTLogger.m new file mode 100644 index 00000000..6556e9f8 --- /dev/null +++ b/native-modules/react-native-background-thread/ios/BTLogger.m @@ -0,0 +1,42 @@ +#import "BTLogger.h" + +static NSString *const kTag = @"BackgroundThread"; + +@implementation BTLogger + ++ (void)debug:(NSString *)message { + [self _log:@"debug::" message:message]; +} + ++ (void)info:(NSString *)message { + [self _log:@"info::" message:message]; +} + ++ (void)warn:(NSString *)message { + [self _log:@"warn::" message:message]; +} + ++ (void)error:(NSString *)message { + [self _log:@"error::" message:message]; +} + +#pragma mark - Private + ++ (void)_log:(NSString *)selectorName message:(NSString *)message { + Class logClass = NSClassFromString(@"ReactNativeNativeLogger.OneKeyLog"); + if (!logClass) { + logClass = NSClassFromString(@"OneKeyLog"); + } + if (!logClass) { + return; + } + SEL sel = NSSelectorFromString(selectorName); + if (![logClass respondsToSelector:sel]) { + return; + } + typedef void (*LogFunc)(id, SEL, NSString *, NSString *); + LogFunc func = (LogFunc)[logClass methodForSelector:sel]; + func(logClass, sel, kTag, message); +} + +@end diff --git a/native-modules/react-native-background-thread/ios/BackgroundRunnerReactNativeDelegate.mm b/native-modules/react-native-background-thread/ios/BackgroundRunnerReactNativeDelegate.mm index aab4e305..503c712a 100644 --- a/native-modules/react-native-background-thread/ios/BackgroundRunnerReactNativeDelegate.mm +++ b/native-modules/react-native-background-thread/ios/BackgroundRunnerReactNativeDelegate.mm @@ -1,4 +1,5 @@ #import "BackgroundRunnerReactNativeDelegate.h" +#import "BTLogger.h" #include #include @@ -156,45 +157,20 @@ - (void)postMessage:(const std::string &)message _onMessageSandbox->call(runtime, {std::move(parsedValue)}); } catch (const jsi::JSError &e) { - // if (self.eventEmitter && self.hasOnErrorHandler) { - // SandboxReactNativeViewEventEmitter::OnError errorEvent = { - // .isFatal = false, .name = "JSError", .message = e.getMessage(), .stack = e.getStack()}; - // self.eventEmitter->onError(errorEvent); - // } + [BTLogger error:[NSString stringWithFormat:@"JSError during postMessage: %s", e.getMessage().c_str()]]; } catch (const std::exception &e) { - // if (self.eventEmitter && self.hasOnErrorHandler) { - // SandboxReactNativeViewEventEmitter::OnError errorEvent = { - // .isFatal = false, .name = "RuntimeError", .message = e.what(), .stack = ""}; - // self.eventEmitter->onError(errorEvent); - // } + [BTLogger error:[NSString stringWithFormat:@"RuntimeError during postMessage: %s", e.what()]]; } catch (...) { - NSLog(@"[BackgroundReactNativeDelegate] Runtime invalid during postMessage for sandbox %s", _origin.c_str()); + [BTLogger error:[NSString stringWithFormat:@"Runtime invalid during postMessage for sandbox %s", _origin.c_str()]]; } }]; } - (bool)routeMessage:(const std::string &)message toSandbox:(const std::string &)targetId { -// auto ®istry = SandboxRegistry::getInstance(); -// auto target = registry.find(targetId); -// if (!target) { -// return false; -// } - -// // Check if the current sandbox is permitted to send messages to the target -// if (!registry.isPermittedFrom(_origin, targetId)) { -// // if (self.eventEmitter && self.hasOnErrorHandler) { -// // std::string errorMessage = -// // fmt::format("Access denied: Sandbox '{}' is not permitted to send messages to '{}'", _origin, targetId); -// // SandboxReactNativeViewEventEmitter::OnError errorEvent = { -// // .isFatal = false, .name = "AccessDeniedError", .message = errorMessage, .stack = ""}; -// // self.eventEmitter->onError(errorEvent); -// // } -// return false; -// } - -// target->postMessage(message); - return true; + // Sandbox routing is not yet implemented. Deny all cross-sandbox messages by default. + [BTLogger warn:@"routeMessage denied: sandbox routing not implemented"]; + return false; } - (void)hostDidStart:(RCTHost *)host diff --git a/native-modules/react-native-background-thread/ios/BackgroundThread.mm b/native-modules/react-native-background-thread/ios/BackgroundThread.mm index 1146b59f..81e51998 100644 --- a/native-modules/react-native-background-thread/ios/BackgroundThread.mm +++ b/native-modules/react-native-background-thread/ios/BackgroundThread.mm @@ -1,5 +1,6 @@ #import "BackgroundThread.h" #import "BackgroundThreadManager.h" +#import "BTLogger.h" @implementation BackgroundThread @@ -8,6 +9,7 @@ @implementation BackgroundThread - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { + [BTLogger info:@"BackgroundThread module initialized"]; return std::make_shared(params); } diff --git a/native-modules/react-native-background-thread/ios/BackgroundThreadManager.mm b/native-modules/react-native-background-thread/ios/BackgroundThreadManager.mm index ba13483b..16a19630 100644 --- a/native-modules/react-native-background-thread/ios/BackgroundThreadManager.mm +++ b/native-modules/react-native-background-thread/ios/BackgroundThreadManager.mm @@ -13,6 +13,7 @@ #import "RCTAppDependencyProvider.h" #endif #import "BackgroundRunnerReactNativeDelegate.h" +#import "BTLogger.h" @interface BackgroundThreadManager () @property (nonatomic, strong) BackgroundReactNativeDelegate *reactNativeFactoryDelegate; @@ -50,7 +51,12 @@ - (instancetype)init { #pragma mark - Public Methods - (void)startBackgroundRunner { +#if DEBUG [self startBackgroundRunnerWithEntryURL:MODULE_DEBUG_URL]; +#else + // In production, use the bundled background.bundle (not the debug HTTP URL) + [self startBackgroundRunnerWithEntryURL:@"background.bundle"]; +#endif } - (void)startBackgroundRunnerWithEntryURL:(NSString *)entryURL { @@ -58,7 +64,8 @@ - (void)startBackgroundRunnerWithEntryURL:(NSString *)entryURL { return; } self.isStarted = YES; - + [BTLogger info:[NSString stringWithFormat:@"Starting background runner with entryURL: %@", entryURL]]; + dispatch_async(dispatch_get_main_queue(), ^{ NSDictionary *initialProperties = @{}; NSDictionary *launchOptions = @{}; diff --git a/native-modules/react-native-background-thread/package.json b/native-modules/react-native-background-thread/package.json index 23dd24af..cd84a984 100644 --- a/native-modules/react-native-background-thread/package.json +++ b/native-modules/react-native-background-thread/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-background-thread", - "version": "1.1.19", + "version": "1.1.24", "description": "react-native-background-thread", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-bundle-update/.gitignore b/native-modules/react-native-bundle-update/.gitignore new file mode 100644 index 00000000..357a2f9a --- /dev/null +++ b/native-modules/react-native-bundle-update/.gitignore @@ -0,0 +1,89 @@ +# OSX +# +.DS_Store + +# XDE +.expo/ + +# VSCode +.vscode/ +jsconfig.json + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace +**/.xcode.env.local + +# Android/IJ +# +.classpath +.cxx +.gradle +.idea +.project +.settings +local.properties +android.iml + +# Cocoapods +# +example/ios/Pods + +# Ruby +example/vendor/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +android/app/libs +android/keystores/debug.keystore + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Expo +.expo/ + +# Turborepo +.turbo/ + +# generated by bob +lib/ + +# React Native Codegen +ios/generated +android/generated + +# React Native Nitro Modules +nitrogen/ + + + diff --git a/native-modules/react-native-bundle-update/LICENSE b/native-modules/react-native-bundle-update/LICENSE new file mode 100644 index 00000000..24ee4114 --- /dev/null +++ b/native-modules/react-native-bundle-update/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 OneKey + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/native-modules/react-native-bundle-update/ReactNativeBundleUpdate.podspec b/native-modules/react-native-bundle-update/ReactNativeBundleUpdate.podspec new file mode 100644 index 00000000..defb8f3f --- /dev/null +++ b/native-modules/react-native-bundle-update/ReactNativeBundleUpdate.podspec @@ -0,0 +1,34 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "ReactNativeBundleUpdate" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => min_ios_version_supported } + s.source = { :git => "https://github.com/OneKeyHQ/app-modules/react-native-bundle-update.git", :tag => "#{s.version}" } + + s.source_files = [ + "ios/**/*.{swift}", + "ios/**/*.{m,mm}", + "cpp/**/*.{hpp,cpp}", + ] + + s.vendored_frameworks = 'ios/Frameworks/Gopenpgp.xcframework' + + s.dependency 'React-jsi' + s.dependency 'React-callinvoker' + s.dependency 'ReactNativeNativeLogger' + s.dependency 'SSZipArchive', '~> 2.4' + s.dependency 'MMKV', '~> 2.2' + + load 'nitrogen/generated/ios/ReactNativeBundleUpdate+autolinking.rb' + add_nitrogen_files(s) + + install_modules_dependencies(s) +end diff --git a/native-modules/react-native-bundle-update/android/CMakeLists.txt b/native-modules/react-native-bundle-update/android/CMakeLists.txt new file mode 100644 index 00000000..c1dcdfbb --- /dev/null +++ b/native-modules/react-native-bundle-update/android/CMakeLists.txt @@ -0,0 +1,24 @@ +project(reactnativebundleupdate) +cmake_minimum_required(VERSION 3.9.0) + +set(PACKAGE_NAME reactnativebundleupdate) +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_CXX_STANDARD 20) + +# Define C++ library and add all sources +add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp) + +# Add Nitrogen specs :) +include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/reactnativebundleupdate+autolinking.cmake) + +# Set up local includes +include_directories("src/main/cpp" "../cpp") + +find_library(LOG_LIB log) + +# Link all libraries together +target_link_libraries( + ${PACKAGE_NAME} + ${LOG_LIB} + android # <-- Android core +) diff --git a/native-modules/react-native-bundle-update/android/build.gradle b/native-modules/react-native-bundle-update/android/build.gradle new file mode 100644 index 00000000..a9986cd7 --- /dev/null +++ b/native-modules/react-native-bundle-update/android/build.gradle @@ -0,0 +1,139 @@ +buildscript { + ext.getExtOrDefault = {name -> + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['ReactNativeBundleUpdate_' + name] + } + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:8.7.2" + // noinspection DifferentKotlinGradleVersion + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}" + } +} + +def reactNativeArchitectures() { + def value = rootProject.getProperties().get("reactNativeArchitectures") + return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" +apply from: '../nitrogen/generated/android/reactnativebundleupdate+autolinking.gradle' + +apply plugin: "com.facebook.react" + +def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ReactNativeBundleUpdate_" + name]).toInteger() +} + +android { + namespace "com.margelo.nitro.reactnativebundleupdate" + + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + + defaultConfig { + minSdkVersion getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + + externalNativeBuild { + cmake { + cppFlags "-frtti -fexceptions -Wall -fstack-protector-all" + arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" + abiFilters (*reactNativeArchitectures()) + + buildTypes { + debug { + cppFlags "-O1 -g" + } + release { + cppFlags "-O2" + } + } + } + } + } + + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } + } + + packagingOptions { + excludes = [ + "META-INF", + "META-INF/**", + "**/libc++_shared.so", + "**/libfbjni.so", + "**/libjsi.so", + "**/libfolly_json.so", + "**/libfolly_runtime.so", + "**/libglog.so", + "**/libhermes.so", + "**/libhermes-executor-debug.so", + "**/libhermes_executor.so", + "**/libreactnative.so", + "**/libreactnativejni.so", + "**/libturbomodulejsijni.so", + "**/libreact_nativemodule_core.so", + "**/libjscexecutor.so" + ] + } + + buildFeatures { + buildConfig true + prefab true + } + + buildTypes { + release { + minifyEnabled false + } + } + + lintOptions { + disable "GradleCompatible" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs += [ + "generated/java", + "generated/jni" + ] + } + } +} + +repositories { + mavenCentral() + google() +} + +def kotlin_version = getExtOrDefault("kotlinVersion") + +dependencies { + implementation "com.facebook.react:react-android" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation project(":react-native-nitro-modules") + + implementation project(":onekeyfe_react-native-native-logger") + + implementation "com.squareup.okhttp3:okhttp:4.12.0" + + // BouncyCastle for GPG/PGP signature verification + implementation "org.bouncycastle:bcpg-jdk15to18:1.78.1" + implementation "org.bouncycastle:bcprov-jdk15to18:1.78.1" + + // MMKV for reading DevSettings (compileOnly: provided by the host app via react-native-mmkv) + compileOnly "io.github.zhongwuzw:mmkv:2.2.4" +} diff --git a/native-modules/react-native-bundle-update/android/gradle.properties b/native-modules/react-native-bundle-update/android/gradle.properties new file mode 100644 index 00000000..e936e5ba --- /dev/null +++ b/native-modules/react-native-bundle-update/android/gradle.properties @@ -0,0 +1,4 @@ +ReactNativeBundleUpdate_kotlinVersion=1.9.25 +ReactNativeBundleUpdate_compileSdkVersion=35 +ReactNativeBundleUpdate_targetSdkVersion=35 +ReactNativeBundleUpdate_minSdkVersion=24 diff --git a/native-modules/react-native-bundle-update/android/src/main/AndroidManifest.xml b/native-modules/react-native-bundle-update/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..94cbbcfc --- /dev/null +++ b/native-modules/react-native-bundle-update/android/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/native-modules/react-native-bundle-update/android/src/main/cpp/cpp-adapter.cpp b/native-modules/react-native-bundle-update/android/src/main/cpp/cpp-adapter.cpp new file mode 100644 index 00000000..7681b571 --- /dev/null +++ b/native-modules/react-native-bundle-update/android/src/main/cpp/cpp-adapter.cpp @@ -0,0 +1,6 @@ +#include +#include "reactnativebundleupdateOnLoad.hpp" + +extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { + return margelo::nitro::reactnativebundleupdate::initialize(vm); +} diff --git a/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt b/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt new file mode 100644 index 00000000..f25543b9 --- /dev/null +++ b/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdate.kt @@ -0,0 +1,1409 @@ +package com.margelo.nitro.reactnativebundleupdate + +import android.content.Context +import android.content.SharedPreferences +import android.os.Build +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.Promise +import com.margelo.nitro.NitroModules +import com.margelo.nitro.nativelogger.OneKeyLog +import com.tencent.mmkv.MMKV +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.BufferedInputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.security.MessageDigest +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection +import org.bouncycastle.openpgp.PGPUtil +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.json.JSONArray +import org.json.JSONObject + +private data class BundleListener( + val id: Double, + val callback: (BundleDownloadEvent) -> Unit +) + +// OneKey GPG public key for signature verification +private const val GPG_PUBLIC_KEY = """-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGJATGwBEADL1K7b8dzYYzlSsvAGiA8mz042pygB7AAh/uFUycpNQdSzuoDE +VoXq/QsXCOsGkMdFLwlUjarRaxFX6RTV6S51LOlJFRsyGwXiMz08GSNagSafQ0YL +Gi+aoemPh6Ta5jWgYGIUWXavkjJciJYw43ACMdVmIWos94bA41Xm93dq9C3VRpl+ +EjvGAKRUMxJbH8r13TPzPmfN4vdrHLq+us7eKGJpwV/VtD9vVHAi0n48wGRq7DQw +IUDU2mKy3wmjwS38vIIu4yQyeUdl4EqwkCmGzWc7Cv2HlOG6rLcUdTAOMNBBX1IQ +iHKg9Bhh96MXYvBhEL7XHJ96S3+gTHw/LtrccBM+eiDJVHPZn+lw2HqX994DueLV +tAFDS+qf3ieX901IC97PTHsX6ztn9YZQtSGBJO3lEMBdC4ez2B7zUv4bgyfU+KvE +zHFIK9HmDehx3LoDAYc66nhZXyasiu6qGPzuxXu8/4qTY8MnhXJRBkbWz5P84fx1 +/Db5WETLE72on11XLreFWmlJnEWN4UOARrNn1Zxbwl+uxlSJyM+2GTl4yoccG+WR +uOUCmRXTgduHxejPGI1PfsNmFpVefAWBDO7SdnwZb1oUP3AFmhH5CD1GnmLnET+l +/c+7XfFLwgSUVSADBdO3GVS4Cr9ux4nIrHGJCrrroFfM2yvG8AtUVr16PQARAQAB +tCJvbmVrZXlocSBkZXZlbG9wZXIgPGRldkBvbmVrZXkuc28+iQJUBBMBCAA+FiEE +62iuVE8f3YzSZGJPs2mmepC/OHsFAmJATGwCGwMFCQeGH0QFCwkIBwIGFQoJCAsC +BBYCAwECHgECF4AACgkQs2mmepC/OHtgvg//bsWFMln08ZJjf5od/buJua7XYb3L +jWq1H5rdjJva5TP1UuQaDULuCuPqllxb+h+RB7g52yRG/1nCIrpTfveYOVtq/mYE +D12KYAycDwanbmtoUp25gcKqCrlNeSE1EXmPlBzyiNzxJutE1DGlvbY3rbuNZLQi +UTFBG3hk6JgsaXkFCwSmF95uATAaItv8aw6eY7RWv47rXhQch6PBMCir4+a/v7vs +lXxQtcpCqfLtjrloq7wvmD423yJVsUGNEa7/BrwFz6/GP6HrUZc6JgvrieuiBE4n +ttXQFm3dkOfD+67MLMO3dd7nPhxtjVEGi+43UH3/cdtmU4JFX3pyCQpKIlXTEGp2 +wqim561auKsRb1B64qroCwT7aACwH0ZTgQS8rPifG3QM8ta9QheuOsjHLlqjo8jI +fpqe0vKYUlT092joT0o6nT2MzmLmHUW0kDqD9p6JEJEZUZpqcSRE84eMTFNyu966 +xy/rjN2SMJTFzkNXPkwXYrMYoahGez1oZfLzV6SQ0+blNc3aATt9aQW6uaCZtMw1 +ibcfWW9neHVpRtTlMYCoa2reGaBGCv0Nd8pMcyFUQkVaes5cQHkh3r5Dba+YrVvp +l4P8HMbN8/LqAv7eBfj3ylPa/8eEPWVifcum2Y9TqherN1C2JDqWIpH4EsApek3k +NMK6q0lPxXjZ3Pa5Ag0EYkBMbAEQAM1R4N3bBkwKkHeYwsQASevUkHwY4eg6Ncgp +f9NbmJHcEioqXTIv0nHCQbos3P2NhXvDowj4JFkK/ZbpP9yo0p7TI4fckseVSWwI +tiF9l/8OmXvYZMtw3hHcUUZVdJnk0xrqT6ni6hyRFIfbqous6/vpqi0GG7nB/+lU +E5StGN8696ZWRyAX9MmwoRoods3ShNJP0+GCYHfIcG0XRhEDMJph+7mWPlkQUcza +4aEjxOQ4Stwwp+ZL1rXSlyJIPk1S9/FIS/Uw5GgqFJXIf5n+SCVtUZ8lGedEWwe4 +wXsoPFxxOc2Gqw5r4TrJFdgA3MptYebXmb2LGMssXQTM1AQS2LdpnWw44+X1CHvQ +0m4pEw/g2OgeoJPBurVUnu2mU/M+ARZiS4ceAR0pLZN7Yq48p1wr6EOBQdA3Usby +uc17MORG/IjRmjz4SK/luQLXjN+0jwQSoM1kcIHoRk37B8feHjVufJDKlqtw83H1 +uNu6lGwb8MxDgTuuHloDijCDQsn6m7ZKU1qqLDGtdvCUY2ovzuOUS9vv6MAhR86J +kqoU3sOBMeQhnBaTNKU0IjT4M+ERCWQ7MewlzXuPHgyb4xow1SKZny+f+fYXPy9+ +hx4/j5xaKrZKdq5zIo+GRGe4lA088l253nGeLgSnXsbSxqADqKK73d7BXLCVEZHx +f4Sa5JN7ABEBAAGJAjwEGAEIACYWIQTraK5UTx/djNJkYk+zaaZ6kL84ewUCYkBM +bAIbDAUJB4YfRAAKCRCzaaZ6kL84e0UGD/4mVWyGoQC86TyPoU4Pb5r8mynXWmiH +ZGKu2ll8qn3l5Q67OophgbA1I0GTBFsYK2f91ahgs7FEsLrmz/25E8ybcdJipITE +6869nyE1b37jVb3z3BJLYS/4MaNvugNz4VjMHWVAL52glXLN+SJBSNscmWZDKnVn +Rnrn+kBEvOWZgLbi4MpPiNVwm2PGnrtPzudTcg/NS3HOcmJTfG3mrnwwNJybTVAx +txlQPoXUpJQqJjtkPPW+CqosolpRdugQ5zpFSg05iL+vN+CMrVPkk85w87dtsidl +yZl/ZNITrLzym9d2UFVQZY2rRohNdRfx3l4rfXJFLaqQtihRvBIiMKTbUb2V0pd3 +rVLz2Ck3gJqPfPEEmCWS0Nx6rME8m0sOkNyMau3dMUUAs4j2c3pOQmsZRjKo7LAc +7/GahKFhZ2aBCQzvcTES+gPH1Z5HnivkcnUF2gnQV9x7UOr1Q/euKJsxPl5CCZtM +N9GFW10cDxFo7cO5Ch+/BkkkfebuI/4Wa1SQTzawsxTx4eikKwcemgfDsyIqRs2W +62PBrqCzs9Tg19l35sCdmvYsvMadrYFXukHXiUKEpwJMdTLAtjJ+AX84YLwuHi3+ +qZ5okRCqZH+QpSojSScT9H5ze4ZpuP0d8pKycxb8M2RfYdyOtT/eqsZ/1EQPg7kq +P2Q5dClenjjjVA== +=F0np +-----END PGP PUBLIC KEY BLOCK-----""" + +// Public static store for CustomReactNativeHost access (called before JS starts) +object BundleUpdateStoreAndroid { + private val bcProvider = BouncyCastleProvider() + private const val PREFS_NAME = "BundleUpdatePrefs" + private const val NATIVE_VERSION_PREFS_NAME = "NativeVersionPrefs" + private const val CURRENT_BUNDLE_VERSION_KEY = "currentBundleVersion" + + fun getDownloadBundleDir(context: Context): String { + val dir = File(context.filesDir, "onekey-bundle-download") + if (!dir.exists()) dir.mkdirs() + return dir.absolutePath + } + + fun getBundleDir(context: Context): String { + val dir = File(context.filesDir, "onekey-bundle") + if (!dir.exists()) dir.mkdirs() + return dir.absolutePath + } + + fun getAscDir(context: Context): String { + val dir = File(getBundleDir(context), "asc") + if (!dir.exists()) dir.mkdirs() + return dir.absolutePath + } + + fun getSignatureFilePath(context: Context, version: String): String { + return File(getAscDir(context), "$version-signature.asc").absolutePath + } + + fun writeSignatureFile(context: Context, version: String, signature: String) { + val file = File(getSignatureFilePath(context, version)) + val existed = file.exists() + file.parentFile?.mkdirs() + file.writeText(signature, Charsets.UTF_8) + OneKeyLog.info("BundleUpdate", "writeSignatureFile: version=$version, existed=$existed, size=${file.length()}, path=${file.absolutePath}") + } + + fun readSignatureFile(context: Context, version: String): String { + val file = File(getSignatureFilePath(context, version)) + if (!file.exists()) { + OneKeyLog.debug("BundleUpdate", "readSignatureFile: not found for version=$version") + return "" + } + return try { + val content = file.readText(Charsets.UTF_8) + OneKeyLog.debug("BundleUpdate", "readSignatureFile: version=$version, size=${content.length}") + content + } catch (e: Exception) { + OneKeyLog.error("BundleUpdate", "readSignatureFile: failed to read $version: ${e.message}") + "" + } + } + + fun deleteSignatureFile(context: Context, version: String) { + val file = File(getSignatureFilePath(context, version)) + if (file.exists()) file.delete() + } + + fun getCurrentBundleVersion(context: Context): String? { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + return prefs.getString(CURRENT_BUNDLE_VERSION_KEY, null) + } + + fun setCurrentBundleVersionAndSignature(context: Context, version: String, signature: String?) { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val currentVersion = prefs.getString(CURRENT_BUNDLE_VERSION_KEY, "") + val editor = prefs.edit() + editor.putString(CURRENT_BUNDLE_VERSION_KEY, version) + // Remove old signature key from prefs (legacy cleanup) + if (!currentVersion.isNullOrEmpty()) { + editor.remove(currentVersion) + } + editor.apply() + + // Store signature to file + if (!signature.isNullOrEmpty()) { + writeSignatureFile(context, version, signature) + } + } + + fun clearUpdateBundleData(context: Context) { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + prefs.edit().clear().apply() + // Clear all signature files + val ascDir = File(getAscDir(context)) + if (ascDir.exists()) { + ascDir.listFiles()?.forEach { it.delete() } + } + } + + fun getCurrentBundleDir(context: Context, currentBundleVersion: String?): String? { + if (currentBundleVersion == null) return null + return File(getBundleDir(context), currentBundleVersion).absolutePath + } + + fun getAppVersion(context: Context): String? { + return try { + val pm = context.packageManager + val pi = pm.getPackageInfo(context.packageName, 0) + pi.versionName + } catch (e: Exception) { + OneKeyLog.error("BundleUpdate", "Error getting package info: ${e.message}") + null + } + } + + fun getNativeVersion(context: Context): String { + val prefs = context.getSharedPreferences(NATIVE_VERSION_PREFS_NAME, Context.MODE_PRIVATE) + return prefs.getString("nativeVersion", "") ?: "" + } + + fun setNativeVersion(context: Context, nativeVersion: String) { + val prefs = context.getSharedPreferences(NATIVE_VERSION_PREFS_NAME, Context.MODE_PRIVATE) + prefs.edit().putString("nativeVersion", nativeVersion).apply() + } + + fun calculateSHA256(filePath: String): String? { + return try { + val digest = MessageDigest.getInstance("SHA-256") + BufferedInputStream(FileInputStream(filePath)).use { bis -> + val buffer = ByteArray(8192) + var count: Int + while (bis.read(buffer).also { count = it } > 0) { + digest.update(buffer, 0, count) + } + } + bytesToHex(digest.digest()) + } catch (e: Exception) { + OneKeyLog.error("BundleUpdate", "Error calculating SHA256: ${e.message}") + null + } + } + + private fun bytesToHex(bytes: ByteArray): String { + return bytes.joinToString("") { "%02x".format(it) } + } + + fun getMetadataFilePath(context: Context, currentBundleVersion: String?): String? { + if (currentBundleVersion == null) return null + val file = File(File(getBundleDir(context), currentBundleVersion), "metadata.json") + return if (file.exists()) file.absolutePath else null + } + + fun getMetadataFileContent(context: Context, currentBundleVersion: String?): String? { + val path = getMetadataFilePath(context, currentBundleVersion) ?: return null + return readFileContent(File(path)) + } + + fun parseMetadataJson(jsonContent: String): Map { + val metadata = mutableMapOf() + try { + val obj = JSONObject(jsonContent) + val keys = obj.keys() + while (keys.hasNext()) { + val key = keys.next() + metadata[key] = obj.getString(key) + } + } catch (e: Exception) { + OneKeyLog.error("BundleUpdate", "Error parsing metadata JSON: ${e.message}") + throw Exception("Failed to parse metadata.json: ${e.message}") + } + if (metadata.isEmpty()) { + throw Exception("metadata.json is empty or contains no file entries") + } + return metadata + } + + fun readMetadataFileSha256(signature: String?): String? { + if (signature.isNullOrEmpty()) return null + + // GPG cleartext signature verification is required + val gpgResult = verifyGPGAndExtractSha256(signature) + if (gpgResult != null) return gpgResult + + OneKeyLog.error("BundleUpdate", "readMetadataFileSha256: GPG verification failed, rejecting unsigned content") + return null + } + + /** Constant-time comparison to prevent timing attacks on hash values */ + fun secureCompare(a: String, b: String): Boolean { + val aBytes = a.toByteArray(Charsets.UTF_8) + val bBytes = b.toByteArray(Charsets.UTF_8) + if (aBytes.size != bBytes.size) return false + var result = 0 + for (i in aBytes.indices) { + result = result or (aBytes[i].toInt() xor bBytes[i].toInt()) + } + return result == 0 + } + + fun isSafeVersionString(version: String): Boolean { + return version.isNotEmpty() && version.all { it.isLetterOrDigit() || it == '.' || it == '-' || it == '_' } + } + + /** + * Verify a PGP cleartext-signed message using BouncyCastle and extract the sha256. + * Returns null if verification fails or the signature is not a PGP cleartext message. + */ + fun verifyGPGAndExtractSha256(signature: String): String? { + if (!signature.contains("-----BEGIN PGP SIGNED MESSAGE-----")) return null + + return try { + // Parse the cleartext signed message + val inputStream = signature.byteInputStream() + val pgpFactory = JcaPGPObjectFactory(PGPUtil.getDecoderStream(inputStream)) + + // Parse the public key + val pubKeyStream = PGPUtil.getDecoderStream(GPG_PUBLIC_KEY.byteInputStream()) + val pgpPubKeyRingCollection = PGPPublicKeyRingCollection(pubKeyStream, JcaKeyFingerprintCalculator()) + + // Extract cleartext and signature from the PGP signed message manually + // BouncyCastle's cleartext handling requires manual parsing + val lines = signature.lines() + val hashHeaderIdx = lines.indexOfFirst { it.startsWith("Hash:") } + val sigStartIdx = lines.indexOfFirst { it == "-----BEGIN PGP SIGNATURE-----" } + val sigEndIdx = lines.indexOfFirst { it == "-----END PGP SIGNATURE-----" } + + if (hashHeaderIdx < 0 || sigStartIdx < 0 || sigEndIdx < 0) { + OneKeyLog.error("BundleUpdate", "Invalid PGP cleartext signed message format") + return null + } + + // The cleartext body is between the Hash header blank line and the PGP SIGNATURE block + val bodyStartIdx = hashHeaderIdx + 2 // skip Hash: line and the blank line after it + val bodyLines = lines.subList(bodyStartIdx, sigStartIdx) + // Remove trailing empty line that PGP adds + val cleartextBody = bodyLines.joinToString("\r\n").trimEnd() + + // The signature block + val sigBlock = lines.subList(sigStartIdx, sigEndIdx + 1).joinToString("\n") + + // Decode the signature + val sigInputStream = PGPUtil.getDecoderStream(sigBlock.byteInputStream()) + val sigFactory = JcaPGPObjectFactory(sigInputStream) + val signatureList = sigFactory.nextObject() + + if (signatureList !is org.bouncycastle.openpgp.PGPSignatureList || signatureList.isEmpty) { + OneKeyLog.error("BundleUpdate", "No PGP signature found in message") + return null + } + + val pgpSignature = signatureList[0] + val keyId = pgpSignature.keyID + + // Find the public key + val publicKey = pgpPubKeyRingCollection.getPublicKey(keyId) + if (publicKey == null) { + OneKeyLog.error("BundleUpdate", "Public key not found for keyId: ${java.lang.Long.toHexString(keyId)}") + return null + } + + // Verify the signature + pgpSignature.init(JcaPGPContentVerifierBuilderProvider().setProvider(bcProvider), publicKey) + + // Dash-unescape the cleartext per RFC 4880 Section 7.1 + val unescapedLines = cleartextBody.lines().map { line -> + if (line.startsWith("- ")) line.substring(2) else line + } + val dataToVerify = unescapedLines.joinToString("\r\n").toByteArray(Charsets.UTF_8) + pgpSignature.update(dataToVerify) + + if (!pgpSignature.verify()) { + OneKeyLog.error("BundleUpdate", "GPG signature verification failed") + return null + } + + // Signature verified, parse the cleartext JSON + val json = JSONObject(cleartextBody.trim()) + val sha256 = json.optString("sha256", "") + if (sha256.isEmpty()) { + OneKeyLog.error("BundleUpdate", "No sha256 field in signed cleartext JSON") + return null + } + + OneKeyLog.info("BundleUpdate", "GPG verification succeeded, sha256: $sha256") + sha256 + } catch (e: Exception) { + OneKeyLog.error("BundleUpdate", "GPG verification error: ${e.message}") + null + } + } + + fun validateMetadataFileSha256(context: Context, currentBundleVersion: String, signature: String?): Boolean { + val metadataFilePath = getMetadataFilePath(context, currentBundleVersion) ?: run { + OneKeyLog.debug("BundleUpdate", "metadataFilePath is null") + return false + } + val extractedSha256 = readMetadataFileSha256(signature) + if (extractedSha256.isNullOrEmpty()) return false + val calculated = calculateSHA256(metadataFilePath) ?: return false + return secureCompare(calculated, extractedSha256) + } + + fun validateAllFilesInDir(context: Context, dirPath: String, metadata: Map, appVersion: String, bundleVersion: String): Boolean { + val dir = File(dirPath) + if (!dir.exists() || !dir.isDirectory) return false + + val parentBundleDir = getBundleDir(context) + val folderName = "$appVersion-$bundleVersion" + val jsBundleDir = File(parentBundleDir, folderName).absolutePath + "/" + + if (!validateFilesRecursive(dir, metadata, jsBundleDir)) return false + + // Verify completeness + for (entry in metadata.entries) { + val expectedFile = File(jsBundleDir + entry.key) + if (!expectedFile.exists()) { + OneKeyLog.error("BundleUpdate", "[bundle-verify] File listed in metadata but missing on disk: ${entry.key}") + return false + } + } + return true + } + + private fun validateFilesRecursive(dir: File, metadata: Map, jsBundleDir: String): Boolean { + val files = dir.listFiles() ?: return true + for (file in files) { + if (file.isDirectory) { + if (!validateFilesRecursive(file, metadata, jsBundleDir)) return false + } else { + if (file.name.contains("metadata.json") || file.name.contains(".DS_Store")) continue + val relativePath = file.absolutePath.replace(jsBundleDir, "") + val expectedSHA256 = metadata[relativePath] + if (expectedSHA256 == null) { + OneKeyLog.error("BundleUpdate", "[bundle-verify] File on disk not found in metadata: $relativePath") + return false + } + val actualSHA256 = calculateSHA256(file.absolutePath) + if (actualSHA256 == null) { + OneKeyLog.error("BundleUpdate", "[bundle-verify] Failed to calculate SHA256 for file: $relativePath") + return false + } + if (!secureCompare(expectedSHA256, actualSHA256)) { + OneKeyLog.error("BundleUpdate", "[bundle-verify] SHA256 mismatch for $relativePath") + return false + } + } + } + return true + } + + fun readFileContent(file: File): String { + return file.readText(Charsets.UTF_8) + } + + fun getFallbackUpdateBundleDataFile(context: Context): File { + val path = File(getBundleDir(context), "fallbackUpdateBundleData.json") + if (!path.exists()) { + try { path.createNewFile() } catch (e: Exception) { + OneKeyLog.error("BundleUpdate", "getFallbackUpdateBundleDataFile: ${e.message}") + } + } + return path + } + + fun readFallbackUpdateBundleDataFile(context: Context): MutableList> { + val file = getFallbackUpdateBundleDataFile(context) + val content = try { readFileContent(file) } catch (e: Exception) { "" } + if (content.isEmpty()) return mutableListOf() + return try { + val arr = JSONArray(content) + val result = mutableListOf>() + for (i in 0 until arr.length()) { + val obj = arr.getJSONObject(i) + val map = mutableMapOf() + val keys = obj.keys() + while (keys.hasNext()) { + val key = keys.next() + map[key] = obj.getString(key) + } + result.add(map) + } + result + } catch (e: Exception) { + OneKeyLog.error("BundleUpdate", "readFallbackUpdateBundleDataFile: ${e.message}") + mutableListOf() + } + } + + fun writeFallbackUpdateBundleDataFile(data: List>, context: Context) { + val file = getFallbackUpdateBundleDataFile(context) + val arr = JSONArray() + for (map in data) { + val obj = JSONObject() + for ((k, v) in map) obj.put(k, v) + arr.put(obj) + } + try { + // Atomic write: write to temp file then rename to avoid partial writes + val tmpFile = File(file.parent, "${file.name}.tmp") + FileOutputStream(tmpFile).use { fos -> + fos.write(arr.toString().toByteArray(Charsets.UTF_8)) + fos.fd.sync() + } + if (!tmpFile.renameTo(file)) { + tmpFile.delete() + throw Exception("Failed to rename temp file to ${file.name}") + } + } catch (e: Exception) { + OneKeyLog.error("BundleUpdate", "writeFallbackUpdateBundleDataFile: ${e.message}") + } + } + + fun getCurrentBundleMainJSBundle(context: Context): String? { + return try { + val currentAppVersion = getAppVersion(context) + val currentBundleVersion = getCurrentBundleVersion(context) ?: run { + OneKeyLog.warn("BundleUpdate", "getJsBundlePath: no currentBundleVersion stored") + return null + } + + OneKeyLog.info("BundleUpdate", "currentAppVersion: $currentAppVersion, currentBundleVersion: $currentBundleVersion") + + val prevNativeVersion = getNativeVersion(context) + if (prevNativeVersion.isEmpty()) { + OneKeyLog.warn("BundleUpdate", "getJsBundlePath: prevNativeVersion is empty") + return "" + } + + if (currentAppVersion != prevNativeVersion) { + OneKeyLog.info("BundleUpdate", "currentAppVersion is not equal to prevNativeVersion $currentAppVersion $prevNativeVersion") + clearUpdateBundleData(context) + return null + } + + val bundleDir = getCurrentBundleDir(context, currentBundleVersion) ?: run { + OneKeyLog.warn("BundleUpdate", "getJsBundlePath: getCurrentBundleDir returned null") + return null + } + if (!File(bundleDir).exists()) { + OneKeyLog.info("BundleUpdate", "currentBundleDir does not exist: $bundleDir") + return null + } + + val signature = readSignatureFile(context, currentBundleVersion) + OneKeyLog.debug("BundleUpdate", "getJsBundlePath: signatureLength=${signature.length}") + + val devSettingsEnabled = isDevSettingsEnabled(context) + if (devSettingsEnabled) { + OneKeyLog.warn("BundleUpdate", "Startup SHA256 validation skipped (DevSettings enabled)") + } + if (!devSettingsEnabled && !validateMetadataFileSha256(context, currentBundleVersion, signature)) { + OneKeyLog.warn("BundleUpdate", "getJsBundlePath: validateMetadataFileSha256 failed, signatureLength=${signature.length}") + return null + } + + val metadataContent = getMetadataFileContent(context, currentBundleVersion) ?: run { + OneKeyLog.warn("BundleUpdate", "getJsBundlePath: getMetadataFileContent returned null") + return null + } + val metadata = parseMetadataJson(metadataContent) + + val lastDashIndex = currentBundleVersion.lastIndexOf("-") + if (lastDashIndex > 0) { + val appVer = currentBundleVersion.substring(0, lastDashIndex) + val bundleVer = currentBundleVersion.substring(lastDashIndex + 1) + if (!validateAllFilesInDir(context, bundleDir, metadata, appVer, bundleVer)) { + OneKeyLog.info("BundleUpdate", "validateAllFilesInDir failed on startup") + return null + } + } + + val mainJSBundleFile = File(bundleDir, "main.jsbundle.hbc") + val mainJSBundlePath = mainJSBundleFile.absolutePath + OneKeyLog.info("BundleUpdate", "mainJSBundlePath: $mainJSBundlePath") + if (!mainJSBundleFile.exists()) { + OneKeyLog.info("BundleUpdate", "mainJSBundleFile does not exist") + return null + } + mainJSBundlePath + } catch (e: Exception) { + OneKeyLog.error("BundleUpdate", "Error getting bundle: ${e.message}") + null + } + } + + fun getWebEmbedPath(context: Context): String { + val currentBundleDir = getCurrentBundleDir(context, getCurrentBundleVersion(context)) ?: return "" + return File(currentBundleDir, "web-embed").absolutePath + } + + /** + * Returns true if the OneKey developer mode (DevSettings) is enabled. + * Reads the persisted value from MMKV storage (key: onekey_developer_mode_enabled, + * instance: onekey-app-dev-setting) written by the JS ServiceDevSetting layer. + */ + fun isDevSettingsEnabled(context: Context): Boolean { + return try { + MMKV.initialize(context) + val mmkv = MMKV.mmkvWithID("onekey-app-dev-setting") ?: return false + mmkv.decodeBool("onekey_developer_mode_enabled", false) + } catch (e: Exception) { + false + } + } + + /** + * Returns true if the skip-GPG-verification toggle is enabled in developer settings. + * Reads the persisted value from MMKV storage (key: onekey_bundle_skip_gpg_verification, + * instance: onekey-app-dev-setting). + */ + fun isSkipGPGEnabled(context: Context): Boolean { + return try { + MMKV.initialize(context) + val mmkv = MMKV.mmkvWithID("onekey-app-dev-setting") ?: return false + mmkv.decodeBool("onekey_bundle_skip_gpg_verification", false) + } catch (e: Exception) { + false + } + } + + private fun deleteDirectory(directory: File) { + if (directory.exists()) { + directory.listFiles()?.forEach { file -> + if (file.isDirectory) deleteDirectory(file) else file.delete() + } + directory.delete() + } + } + + fun deleteDir(dir: File) = deleteDirectory(dir) + + private const val MAX_UNZIPPED_SIZE = 512L * 1024 * 1024 // 512 MB limit + + fun unzipFile(zipFilePath: String, destDirectory: String) { + val destDir = File(destDirectory) + if (!destDir.exists()) destDir.mkdirs() + + val destDirPath: Path = Paths.get(destDir.canonicalPath) + var totalBytesWritten = 0L + ZipInputStream(FileInputStream(zipFilePath)).use { zipIn -> + var entry: ZipEntry? = zipIn.nextEntry + while (entry != null) { + val outFile = File(destDir, entry.name) + val outPath: Path = Paths.get(outFile.canonicalPath) + if (!outPath.startsWith(destDirPath)) { + throw java.io.IOException("Entry is outside of the target dir: ${entry.name}") + } + if (!entry.isDirectory) { + outFile.parentFile?.mkdirs() + FileOutputStream(outFile).use { fos -> + val buffer = ByteArray(8192) + var length: Int + while (zipIn.read(buffer).also { length = it } > 0) { + totalBytesWritten += length + if (totalBytesWritten > MAX_UNZIPPED_SIZE) { + throw java.io.IOException("Decompression bomb detected: extracted size exceeds ${MAX_UNZIPPED_SIZE / 1024 / 1024} MB") + } + fos.write(buffer, 0, length) + } + } + // Validate no symlink was created (zip slip via symlink attack) + if (Files.isSymbolicLink(outFile.toPath())) { + outFile.delete() + throw java.io.IOException("Symlink detected in zip entry: ${entry.name}") + } + } else { + outFile.mkdirs() + } + zipIn.closeEntry() + entry = zipIn.nextEntry + } + } + } +} + +@DoNotStrip +class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() { + + companion object { + private const val PREFS_NAME = "BundleUpdatePrefs" + } + + private val listeners = CopyOnWriteArrayList() + private val nextListenerId = AtomicLong(1) + private val isDownloading = AtomicBoolean(false) + private val httpClient = OkHttpClient.Builder() + .addNetworkInterceptor { chain -> + val req = chain.request() + if (!req.url.isHttps) { + throw java.io.IOException("Redirect to non-HTTPS URL is not allowed: ${req.url}") + } + chain.proceed(req) + } + .build() + + private fun sendEvent(type: String, progress: Int = 0, message: String = "") { + val event = BundleDownloadEvent(type = type, progress = progress.toDouble(), message = message) + for (listener in listeners) { + try { + listener.callback(event) + } catch (e: Exception) { + OneKeyLog.error("BundleUpdate", "Error sending event: ${e.message}") + } + } + } + + override fun addDownloadListener(callback: (BundleDownloadEvent) -> Unit): Double { + val id = nextListenerId.getAndIncrement().toDouble() + listeners.add(BundleListener(id, callback)) + OneKeyLog.debug("BundleUpdate", "addDownloadListener: id=$id, totalListeners=${listeners.size}") + return id + } + + override fun removeDownloadListener(id: Double) { + listeners.removeAll { it.id == id } + OneKeyLog.debug("BundleUpdate", "removeDownloadListener: id=$id, totalListeners=${listeners.size}") + } + + private fun getContext(): Context { + return NitroModules.applicationContext + ?: throw Exception("Application context unavailable") + } + + private fun isDebuggable(): Boolean { + val context = NitroModules.applicationContext ?: return false + return (context.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0 + } + + /** Returns true if OneKey developer mode (DevSettings) is enabled via MMKV storage. */ + private fun isDevSettingsEnabled(): Boolean { + return try { + val context = NitroModules.applicationContext ?: return false + BundleUpdateStoreAndroid.isDevSettingsEnabled(context) + } catch (e: Exception) { + false + } + } + + /** Returns true if the skip-GPG-verification toggle is enabled via MMKV storage. */ + private fun isSkipGPGEnabled(): Boolean { + return try { + val context = NitroModules.applicationContext ?: return false + BundleUpdateStoreAndroid.isSkipGPGEnabled(context) + } catch (e: Exception) { + false + } + } + + override fun downloadBundle(params: BundleDownloadParams): Promise { + return Promise.async { + if (isDownloading.getAndSet(true)) { + OneKeyLog.warn("BundleUpdate", "downloadBundle: rejected, already downloading") + throw Exception("Already downloading") + } + + try { + val context = getContext() + val appVersion = params.latestVersion + val bundleVersion = params.bundleVersion + val downloadUrl = params.downloadUrl + val sha256 = params.sha256 + + OneKeyLog.info("BundleUpdate", "downloadBundle: appVersion=$appVersion, bundleVersion=$bundleVersion, fileSize=${params.fileSize}, url=$downloadUrl") + + if (!BundleUpdateStoreAndroid.isSafeVersionString(appVersion) || + !BundleUpdateStoreAndroid.isSafeVersionString(bundleVersion)) { + OneKeyLog.error("BundleUpdate", "downloadBundle: invalid version string format: appVersion=$appVersion, bundleVersion=$bundleVersion") + throw Exception("Invalid version string format") + } + + if (!downloadUrl.startsWith("https://")) { + OneKeyLog.error("BundleUpdate", "downloadBundle: URL is not HTTPS: $downloadUrl") + throw Exception("Bundle download URL must use HTTPS") + } + + val fileName = "$appVersion-$bundleVersion.zip" + val filePath = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context), fileName).absolutePath + + val result = BundleDownloadResult( + downloadedFile = filePath, + downloadUrl = downloadUrl, + latestVersion = appVersion, + bundleVersion = bundleVersion, + sha256 = sha256 + ) + + OneKeyLog.info("BundleUpdate", "downloadBundle: filePath=$filePath") + + val downloadedFile = File(filePath) + if (downloadedFile.exists()) { + OneKeyLog.info("BundleUpdate", "downloadBundle: file already exists, verifying SHA256...") + if (verifyBundleSHA256(filePath, sha256)) { + OneKeyLog.info("BundleUpdate", "downloadBundle: existing file SHA256 valid, skipping download") + isDownloading.set(false) + Thread.sleep(1000) + sendEvent("update/complete") + return@async result + } else { + OneKeyLog.warn("BundleUpdate", "downloadBundle: existing file SHA256 mismatch, re-downloading") + downloadedFile.delete() + } + } + + sendEvent("update/start") + OneKeyLog.info("BundleUpdate", "downloadBundle: starting download...") + + val request = Request.Builder().url(downloadUrl).build() + val response = httpClient.newCall(request).execute() + + if (!response.isSuccessful) { + OneKeyLog.error("BundleUpdate", "downloadBundle: HTTP error, statusCode=${response.code}") + sendEvent("update/error", message = "HTTP ${response.code}") + throw Exception("HTTP ${response.code}") + } + + val body = response.body ?: throw Exception("Empty response body") + val fileSize = if (params.fileSize > 0) params.fileSize.toLong() else body.contentLength() + OneKeyLog.info("BundleUpdate", "downloadBundle: HTTP 200, contentLength=$fileSize, downloading...") + + // Ensure parent directory exists before writing + val parentDir = File(filePath).parentFile + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs() + OneKeyLog.info("BundleUpdate", "downloadBundle: created parent directory: ${parentDir.absolutePath}") + } + + var totalBytesRead = 0L + body.byteStream().use { inputStream -> + FileOutputStream(filePath).use { outputStream -> + val buffer = ByteArray(8192) + var bytesRead: Int + + var prevProgress = 0 + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + outputStream.write(buffer, 0, bytesRead) + totalBytesRead += bytesRead + if (fileSize > 0) { + val progress = ((totalBytesRead * 100) / fileSize).toInt() + if (progress != prevProgress) { + sendEvent("update/downloading", progress = progress) + OneKeyLog.info("BundleUpdate", "download progress: $progress% ($totalBytesRead/$fileSize)") + prevProgress = progress + } + } + } + } + } + + val downloadedFileAfter = File(filePath) + OneKeyLog.info("BundleUpdate", "downloadBundle: download finished, totalBytesRead=$totalBytesRead, fileExists=${downloadedFileAfter.exists()}, fileSize=${if (downloadedFileAfter.exists()) downloadedFileAfter.length() else -1}, verifying SHA256...") + if (!verifyBundleSHA256(filePath, sha256)) { + File(filePath).delete() + OneKeyLog.error("BundleUpdate", "downloadBundle: SHA256 verification failed after download") + sendEvent("update/error", message = "Bundle signature verification failed") + throw Exception("Bundle signature verification failed") + } + + sendEvent("update/complete") + OneKeyLog.info("BundleUpdate", "downloadBundle: completed successfully, appVersion=$appVersion, bundleVersion=$bundleVersion") + result + } catch (e: Exception) { + OneKeyLog.error("BundleUpdate", "downloadBundle: failed: ${e.javaClass.simpleName}: ${e.message}") + sendEvent("update/error", message = "${e.javaClass.simpleName}: ${e.message}") + throw e + } finally { + isDownloading.set(false) + } + } + } + + private fun verifyBundleSHA256(bundlePath: String, sha256: String): Boolean { + val calculated = BundleUpdateStoreAndroid.calculateSHA256(bundlePath) + if (calculated == null) { + OneKeyLog.error("BundleUpdate", "verifyBundleSHA256: failed to calculate SHA256 for: $bundlePath") + return false + } + val isValid = BundleUpdateStoreAndroid.secureCompare(calculated, sha256) + OneKeyLog.debug("BundleUpdate", "verifyBundleSHA256: path=$bundlePath, expected=${sha256.take(16)}..., calculated=${calculated.take(16)}..., valid=$isValid") + return isValid + } + + override fun downloadBundleASC(params: BundleDownloadASCParams): Promise { + return Promise.async { + val context = getContext() + val appVersion = params.latestVersion + val bundleVersion = params.bundleVersion + val signature = params.signature + + OneKeyLog.info("BundleUpdate", "downloadBundleASC: appVersion=$appVersion, bundleVersion=$bundleVersion, signatureLength=${signature.length}") + + val storageKey = "$appVersion-$bundleVersion" + BundleUpdateStoreAndroid.writeSignatureFile(context, storageKey, signature) + + OneKeyLog.info("BundleUpdate", "downloadBundleASC: stored signature for key=$storageKey") + } + } + + override fun verifyBundleASC(params: BundleVerifyASCParams): Promise { + return Promise.async { + val context = getContext() + val filePath = params.downloadedFile + val sha256 = params.sha256 + val appVersion = params.latestVersion + val bundleVersion = params.bundleVersion + val signature = params.signature + + OneKeyLog.info("BundleUpdate", "verifyBundleASC: appVersion=$appVersion, bundleVersion=$bundleVersion, file=$filePath, signatureLength=${signature.length}") + + // GPG verification skipped only when both DevSettings and skip-GPG toggle are enabled + val devSettings = isDevSettingsEnabled() + val skipGPGToggle = isSkipGPGEnabled() + val skipGPG = devSettings && skipGPGToggle + OneKeyLog.info("BundleUpdate", "verifyBundleASC: GPG check: devSettings=$devSettings, skipGPGToggle=$skipGPGToggle, skipGPG=$skipGPG") + + if (!skipGPG) { + OneKeyLog.info("BundleUpdate", "verifyBundleASC: verifying SHA256 of downloaded file...") + if (!verifyBundleSHA256(filePath, sha256)) { + OneKeyLog.error("BundleUpdate", "verifyBundleASC: SHA256 verification failed for file=$filePath") + throw Exception("Bundle signature verification failed") + } + OneKeyLog.info("BundleUpdate", "verifyBundleASC: SHA256 verified OK") + } else { + OneKeyLog.warn("BundleUpdate", "verifyBundleASC: SHA256 + GPG verification skipped (DevSettings enabled)") + } + + val folderName = "$appVersion-$bundleVersion" + val destination = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName).absolutePath + val destinationDir = File(destination) + + try { + OneKeyLog.info("BundleUpdate", "verifyBundleASC: extracting zip to $destination...") + BundleUpdateStoreAndroid.unzipFile(filePath, destination) + OneKeyLog.info("BundleUpdate", "verifyBundleASC: extraction completed") + + val metadataFile = File(destination, "metadata.json") + if (!metadataFile.exists()) { + OneKeyLog.error("BundleUpdate", "verifyBundleASC: metadata.json not found after extraction") + BundleUpdateStoreAndroid.deleteDir(destinationDir) + throw Exception("Failed to read metadata.json") + } + + val currentBundleVersion = "$appVersion-$bundleVersion" + if (!skipGPG) { + OneKeyLog.info("BundleUpdate", "verifyBundleASC: validating GPG signature for metadata...") + if (!BundleUpdateStoreAndroid.validateMetadataFileSha256(context, currentBundleVersion, signature)) { + OneKeyLog.error("BundleUpdate", "verifyBundleASC: GPG signature verification failed") + BundleUpdateStoreAndroid.deleteDir(destinationDir) + throw Exception("Bundle signature verification failed") + } + OneKeyLog.info("BundleUpdate", "verifyBundleASC: GPG signature verified OK") + } + + OneKeyLog.info("BundleUpdate", "verifyBundleASC: validating all extracted files against metadata...") + val metadataContent = BundleUpdateStoreAndroid.readFileContent(metadataFile) + val metadata = BundleUpdateStoreAndroid.parseMetadataJson(metadataContent) + if (!BundleUpdateStoreAndroid.validateAllFilesInDir(context, destination, metadata, appVersion, bundleVersion)) { + OneKeyLog.error("BundleUpdate", "verifyBundleASC: file integrity check failed") + BundleUpdateStoreAndroid.deleteDir(destinationDir) + throw Exception("Extracted files verification against metadata failed") + } + + OneKeyLog.info("BundleUpdate", "verifyBundleASC: all verifications passed, appVersion=$appVersion, bundleVersion=$bundleVersion") + } catch (e: Exception) { + if (destinationDir.exists()) { + BundleUpdateStoreAndroid.deleteDir(destinationDir) + } + throw e + } + } + } + + override fun verifyBundle(params: BundleVerifyParams): Promise { + return Promise.async { + val filePath = params.downloadedFile + val sha256 = params.sha256 + val appVersion = params.latestVersion + val bundleVersion = params.bundleVersion + + OneKeyLog.info("BundleUpdate", "verifyBundle: appVersion=$appVersion, bundleVersion=$bundleVersion, file=$filePath") + + // Verify SHA256 of the downloaded file + val calculated = BundleUpdateStoreAndroid.calculateSHA256(filePath) + if (calculated == null) { + OneKeyLog.error("BundleUpdate", "verifyBundle: failed to calculate SHA256 for file=$filePath") + throw Exception("Failed to calculate SHA256") + } + if (!BundleUpdateStoreAndroid.secureCompare(calculated, sha256)) { + OneKeyLog.error("BundleUpdate", "verifyBundle: SHA256 mismatch, expected=${sha256.take(16)}..., got=${calculated.take(16)}...") + throw Exception("SHA256 verification failed") + } + + OneKeyLog.info("BundleUpdate", "verifyBundle: SHA256 verified OK for appVersion=$appVersion, bundleVersion=$bundleVersion") + } + } + + override fun installBundle(params: BundleInstallParams): Promise { + return Promise.async { + val context = getContext() + val appVersion = params.latestVersion + val bundleVersion = params.bundleVersion + val signature = params.signature + + OneKeyLog.info("BundleUpdate", "installBundle: appVersion=$appVersion, bundleVersion=$bundleVersion, signatureLength=${signature.length}") + + // GPG verification skipped only when both DevSettings and skip-GPG toggle are enabled + val devSettings = isDevSettingsEnabled() + val skipGPGToggle = isSkipGPGEnabled() + val skipGPG = devSettings && skipGPGToggle + OneKeyLog.info("BundleUpdate", "installBundle: GPG check: devSettings=$devSettings, skipGPGToggle=$skipGPGToggle, skipGPG=$skipGPG") + + val folderName = "$appVersion-$bundleVersion" + val currentFolderName = BundleUpdateStoreAndroid.getCurrentBundleVersion(context) + OneKeyLog.info("BundleUpdate", "installBundle: target=$folderName, current=$currentFolderName") + + // Verify bundle directory exists + val bundleDirPath = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName) + if (!bundleDirPath.exists()) { + OneKeyLog.error("BundleUpdate", "installBundle: bundle directory not found: ${bundleDirPath.absolutePath}") + throw Exception("Bundle directory not found: $folderName") + } + + val currentSignature = if (currentFolderName != null) BundleUpdateStoreAndroid.readSignatureFile(context, currentFolderName) else "" + + BundleUpdateStoreAndroid.setCurrentBundleVersionAndSignature(context, folderName, signature) + val nativeVersion = BundleUpdateStoreAndroid.getAppVersion(context) ?: "" + BundleUpdateStoreAndroid.setNativeVersion(context, nativeVersion) + + // Manage fallback data + try { + val fallbackData = BundleUpdateStoreAndroid.readFallbackUpdateBundleDataFile(context) + + if (!currentFolderName.isNullOrEmpty()) { + val lastDashIndex = currentFolderName.lastIndexOf("-") + if (lastDashIndex > 0) { + val curAppVersion = currentFolderName.substring(0, lastDashIndex) + val curBundleVersion = currentFolderName.substring(lastDashIndex + 1) + if (currentSignature.isNotEmpty()) { + fallbackData.add(mutableMapOf( + "appVersion" to curAppVersion, + "bundleVersion" to curBundleVersion, + "signature" to currentSignature + )) + } + } + } + + if (fallbackData.size > 3) { + val shifted = fallbackData.removeAt(0) + val shiftApp = shifted["appVersion"] + val shiftBundle = shifted["bundleVersion"] + if (shiftApp != null && shiftBundle != null) { + val shiftFolderName = "$shiftApp-$shiftBundle" + val oldDir = File(BundleUpdateStoreAndroid.getBundleDir(context), shiftFolderName) + if (oldDir.exists()) { + BundleUpdateStoreAndroid.deleteDir(oldDir) + } + BundleUpdateStoreAndroid.deleteSignatureFile(context, shiftFolderName) + } + } + + BundleUpdateStoreAndroid.writeFallbackUpdateBundleDataFile(fallbackData, context) + OneKeyLog.info("BundleUpdate", "installBundle: completed successfully, installed version=$folderName, fallbackCount=${fallbackData.size}") + } catch (e: Exception) { + OneKeyLog.error("BundleUpdate", "installBundle: fallbackUpdateBundleData error: ${e.message}") + } + } + } + + override fun clearBundle(): Promise { + return Promise.async { + OneKeyLog.info("BundleUpdate", "clearBundle: clearing download directory...") + val context = getContext() + val downloadDir = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context)) + if (downloadDir.exists()) { + BundleUpdateStoreAndroid.deleteDir(downloadDir) + OneKeyLog.info("BundleUpdate", "clearBundle: download directory deleted") + } else { + OneKeyLog.info("BundleUpdate", "clearBundle: download directory does not exist, skipping") + } + isDownloading.set(false) + OneKeyLog.info("BundleUpdate", "clearBundle: completed") + } + } + + override fun clearAllJSBundleData(): Promise { + return Promise.async { + OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: starting...") + val context = getContext() + val downloadDir = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context)) + if (downloadDir.exists()) { + BundleUpdateStoreAndroid.deleteDir(downloadDir) + OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: deleted download dir") + } + val bundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context)) + if (bundleDir.exists()) { + BundleUpdateStoreAndroid.deleteDir(bundleDir) + OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: deleted bundle dir") + } + BundleUpdateStoreAndroid.clearUpdateBundleData(context) + + OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: completed successfully") + TestResult(success = true, message = "Successfully cleared all JS bundle data") + } + } + + override fun getFallbackUpdateBundleData(): Promise> { + return Promise.async { + val context = getContext() + val data = BundleUpdateStoreAndroid.readFallbackUpdateBundleDataFile(context) + val result = data.mapNotNull { map -> + val appVersion = map["appVersion"] ?: return@mapNotNull null + val bundleVersion = map["bundleVersion"] ?: return@mapNotNull null + val signature = map["signature"] ?: return@mapNotNull null + FallbackBundleInfo(appVersion = appVersion, bundleVersion = bundleVersion, signature = signature) + }.toTypedArray() + OneKeyLog.info("BundleUpdate", "getFallbackUpdateBundleData: found ${result.size} fallback entries") + result + } + } + + override fun setCurrentUpdateBundleData(params: BundleSwitchParams): Promise { + return Promise.async { + val context = getContext() + val bundleVersion = "${params.appVersion}-${params.bundleVersion}" + OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: switching to $bundleVersion") + + // Verify the bundle directory actually exists + val bundleDirPath = File(BundleUpdateStoreAndroid.getBundleDir(context), bundleVersion) + if (!bundleDirPath.exists()) { + OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: bundle directory not found: ${bundleDirPath.absolutePath}") + throw Exception("Bundle directory not found") + } + + // Verify GPG signature is valid (skipped when both DevSettings and skip-GPG toggle are enabled) + val devSettings = isDevSettingsEnabled() + val skipGPGToggle = isSkipGPGEnabled() + OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: GPG check: devSettings=$devSettings, skipGPGToggle=$skipGPGToggle") + if (!(devSettings && skipGPGToggle)) { + if (params.signature.isEmpty() || + !BundleUpdateStoreAndroid.validateMetadataFileSha256(context, bundleVersion, params.signature)) { + OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verification failed") + throw Exception("Bundle signature verification failed") + } + OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verified OK") + } else { + OneKeyLog.warn("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verification skipped (DevSettings + skip-GPG enabled)") + } + + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + prefs.edit() + .putString("currentBundleVersion", bundleVersion) + .apply() + if (params.signature.isNotEmpty()) { + BundleUpdateStoreAndroid.writeSignatureFile(context, bundleVersion, params.signature) + } + OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: switched to $bundleVersion") + } + } + + override fun getWebEmbedPath(): String { + val context = NitroModules.applicationContext ?: return "" + val path = BundleUpdateStoreAndroid.getWebEmbedPath(context) + OneKeyLog.debug("BundleUpdate", "getWebEmbedPath: $path") + return path + } + + override fun getWebEmbedPathAsync(): Promise { + return Promise.async { + val context = getContext() + val path = BundleUpdateStoreAndroid.getWebEmbedPath(context) + OneKeyLog.debug("BundleUpdate", "getWebEmbedPathAsync: $path") + path + } + } + + override fun getJsBundlePath(): Promise { + return Promise.async { + val context = getContext() + val path = BundleUpdateStoreAndroid.getCurrentBundleMainJSBundle(context) ?: "" + OneKeyLog.info("BundleUpdate", "getJsBundlePath: ${if (path.isEmpty()) "(empty/no bundle)" else path}") + path + } + } + + override fun getNativeAppVersion(): Promise { + return Promise.async { + val context = getContext() + val version = BundleUpdateStoreAndroid.getAppVersion(context) ?: "" + OneKeyLog.info("BundleUpdate", "getNativeAppVersion: $version") + version + } + } + + override fun testVerification(): Promise { + return Promise.async { + val testSignature = """-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA256 + +{ + "fileName": "metadata.json", + "sha256": "2ada9c871104fc40649fa3de67a7d8e33faadc18e9abd587e8bb85be0a003eba", + "size": 158590, + "generatedAt": "2025-09-19T07:49:13.000Z" +} +-----BEGIN PGP SIGNATURE----- + +iQJCBAEBCAAsFiEE62iuVE8f3YzSZGJPs2mmepC/OHsFAmjNJ1IOHGRldkBvbmVr +ZXkuc28ACgkQs2mmepC/OHs6Rw/9FKHl5aNsE7V0IsFf/l+h16BYKFwVsL69alMk +CFLna8oUn0+tyECF6wKBKw5pHo5YR27o2pJfYbAER6dygDF6WTZ1lZdf5QcBMjGA +LCeXC0hzUBzSSOH4bKBTa3fHp//HdSV1F2OnkymbXqYN7WXvuQPLZ0nV6aU88hCk +HgFifcvkXAnWKoosUtj0Bban/YBRyvmQ5C2akxUPEkr4Yck1QXwzJeNRd7wMXHjH +JFK6lJcuABiB8wpJDXJkFzKs29pvHIK2B2vdOjU2rQzKOUwaKHofDi5C4+JitT2b +2pSeYP3PAxXYw6XDOmKTOiC7fPnfLjtcPjNYNFCezVKZT6LKvZW9obnW8Q9LNJ4W +okMPgHObkabv3OqUaTA9QNVfI/X9nvggzlPnaKDUrDWTf7n3vlrdexugkLtV/tJA +uguPlI5hY7Ue5OW7ckWP46hfmq1+UaIdeUY7dEO+rPZDz6KcArpaRwBiLPBhneIr +/X3KuMzS272YbPbavgCZGN9xJR5kZsEQE5HhPCbr6Nf0qDnh+X8mg0tAB/U6F+ZE +o90sJL1ssIaYvST+VWVaGRr4V5nMDcgHzWSF9Q/wm22zxe4alDaBdvOlUseW0iaM +n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ +91Js0rQ= +=A/Ii +-----END PGP SIGNATURE-----""" + val result = BundleUpdateStoreAndroid.verifyGPGAndExtractSha256(testSignature) + val isValid = result == "2ada9c871104fc40649fa3de67a7d8e33faadc18e9abd587e8bb85be0a003eba" + OneKeyLog.info("BundleUpdate", "testVerification: GPG verification result: $isValid") + isValid + } + } + + override fun isBundleExists(appVersion: String, bundleVersion: String): Promise { + return Promise.async { + val context = getContext() + val folderName = "$appVersion-$bundleVersion" + val bundlePath = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName) + val exists = bundlePath.exists() + OneKeyLog.info("BundleUpdate", "isBundleExists: appVersion=$appVersion, bundleVersion=$bundleVersion, exists=$exists") + exists + } + } + + override fun verifyExtractedBundle(appVersion: String, bundleVersion: String): Promise { + return Promise.async { + OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: appVersion=$appVersion, bundleVersion=$bundleVersion") + val context = getContext() + val folderName = "$appVersion-$bundleVersion" + val bundlePath = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName) + if (!bundlePath.exists()) { + OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: bundle directory not found: ${bundlePath.absolutePath}") + throw Exception("Bundle directory not found") + } + val metadataFile = File(bundlePath, "metadata.json") + if (!metadataFile.exists()) { + OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: metadata.json not found in ${bundlePath.absolutePath}") + throw Exception("metadata.json not found") + } + + OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: parsing metadata and validating files...") + val metadataContent = BundleUpdateStoreAndroid.readFileContent(metadataFile) + val metadata = BundleUpdateStoreAndroid.parseMetadataJson(metadataContent) + if (!BundleUpdateStoreAndroid.validateAllFilesInDir(context, bundlePath.absolutePath, metadata, appVersion, bundleVersion)) { + OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: file integrity check failed") + throw Exception("File integrity check failed") + } + OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: all files verified OK, fileCount=${metadata.size}") + } + } + + override fun listLocalBundles(): Promise> { + return Promise.async { + val context = getContext() + val bundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context)) + val results = mutableListOf() + if (bundleDir.exists() && bundleDir.isDirectory) { + bundleDir.listFiles()?.forEach { child -> + if (!child.isDirectory) return@forEach + val name = child.name + val lastDash = name.lastIndexOf('-') + if (lastDash <= 0) return@forEach + val appVer = name.substring(0, lastDash) + val bundleVer = name.substring(lastDash + 1) + if (appVer.isNotEmpty() && bundleVer.isNotEmpty()) { + results.add(LocalBundleInfo(appVersion = appVer, bundleVersion = bundleVer)) + } + } + } + OneKeyLog.info("BundleUpdate", "listLocalBundles: found ${results.size} bundles") + results.toTypedArray() + } + } + + override fun listAscFiles(): Promise> { + return Promise.async { + val context = getContext() + val ascDir = File(BundleUpdateStoreAndroid.getAscDir(context)) + val results = mutableListOf() + if (ascDir.exists() && ascDir.isDirectory) { + ascDir.listFiles()?.forEach { file -> + if (file.isFile) { + results.add(AscFileInfo(fileName = file.name, filePath = file.absolutePath, fileSize = file.length().toDouble())) + } + } + } + OneKeyLog.info("BundleUpdate", "listAscFiles: found ${results.size} files") + results.toTypedArray() + } + } + + override fun getSha256FromFilePath(filePath: String): Promise { + return Promise.async { + OneKeyLog.info("BundleUpdate", "getSha256FromFilePath: filePath=$filePath") + if (filePath.isEmpty()) { + OneKeyLog.warn("BundleUpdate", "getSha256FromFilePath: empty filePath") + return@async "" + } + + // Restrict to bundle-related directories only + val context = getContext() + val resolvedPath = File(filePath).canonicalPath + val bundleDir = File(BundleUpdateStoreAndroid.getBundleDir(context)).canonicalPath + val downloadDir = File(BundleUpdateStoreAndroid.getDownloadBundleDir(context)).canonicalPath + if (!resolvedPath.startsWith(bundleDir) && !resolvedPath.startsWith(downloadDir)) { + OneKeyLog.error("BundleUpdate", "getSha256FromFilePath: path outside allowed directories: $resolvedPath") + throw Exception("File path outside allowed bundle directories") + } + + val sha256 = BundleUpdateStoreAndroid.calculateSHA256(filePath) ?: "" + OneKeyLog.info("BundleUpdate", "getSha256FromFilePath: sha256=${if (sha256.isEmpty()) "(empty)" else sha256.take(16) + "..."}") + sha256 + } + } + + override fun testDeleteJsBundle(appVersion: String, bundleVersion: String): Promise { + return Promise.async { + OneKeyLog.info("BundleUpdate", "testDeleteJsBundle: appVersion=$appVersion, bundleVersion=$bundleVersion") + val context = getContext() + val folderName = "$appVersion-$bundleVersion" + val jsBundlePath = File(File(BundleUpdateStoreAndroid.getBundleDir(context), folderName), "main.jsbundle.hbc") + + if (jsBundlePath.exists()) { + val success = jsBundlePath.delete() + if (success) { + OneKeyLog.info("BundleUpdate", "testDeleteJsBundle: deleted ${jsBundlePath.absolutePath}") + TestResult(success = true, message = "Deleted jsBundle: ${jsBundlePath.absolutePath}") + } else { + OneKeyLog.error("BundleUpdate", "testDeleteJsBundle: failed to delete ${jsBundlePath.absolutePath}") + throw Exception("Failed to delete jsBundle: ${jsBundlePath.absolutePath}") + } + } else { + OneKeyLog.warn("BundleUpdate", "testDeleteJsBundle: file not found: ${jsBundlePath.absolutePath}") + TestResult(success = false, message = "jsBundle not found: ${jsBundlePath.absolutePath}") + } + } + } + + override fun testDeleteJsRuntimeDir(appVersion: String, bundleVersion: String): Promise { + return Promise.async { + OneKeyLog.info("BundleUpdate", "testDeleteJsRuntimeDir: appVersion=$appVersion, bundleVersion=$bundleVersion") + val context = getContext() + val folderName = "$appVersion-$bundleVersion" + val dir = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName) + + if (dir.exists()) { + BundleUpdateStoreAndroid.deleteDir(dir) + if (!dir.exists()) { + OneKeyLog.info("BundleUpdate", "testDeleteJsRuntimeDir: deleted ${dir.absolutePath}") + TestResult(success = true, message = "Deleted js runtime directory: ${dir.absolutePath}") + } else { + OneKeyLog.error("BundleUpdate", "testDeleteJsRuntimeDir: failed to delete ${dir.absolutePath}") + throw Exception("Failed to delete js runtime directory: ${dir.absolutePath}") + } + } else { + OneKeyLog.warn("BundleUpdate", "testDeleteJsRuntimeDir: directory not found: ${dir.absolutePath}") + TestResult(success = false, message = "js runtime directory not found: ${dir.absolutePath}") + } + } + } + + override fun testDeleteMetadataJson(appVersion: String, bundleVersion: String): Promise { + return Promise.async { + OneKeyLog.info("BundleUpdate", "testDeleteMetadataJson: appVersion=$appVersion, bundleVersion=$bundleVersion") + val context = getContext() + val folderName = "$appVersion-$bundleVersion" + val metadataFile = File(File(BundleUpdateStoreAndroid.getBundleDir(context), folderName), "metadata.json") + + if (metadataFile.exists()) { + val success = metadataFile.delete() + if (success) { + OneKeyLog.info("BundleUpdate", "testDeleteMetadataJson: deleted ${metadataFile.absolutePath}") + TestResult(success = true, message = "Deleted metadata.json: ${metadataFile.absolutePath}") + } else { + OneKeyLog.error("BundleUpdate", "testDeleteMetadataJson: failed to delete ${metadataFile.absolutePath}") + throw Exception("Failed to delete metadata.json: ${metadataFile.absolutePath}") + } + } else { + OneKeyLog.warn("BundleUpdate", "testDeleteMetadataJson: file not found: ${metadataFile.absolutePath}") + TestResult(success = false, message = "metadata.json not found: ${metadataFile.absolutePath}") + } + } + } + + override fun testWriteEmptyMetadataJson(appVersion: String, bundleVersion: String): Promise { + return Promise.async { + OneKeyLog.info("BundleUpdate", "testWriteEmptyMetadataJson: appVersion=$appVersion, bundleVersion=$bundleVersion") + val context = getContext() + val folderName = "$appVersion-$bundleVersion" + val jsRuntimeDir = File(BundleUpdateStoreAndroid.getBundleDir(context), folderName) + val metadataPath = File(jsRuntimeDir, "metadata.json") + + if (!jsRuntimeDir.exists()) { + if (!jsRuntimeDir.mkdirs()) { + OneKeyLog.error("BundleUpdate", "testWriteEmptyMetadataJson: failed to create dir: ${jsRuntimeDir.absolutePath}") + throw Exception("Failed to create directory: ${jsRuntimeDir.absolutePath}") + } + } + + val emptyJson = JSONObject() + FileOutputStream(metadataPath).use { fos -> + fos.write(emptyJson.toString(2).toByteArray(Charsets.UTF_8)) + fos.flush() + } + + OneKeyLog.info("BundleUpdate", "testWriteEmptyMetadataJson: created ${metadataPath.absolutePath}") + TestResult(success = true, message = "Created empty metadata.json: ${metadataPath.absolutePath}") + } + } +} diff --git a/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdatePackage.kt b/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdatePackage.kt new file mode 100644 index 00000000..217f378b --- /dev/null +++ b/native-modules/react-native-bundle-update/android/src/main/java/com/margelo/nitro/reactnativebundleupdate/ReactNativeBundleUpdatePackage.kt @@ -0,0 +1,24 @@ +package com.margelo.nitro.reactnativebundleupdate + +import com.facebook.react.BaseReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfoProvider + +class ReactNativeBundleUpdatePackage : BaseReactPackage() { + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return null + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { HashMap() } + } + + companion object { + init { + System.loadLibrary("reactnativebundleupdate") + } + } +} + + diff --git a/native-modules/react-native-bundle-update/babel.config.js b/native-modules/react-native-bundle-update/babel.config.js new file mode 100644 index 00000000..af87eb60 --- /dev/null +++ b/native-modules/react-native-bundle-update/babel.config.js @@ -0,0 +1,13 @@ +module.exports = { + presets: ['@react-native/babel-preset'], + plugins: [ + [ + 'module-resolver', + { + alias: { + 'react-native-bundle-update': './src/index', + }, + }, + ], + ], +}; diff --git a/native-modules/react-native-bundle-update/eslint.config.mjs b/native-modules/react-native-bundle-update/eslint.config.mjs new file mode 100644 index 00000000..3416cf5c --- /dev/null +++ b/native-modules/react-native-bundle-update/eslint.config.mjs @@ -0,0 +1,5 @@ +import { createEslintConfig } from '@react-native/eslint-config'; + +export default createEslintConfig({ + extends: ['@react-native/eslint-config'], +}); diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/Info.plist b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/Info.plist new file mode 100644 index 00000000..389e49cf --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/Info.plist @@ -0,0 +1,52 @@ + + + + + AvailableLibraries + + + BinaryPath + Gopenpgp.framework/Gopenpgp + LibraryIdentifier + ios-arm64_x86_64-simulator + LibraryPath + Gopenpgp.framework + SupportedArchitectures + + arm64 + x86_64 + + SupportedPlatform + ios + SupportedPlatformVariant + simulator + MinimumOSVersion + 15.5 + + + BinaryPath + Gopenpgp.framework/Gopenpgp + LibraryIdentifier + ios-arm64 + LibraryPath + Gopenpgp.framework + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + MinimumOSVersion + 15.5 + + + CFBundlePackageType + XFWK + RCTNewArchEnabled + + XCFrameworkFormatVersion + 1.0 + MinimumOSVersion + 15.5 + + diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Gopenpgp b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Gopenpgp new file mode 100644 index 00000000..9de3a1a9 Binary files /dev/null and b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Gopenpgp differ diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Armor.objc.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Armor.objc.h new file mode 100644 index 00000000..3001f182 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Armor.objc.h @@ -0,0 +1,96 @@ +// Objective-C API for talking to github.com/ProtonMail/gopenpgp/v3/armor Go package. +// gobind -lang=objc github.com/ProtonMail/gopenpgp/v3/armor +// +// File is generated by gobind. Do not edit. + +#ifndef __Armor_H__ +#define __Armor_H__ + +@import Foundation; +#include "ref.h" +#include "Universe.objc.h" + +#include "Constants.objc.h" + +/** + * ArmorKey armors input as a public key. + */ +FOUNDATION_EXPORT NSString* _Nonnull ArmorArmorKey(NSData* _Nullable input, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT NSString* _Nonnull ArmorArmorPGPMessage(NSData* _Nullable signature, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT NSData* _Nullable ArmorArmorPGPMessageBytes(NSData* _Nullable signature, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT NSData* _Nullable ArmorArmorPGPMessageBytesChecksum(NSData* _Nullable signature, BOOL checksum, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT NSString* _Nonnull ArmorArmorPGPMessageChecksum(NSData* _Nullable signature, BOOL checksum, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT NSString* _Nonnull ArmorArmorPGPSignature(NSData* _Nullable signature, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT NSData* _Nullable ArmorArmorPGPSignatureBinary(NSData* _Nullable signature, NSError* _Nullable* _Nullable error); + +// skipped function ArmorReader with unsupported parameter or return types + + +/** + * ArmorWithType armors input with the given armorType. + */ +FOUNDATION_EXPORT NSString* _Nonnull ArmorArmorWithType(NSData* _Nullable input, NSString* _Nullable armorType, NSError* _Nullable* _Nullable error); + +/** + * ArmorWithTypeAndCustomHeaders armors input with the given armorType and +headers. + */ +FOUNDATION_EXPORT NSString* _Nonnull ArmorArmorWithTypeAndCustomHeaders(NSData* _Nullable input, NSString* _Nullable armorType, NSString* _Nullable version, NSString* _Nullable comment, NSError* _Nullable* _Nullable error); + +/** + * ArmorWithTypeAndCustomHeadersBytes armors input with the given armorType and +headers. + */ +FOUNDATION_EXPORT NSData* _Nullable ArmorArmorWithTypeAndCustomHeadersBytes(NSData* _Nullable input, NSString* _Nullable armorType, NSString* _Nullable version, NSString* _Nullable comment, NSError* _Nullable* _Nullable error); + +/** + * ArmorWithTypeAndCustomHeadersChecksum armors input with the given armorType and +headers and checksum option. + */ +FOUNDATION_EXPORT NSString* _Nonnull ArmorArmorWithTypeAndCustomHeadersChecksum(NSData* _Nullable input, NSString* _Nullable armorType, NSString* _Nullable version, NSString* _Nullable comment, BOOL checksum, NSError* _Nullable* _Nullable error); + +/** + * ArmorWithTypeBytes armors input with the given armorType. + */ +FOUNDATION_EXPORT NSData* _Nullable ArmorArmorWithTypeBytes(NSData* _Nullable input, NSString* _Nullable armorType, NSError* _Nullable* _Nullable error); + +/** + * ArmorWithTypeBytesChecksum armors input with the given armorType and checksum option. + */ +FOUNDATION_EXPORT NSData* _Nullable ArmorArmorWithTypeBytesChecksum(NSData* _Nullable input, NSString* _Nullable armorType, BOOL checksum, NSError* _Nullable* _Nullable error); + +/** + * ArmorWithTypeChecksum armors input with the given armorType. +The checksum option determines if an armor checksum is written at the end. + */ +FOUNDATION_EXPORT NSString* _Nonnull ArmorArmorWithTypeChecksum(NSData* _Nullable input, NSString* _Nullable armorType, BOOL checksum, NSError* _Nullable* _Nullable error); + +// skipped function ArmorWriterWithType with unsupported parameter or return types + + +// skipped function ArmorWriterWithTypeAndCustomHeaders with unsupported parameter or return types + + +// skipped function ArmorWriterWithTypeChecksum with unsupported parameter or return types + + +// skipped function IsPGPArmored with unsupported parameter or return types + + +/** + * Unarmor unarmors an armored input into a byte array. + */ +FOUNDATION_EXPORT NSData* _Nullable ArmorUnarmor(NSString* _Nullable input, NSError* _Nullable* _Nullable error); + +/** + * UnarmorBytes unarmors an armored input into a byte array. + */ +FOUNDATION_EXPORT NSData* _Nullable ArmorUnarmorBytes(NSData* _Nullable input, NSError* _Nullable* _Nullable error); + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Constants.objc.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Constants.objc.h new file mode 100644 index 00000000..3cc01507 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Constants.objc.h @@ -0,0 +1,197 @@ +// Objective-C API for talking to github.com/ProtonMail/gopenpgp/v3/constants Go package. +// gobind -lang=objc github.com/ProtonMail/gopenpgp/v3/constants +// +// File is generated by gobind. Do not edit. + +#ifndef __Constants_H__ +#define __Constants_H__ + +@import Foundation; +#include "ref.h" +#include "Universe.objc.h" + + +/** + * Cipher suite names. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsAES128; +/** + * Cipher suite names. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsAES192; +/** + * Cipher suite names. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsAES256; +/** + * ArmorChecksumEnabled defines the default behavior for adding an armor checksum +to an armored message. + +If set to true, an armor checksum is added to the message. + +If set to false, no armor checksum is added. + */ +FOUNDATION_EXPORT const BOOL ConstantsArmorChecksumEnabled; +/** + * Constants for armored data. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsArmorHeaderComment; +/** + * Constants for armored data. + */ +FOUNDATION_EXPORT const BOOL ConstantsArmorHeaderEnabled; +/** + * Constants for armored data. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsArmorHeaderVersion; +/** + * Cipher suite names. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsCAST5; +/** + * Wraps the packet.CipherFunction enum from go-crypto +for go-mobile clients. +int8 type for go-mobile support. + */ +FOUNDATION_EXPORT const int8_t ConstantsCipher3DES; +/** + * Wraps the packet.CipherFunction enum from go-crypto +for go-mobile clients. +int8 type for go-mobile support. + */ +FOUNDATION_EXPORT const int8_t ConstantsCipherAES128; +/** + * Wraps the packet.CipherFunction enum from go-crypto +for go-mobile clients. +int8 type for go-mobile support. + */ +FOUNDATION_EXPORT const int8_t ConstantsCipherAES192; +/** + * Wraps the packet.CipherFunction enum from go-crypto +for go-mobile clients. +int8 type for go-mobile support. + */ +FOUNDATION_EXPORT const int8_t ConstantsCipherAES256; +/** + * Wraps the packet.CipherFunction enum from go-crypto +for go-mobile clients. +int8 type for go-mobile support. + */ +FOUNDATION_EXPORT const int8_t ConstantsCipherCAST5; +/** + * Use compression defined by the pgp profile. + */ +FOUNDATION_EXPORT const int8_t ConstantsDefaultCompression; +/** + * HighSecurity is the high security level. + */ +FOUNDATION_EXPORT const int8_t ConstantsHighSecurity; +/** + * Use no compression (default). + */ +FOUNDATION_EXPORT const int8_t ConstantsNoCompression; +/** + * Constants for armored data. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsPGPMessageHeader; +/** + * Constants for armored data. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsPGPSignatureHeader; +/** + * Constants for armored data. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsPrivateKeyHeader; +/** + * Constants for armored data. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsPublicKeyHeader; +FOUNDATION_EXPORT const long ConstantsSIGNATURE_BAD_CONTEXT; +FOUNDATION_EXPORT const long ConstantsSIGNATURE_FAILED; +FOUNDATION_EXPORT const long ConstantsSIGNATURE_NOT_SIGNED; +FOUNDATION_EXPORT const long ConstantsSIGNATURE_NO_VERIFIER; +FOUNDATION_EXPORT const long ConstantsSIGNATURE_OK; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeBinary; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeCasualCert; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeCertificationRevocation; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeDirectSignature; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeGenericCert; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeKeyRevocation; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypePersonaCert; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypePositiveCert; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypePrimaryKeyBinding; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeSubkeyBinding; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeSubkeyRevocation; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeText; +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsSignatureContextName; +/** + * StandardSecurity is the default security level. + */ +FOUNDATION_EXPORT const int8_t ConstantsStandardSecurity; +/** + * Cipher suite names. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsThreeDES; +/** + * Cipher suite names. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsTripleDES; +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsVersion; +/** + * Use ZIP compression. + */ +FOUNDATION_EXPORT const int8_t ConstantsZIPCompression; +/** + * Use ZLIB compression. + */ +FOUNDATION_EXPORT const int8_t ConstantsZLIBCompression; + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Crypto.objc.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Crypto.objc.h new file mode 100644 index 00000000..9831bbea --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Crypto.objc.h @@ -0,0 +1,1963 @@ +// Objective-C API for talking to github.com/ProtonMail/gopenpgp/v3/crypto Go package. +// gobind -lang=objc github.com/ProtonMail/gopenpgp/v3/crypto +// +// File is generated by gobind. Do not edit. + +#ifndef __Crypto_H__ +#define __Crypto_H__ + +@import Foundation; +#include "ref.h" +#include "Universe.objc.h" + +#include "Profile.objc.h" +#include "Constants.objc.h" +#include "Armor.objc.h" + +@class CryptoDecryptionHandleBuilder; +@class CryptoEncryptionHandleBuilder; +@class CryptoIdentity; +@class CryptoKey; +@class CryptoKeyGenerationBuilder; +@class CryptoKeyRing; +@class CryptoLiteralMetadata; +@class CryptoPGPHandle; +@class CryptoPGPMessage; +@class CryptoPGPMessageBuffer; +@class CryptoSessionKey; +@class CryptoSignHandleBuilder; +@class CryptoSignatureVerificationError; +@class CryptoSigningContext; +@class CryptoVerificationContext; +@class CryptoVerifiedDataResult; +@class CryptoVerifiedSignature; +@class CryptoVerifyCleartextResult; +@class CryptoVerifyDataReader; +@class CryptoVerifyHandleBuilder; +@class CryptoVerifyResult; +@protocol CryptoEncryptionProfile; +@class CryptoEncryptionProfile; +@protocol CryptoKeyEncryptionProfile; +@class CryptoKeyEncryptionProfile; +@protocol CryptoKeyGenerationProfile; +@class CryptoKeyGenerationProfile; +@protocol CryptoPGPDecryption; +@class CryptoPGPDecryption; +@protocol CryptoPGPEncryption; +@class CryptoPGPEncryption; +@protocol CryptoPGPKeyGeneration; +@class CryptoPGPKeyGeneration; +@protocol CryptoPGPSign; +@class CryptoPGPSign; +@protocol CryptoPGPSplitReader; +@class CryptoPGPSplitReader; +@protocol CryptoPGPSplitWriter; +@class CryptoPGPSplitWriter; +@protocol CryptoPGPVerify; +@class CryptoPGPVerify; +@protocol CryptoReader; +@class CryptoReader; +@protocol CryptoSignProfile; +@class CryptoSignProfile; +@protocol CryptoWriteCloser; +@class CryptoWriteCloser; +@protocol CryptoWriter; +@class CryptoWriter; + +@protocol CryptoEncryptionProfile +// skipped method EncryptionProfile.CompressionConfig with unsupported parameter or return types + +// skipped method EncryptionProfile.EncryptionConfig with unsupported parameter or return types + +@end + +@protocol CryptoKeyEncryptionProfile +// skipped method KeyEncryptionProfile.KeyEncryptionConfig with unsupported parameter or return types + +@end + +@protocol CryptoKeyGenerationProfile +// skipped method KeyGenerationProfile.KeyGenerationConfig with unsupported parameter or return types + +@end + +@protocol CryptoPGPDecryption +/** + * ClearPrivateParams clears all private key material contained in EncryptionHandle from memory. + */ +- (void)clearPrivateParams; +/** + * Decrypt decrypts an encrypted pgp message. +Returns a VerifiedDataResult, which can be queried for potential signature verification errors, +and the plaintext data. Note that on a signature error, the method does not return an error. +Instead, the signature error is stored within the VerifiedDataResult. +The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto +where Auto tries to detect automatically. + */ +- (CryptoVerifiedDataResult* _Nullable)decrypt:(NSData* _Nullable)pgpMessage encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * DecryptDetached provides the same functionality as Decrypt but allows +to supply an encrypted detached signature that should be decrypted and verified +against the data in the pgp message. If encDetachedSignature is nil, the behavior is similar +to Decrypt. The encoding indicates if the input message should be unarmored or not, +i.e., Bytes/Armor/Auto where Auto tries to detect automatically. + */ +- (CryptoVerifiedDataResult* _Nullable)decryptDetached:(NSData* _Nullable)pgpMessage encDetachedSignature:(NSData* _Nullable)encDetachedSignature encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * DecryptSessionKey decrypts an encrypted session key. +To decrypt a session key, the decryption handle must contain either a decryption key or a password. + */ +- (CryptoSessionKey* _Nullable)decryptSessionKey:(NSData* _Nullable)keyPackets error:(NSError* _Nullable* _Nullable)error; +/** + * DecryptingReader returns a wrapper around underlying encryptedMessage Reader, +such that any read-operation via the wrapper results in a read from the decrypted pgp message. +The returned VerifyDataReader has to be fully read before any potential signatures can be verified. +Either read the message fully end then call VerifySignature or use the helper method ReadAllAndVerifySignature. +The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto +where Auto tries to detect automatically. +If encryptedMessage is of type PGPSplitReader, the method tries to verify an encrypted detached signature +that is read from the separate reader. + */ +- (CryptoVerifyDataReader* _Nullable)decryptingReader:(id _Nullable)encryptedMessage encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +@end + +@protocol CryptoPGPEncryption +/** + * ClearPrivateParams clears all private key material contained in EncryptionHandle from memory. + */ +- (void)clearPrivateParams; +/** + * Encrypt encrypts a plaintext message. + */ +- (CryptoPGPMessage* _Nullable)encrypt:(NSData* _Nullable)message error:(NSError* _Nullable* _Nullable)error; +/** + * EncryptSessionKey encrypts a session key with the encryption handle. +To encrypt a session key, the handle must contain either recipients or a password. + */ +- (NSData* _Nullable)encryptSessionKey:(CryptoSessionKey* _Nullable)sessionKey error:(NSError* _Nullable* _Nullable)error; +/** + * EncryptingWriter returns a wrapper around underlying output Writer, +such that any write-operation via the wrapper results in a write to an encrypted pgp message. +If the output Writer is of type PGPSplitWriter, the output can be split to multiple writers +for different parts of the message. For example to write key packets and encrypted data packets +to different writers or to write a detached signature separately. +The encoding argument defines the output encoding, i.e., Bytes or Armored +The returned pgp message WriteCloser must be closed after the plaintext has been written. + */ +- (id _Nullable)encryptingWriter:(id _Nullable)output encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * GenerateSessionKey generates a random session key for the given encryption handle +considering the algorithm preferences of the recipient keys. + */ +- (CryptoSessionKey* _Nullable)generateSessionKey:(NSError* _Nullable* _Nullable)error; +@end + +@protocol CryptoPGPKeyGeneration +/** + * GenerateKey generates a pgp key with the standard security level. + */ +- (CryptoKey* _Nullable)generateKey:(NSError* _Nullable* _Nullable)error; +/** + * GenerateKeyWithSecurity generates a pgp key with the given security level. +The argument security allows to set the security level, either standard or high. + */ +- (CryptoKey* _Nullable)generateKeyWithSecurity:(int8_t)securityLevel error:(NSError* _Nullable* _Nullable)error; +@end + +@protocol CryptoPGPSign +/** + * ClearPrivateParams clears all secret key material contained in the PGPSign from memory. + */ +- (void)clearPrivateParams; +/** + * Sign creates a detached or inline signature from the provided byte slice. +The encoding argument defines the output encoding, i.e., Bytes or Armored + */ +- (NSData* _Nullable)sign:(NSData* _Nullable)message encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * SignCleartext produces an armored cleartext message according to the specification. +Returns an armored message even if the PGPSign is not configured for armored output. + */ +- (NSData* _Nullable)signCleartext:(NSData* _Nullable)message error:(NSError* _Nullable* _Nullable)error; +/** + * SigningWriter returns a wrapper around underlying output Writer, +such that any write-operation via the wrapper results in a write to a detached or inline signature message. +The encoding argument defines the output encoding, i.e., Bytes or Armored +Once close is called on the returned WriteCloser the final signature is written to the output. +Thus, the returned WriteCloser must be closed after the plaintext has been written. + */ +- (id _Nullable)signingWriter:(id _Nullable)output encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +@end + +@protocol CryptoPGPSplitReader +- (BOOL)read:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +- (id _Nullable)signature; +@end + +@protocol CryptoPGPSplitWriter +/** + * Keys returns the Writer to which the key packets are written to. + */ +- (id _Nullable)keys; +/** + * Signature returns the Writer to which an encrypted detached signature is written to. + */ +- (id _Nullable)signature; +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +@protocol CryptoPGPVerify +/** + * VerifyCleartext verifies an armored cleartext message +and returns a VerifyCleartextResult. The VerifyCleartextResult can be checked for failure +and allows access the contained message +Note that an error is only returned if it is not a signature error. + */ +- (CryptoVerifyCleartextResult* _Nullable)verifyCleartext:(NSData* _Nullable)cleartext error:(NSError* _Nullable* _Nullable)error; +/** + * VerifyDetached verifies a detached signature pgp message +and returns a VerifyResult. The VerifyResult can be checked for failure +and allows access to information about the signatures. +Note that an error is only returned if it is not a signature error. +The encoding indicates if the input signature message should be unarmored or not, +i.e., Bytes/Armor/Auto where Auto tries to detect it automatically. + */ +- (CryptoVerifyResult* _Nullable)verifyDetached:(NSData* _Nullable)data signature:(NSData* _Nullable)signature encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * VerifyInline verifies an inline signed pgp message +and returns a VerifiedDataResult. The VerifiedDataResult can be checked for failure, +allows access to information about the signatures, and includes the plain message. +Note that an error is only returned if it is not a signature error. +The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto +where Auto tries to detect it automatically. + */ +- (CryptoVerifiedDataResult* _Nullable)verifyInline:(NSData* _Nullable)message encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * VerifyingReader wraps a reader with a signature verify reader. +Once all data is read from the returned verify reader, the signature can be verified +with (VerifyDataReader).VerifySignature(). +Note that an error is only returned if it is not a signature error. +The encoding indicates if the input signature message should be unarmored or not, +i.e., Bytes/Armor/Auto where Auto tries to detect it automatically. +If detachedData is nil, signatureMessage is treated as an inline signature message. +Thus, it is expected that signatureMessage contains the data to be verified. +If detachedData is not nil, signatureMessage must contain a detached signature, +which is verified against the detachedData. + */ +- (CryptoVerifyDataReader* _Nullable)verifyingReader:(id _Nullable)detachedData signatureMessage:(id _Nullable)signatureMessage encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +@end + +@protocol CryptoReader +- (BOOL)read:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +@protocol CryptoSignProfile +// skipped method SignProfile.SignConfig with unsupported parameter or return types + +@end + +@protocol CryptoWriteCloser +- (BOOL)close:(NSError* _Nullable* _Nullable)error; +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +@protocol CryptoWriter +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * DecryptionHandleBuilder allows to configure a decryption handle +to decrypt a pgp message. + */ +@interface CryptoDecryptionHandleBuilder : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +- (CryptoDecryptionHandleBuilder* _Nullable)decryptionKey:(CryptoKey* _Nullable)decryptionKey; +/** + * DecryptionKeys sets the secret keys for decrypting the pgp message. +Assumes that the message was encrypted towards one of the secret keys. +Triggers the hybrid decryption mode. +If not set, set another field for the type of decryption: SessionKey or Password. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)decryptionKeys:(CryptoKeyRing* _Nullable)decryptionKeyRing; +/** + * DisableAutomaticTextSanitize indicates that automatic text sanitization should be disabled. +If not disabled, the output will be sanitized if a text signature is present. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)disableAutomaticTextSanitize; +/** + * DisableIntendedRecipients indicates if the signature verification should not check if +the decryption key matches the intended recipients of the message. +If disabled, the decryption methods throw no error in a non-matching case. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)disableIntendedRecipients; +/** + * DisableStrictMessageParsing disables the check that decryption inputs conform +to the OpenPGP Message grammar. +If set, the decryption methods return no error if the message does not conform to the +OpenPGP message grammar. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)disableStrictMessageParsing; +/** + * DisableVerifyTimeCheck disables the check for comparing the signature creation time +against the verification time. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)disableVerifyTimeCheck; +- (BOOL)error:(NSError* _Nullable* _Nullable)error; +/** + * InsecureAllowDecryptionWithSigningKeys enables decryption of messages using keys +that are designated solely as signing keys. +While using the same key for both encryption and signing is discouraged +due to reduced security, this flag is useful for decrypting legacy messages. +This is because some older libraries did not respect key flags when +selecting a key for encryption. +SECURITY HAZARD: Use with care. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)insecureAllowDecryptionWithSigningKeys; +/** + * InsecureDisableUnauthenticatedMessagesCheck enables to read +encrypted messages without Modification Detection Code (MDC). +MDC is mandated by the latest standard and has long been implemented +in most OpenPGP implementations. Messages without MDC are considered unnecessarily +insecure and should be prevented whenever possible. +In case one needs to deal with messages from very old OpenPGP implementations, there +might be no other way than to tolerate the missing MDC. Setting this flag, allows this +mode of operation. It should be considered a measure of last resort. +SECURITY HAZARD: Use with care. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)insecureDisableUnauthenticatedMessagesCheck; +/** + * MaxDecompressedMessageSize defines the maximum number of bytes allowed for a message +after decompression. An error is thrown if the decompressed data exceeds this limit. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)maxDecompressedMessageSize:(int64_t)size; +/** + * New creates a DecryptionHandle and checks that the given +combination of parameters is valid. If one of the parameters are invalid +the latest error is returned. + */ +- (id _Nullable)new:(NSError* _Nullable* _Nullable)error; +/** + * Password sets a password that is used to derive a key to decrypt the pgp message. +Assumes that the message was encrypted with a key derived from the password. +Triggers the password decryption mode. +If not set, set another field for the type of decryption: DecryptionKeys or SessionKey. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)password:(NSData* _Nullable)password; +// skipped method DecryptionHandleBuilder.Passwords with unsupported parameter or return types + +/** + * PlainDetachedSignature indicates that the detached signature to verify is not decrypted +and can be verified as is. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)plainDetachedSignature; +/** + * RetrieveSessionKey sets the flag to indicate if the session key used for decryption +should be returned to the caller of the decryption function. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)retrieveSessionKey; +/** + * SessionKey sets a session key for decrypting the pgp message. +Assumes that the message was encrypted with session key provided. +Triggers the session key decryption mode. +If not set, set another field for the type of decryption: DecryptionKeys or Password. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)sessionKey:(CryptoSessionKey* _Nullable)sessionKey; +// skipped method DecryptionHandleBuilder.SessionKeys with unsupported parameter or return types + +/** + * Utf8 indicates if the output plaintext is Utf8 and +should be sanitized from canonicalised line endings. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)utf8; +/** + * VerificationContext sets a verification context for signatures of the pgp message, if any. +Only considered if VerifyKeys are set. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)verificationContext:(CryptoVerificationContext* _Nullable)verifyContext; +/** + * VerificationKey sets the public key for verifying the signatures of the pgp message, if any. +If not set, the signatures cannot be verified. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)verificationKey:(CryptoKey* _Nullable)key; +/** + * VerificationKeys sets the public keys for verifying the signatures of the pgp message, if any. +If not set, the signatures cannot be verified. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)verificationKeys:(CryptoKeyRing* _Nullable)keys; +/** + * VerifyTime sets the verification time to the provided timestamp. +If not set, the systems current time is used for signature verification. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)verifyTime:(int64_t)unixTime; +@end + +/** + * EncryptionHandleBuilder allows to configure a decryption handle to decrypt an OpenPGP message. + */ +@interface CryptoEncryptionHandleBuilder : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +/** + * Compress indicates if the plaintext should be compressed before encryption. +Compression affects security and opens the door for side-channel attacks, which +might allow to extract the plaintext data without a decryption key. +RFC9580 recommends to not use compression. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)compress; +/** + * CompressWith indicates if the plaintext should be compressed before encryption. +Compression affects security and opens the door for side-channel attacks, which +might allow to extract the plaintext data without a decryption key. +RFC9580 recommends to not use compression. +Allowed config options: +constants.NoCompression: none, constants.DefaultCompression: profile default +constants.ZIPCompression: zip, constants.ZLIBCompression: zlib. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)compressWith:(int8_t)config; +/** + * DetachedSignature indicates that the message should be signed, +but the signature should not be included in the same pgp message as the input data. +Instead the detached signature is encrypted in a separate pgp message. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)detachedSignature; +/** + * EncryptionTime allows to specify a separate time for selecting encryption keys +instead of the internal clock (also used for signing). Note that the internal clock can be changed with SignTime. +If the input unixTime is 0 no expiration checks are performed on the encryption keys. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)encryptionTime:(int64_t)unixTime; +/** + * Error returns an errors that occurred within the builder. + */ +- (BOOL)error:(NSError* _Nullable* _Nullable)error; +/** + * HiddenRecipient sets a public key to which the message should be encrypted to. +Triggers hybrid encryption with public keys of the recipients and hidden recipients. +The hidden recipients are NOT included in the intended recipient fingerprint list +of the signature, if a signature is present. +If not set, set another type of encryption: Recipients, SessionKey, or Password. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)hiddenRecipient:(CryptoKey* _Nullable)key; +/** + * HiddenRecipients sets the public keys to which the message should be encrypted to. +Triggers hybrid encryption with public keys of the recipients and hidden recipients. +The hidden recipients are NOT included in the intended recipient fingerprint list +of the signature, if a signature is present. +If not set, set another type of encryption: Recipients, SessionKey, or Password. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)hiddenRecipients:(CryptoKeyRing* _Nullable)hiddenRecipients; +/** + * IncludeExternalSignature indicates that the provided signature should be included +in the produced encrypted message. +Special feature: should not be used in normal use-cases, +can lead to broken or invalid PGP messages. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)includeExternalSignature:(NSData* _Nullable)signature; +/** + * New creates an EncryptionHandle and checks that the given +combination of parameters is valid. If the parameters are invalid +an error is returned. + */ +- (id _Nullable)new:(NSError* _Nullable* _Nullable)error; +/** + * Password sets a password the message should be encrypted with. +Triggers password based encryption with a key derived from the password. +If not set, set another the type of encryption: Recipients, HiddenRecipients, or SessionKey. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)password:(NSData* _Nullable)password; +/** + * PlainDetachedSignature indicates that the message should be signed, +but the signature should not be included in the same pgp message as the input data. +Instead the detached signature is a separate signature pgp message. +If DetachedSignature signature is set (i.e., the detached signature is encrypted), this option is ignored. +NOTE: A plaintext detached signature might reveal information about the encrypted plaintext. Thus, use with care. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)plainDetachedSignature; +/** + * Recipient sets the public key to which the message should be encrypted to. +Triggers hybrid encryption with public keys of the recipients and hidden recipients. +The recipients are included in the intended recipient fingerprint list +of the signature, if a signature is present. +If not set, set another type of encryption: HiddenRecipients, SessionKey, or Password. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)recipient:(CryptoKey* _Nullable)key; +/** + * Recipients sets the public keys to which the message should be encrypted to. +Triggers hybrid encryption with public keys of the recipients and hidden recipients. +The recipients are included in the intended recipient fingerprint list +of the signature, if a signature is present. +If not set, set another type of encryption: HiddenRecipients, SessionKey, or Password. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)recipients:(CryptoKeyRing* _Nullable)recipients; +/** + * SessionKey sets the session key the message should be encrypted with. +Triggers session key encryption with the included session key. +If not set, set another the type of encryption: Recipients, HiddenRecipients, or Password. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)sessionKey:(CryptoSessionKey* _Nullable)sessionKey; +/** + * SignTime sets the internal clock to always return +the supplied unix time for signing instead of the system time. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)signTime:(int64_t)unixTime; +/** + * SigningContext provides a signing context for the signature in the message. +Triggers that each signature includes the sining context. +SigningKeys have to be set if a SigningContext is provided. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)signingContext:(CryptoSigningContext* _Nullable)siningContext; +/** + * SigningKey sets the signing key that are used to create signature of the message. +Triggers that signatures are created for each signing key. +If not set, no signature is included. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)signingKey:(CryptoKey* _Nullable)key; +/** + * SigningKeys sets the signing keys that are used to create signature of the message. +Triggers that signatures are created for each signing key. +If not set, no signature is included. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)signingKeys:(CryptoKeyRing* _Nullable)signingKeys; +/** + * Utf8 indicates if the plaintext should be signed with a text type +signature. If set, the plaintext is signed after canonicalising the line endings. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)utf8; +@end + +/** + * Identity contains the name and the email of a key holder. + */ +@interface CryptoIdentity : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +@property (nonatomic) NSString* _Nonnull name; +@property (nonatomic) NSString* _Nonnull email; +@end + +/** + * Key contains a single private or public key. + */ +@interface CryptoKey : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewKey creates a new key from the first key in the unarmored or armored binary data. +Clones the binKeys data for go-mobile compatibility. + */ +- (nullable instancetype)init:(NSData* _Nullable)binKeys; +/** + * NewKeyFromArmored creates a new key from the first key in an armored string. + */ +- (nullable instancetype)initFromArmored:(NSString* _Nullable)armored; +// skipped constructor Key.NewKeyFromEntity with unsupported parameter or return types + +// skipped constructor Key.NewKeyFromReader with unsupported parameter or return types + +// skipped constructor Key.NewKeyFromReaderExplicit with unsupported parameter or return types + +/** + * NewKeyWithCloneFlag creates a new key from the first key in the unarmored or armored binary data. + */ +- (nullable instancetype)initWithCloneFlag:(NSData* _Nullable)binKeys clone:(BOOL)clone; +/** + * Armor returns the armored key as a string with default gopenpgp headers. + */ +- (NSString* _Nonnull)armor:(NSError* _Nullable* _Nullable)error; +/** + * ArmorWithCustomHeaders returns the armored key as a string, with +the given headers. Empty parameters are omitted from the headers. + */ +- (NSString* _Nonnull)armorWithCustomHeaders:(NSString* _Nullable)comment version:(NSString* _Nullable)version error:(NSError* _Nullable* _Nullable)error; +/** + * CanEncrypt returns true if any of the subkeys can be used for encryption. + */ +- (BOOL)canEncrypt:(int64_t)unixTime; +/** + * CanVerify returns true if any of the subkeys can be used for verification. + */ +- (BOOL)canVerify:(int64_t)unixTime; +/** + * Check verifies if the public keys match the private key parameters by +signing and verifying. +Deprecated: all keys are now checked on parsing. + */ +- (BOOL)check:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; +/** + * ClearPrivateParams zeroes the sensitive data in the key. + */ +- (BOOL)clearPrivateParams; +/** + * Copy creates a deep copy of the key. + */ +- (CryptoKey* _Nullable)copy:(NSError* _Nullable* _Nullable)error; +/** + * GetArmoredPublicKey returns the armored public keys from this keyring. + */ +- (NSString* _Nonnull)getArmoredPublicKey:(NSError* _Nullable* _Nullable)error; +/** + * GetArmoredPublicKeyWithCustomHeaders returns the armored public key as a string, with +the given headers. Empty parameters are omitted from the headers. + */ +- (NSString* _Nonnull)getArmoredPublicKeyWithCustomHeaders:(NSString* _Nullable)comment version:(NSString* _Nullable)version error:(NSError* _Nullable* _Nullable)error; +// skipped method Key.GetEntity with unsupported parameter or return types + +/** + * GetFingerprint gets the fingerprint from the key. + */ +- (NSString* _Nonnull)getFingerprint; +/** + * GetFingerprintBytes gets the fingerprint from the key as a byte slice. + */ +- (NSData* _Nullable)getFingerprintBytes; +/** + * GetHexKeyID returns the key ID, hex encoded as a string. + */ +- (NSString* _Nonnull)getHexKeyID; +/** + * GetJsonSHA256Fingerprints returns the SHA256 fingerprints of key and subkeys +encoded in JSON, for gomobile clients that cannot handle arrays. + */ +- (NSData* _Nullable)getJsonSHA256Fingerprints:(NSError* _Nullable* _Nullable)error; +// skipped method Key.GetKeyID with unsupported parameter or return types + +/** + * GetPublicKey returns the unarmored public keys from this keyring. + */ +- (NSData* _Nullable)getPublicKey:(NSError* _Nullable* _Nullable)error; +/** + * GetSHA256Fingerprint computes the SHA256 fingerprint of the primary key. + */ +- (NSString* _Nonnull)getSHA256Fingerprint; +// skipped method Key.GetSHA256Fingerprints with unsupported parameter or return types + +/** + * GetVersion returns the OpenPGP key packet version of this key. + */ +- (long)getVersion; +/** + * IsExpired checks whether the key is expired. + */ +- (BOOL)isExpired:(int64_t)unixTime; +/** + * IsLocked checks if a private key is locked. + */ +- (BOOL)isLocked:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; +/** + * IsPrivate returns true if the key is private. + */ +- (BOOL)isPrivate; +/** + * IsRevoked checks whether the key or the primary identity has a valid revocation signature. + */ +- (BOOL)isRevoked:(int64_t)unixTime; +/** + * IsUnlocked checks if a private key is unlocked. + */ +- (BOOL)isUnlocked:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; +/** + * PrintFingerprints is a debug helper function that prints the key and subkey fingerprints. + */ +- (void)printFingerprints; +- (NSData* _Nullable)serialize:(NSError* _Nullable* _Nullable)error; +/** + * ToPublic returns the corresponding public key of the given private key. + */ +- (CryptoKey* _Nullable)toPublic:(NSError* _Nullable* _Nullable)error; +/** + * Unlock unlocks a copy of the key. + */ +- (CryptoKey* _Nullable)unlock:(NSData* _Nullable)passphrase error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * KeyGenerationBuilder allows to configure a key generation handle to generate OpenPGP keys. + */ +@interface CryptoKeyGenerationBuilder : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +/** + * AddUserId adds the provided user identity to any generated key. + */ +- (CryptoKeyGenerationBuilder* _Nullable)addUserId:(NSString* _Nullable)name email:(NSString* _Nullable)email; +/** + * GenerationTime sets the key generation time to the given unixTime. + */ +- (CryptoKeyGenerationBuilder* _Nullable)generationTime:(int64_t)unixTime; +/** + * Lifetime sets the key lifetime to the given value in seconds. +The lifetime defaults to zero i.e., infinite lifetime. + */ +- (CryptoKeyGenerationBuilder* _Nullable)lifetime:(int32_t)seconds; +/** + * New creates a new key generation handle from the internal configuration +that allows to generate pgp keys. + */ +- (id _Nullable)new; +/** + * OverrideProfileAlgorithm allows to override the algorithm of the output key instead of using the profile's +algorithm with the respective security level. + +Allowed inputs (integer enum for go-mobile compatibility): +crypto.KeyGenerationRSA4096, crypto.KeyGenerationC25519, crypto.KeyGenerationC25519Refresh +crypto.KeyGenerationC448, crypto.KeyGenerationC448Refresh. + */ +- (CryptoKeyGenerationBuilder* _Nullable)overrideProfileAlgorithm:(long)algorithm; +@end + +/** + * KeyRing contains multiple private and public keys. + */ +@interface CryptoKeyRing : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewKeyRing creates a new KeyRing, empty if key is nil. + */ +- (nullable instancetype)init:(CryptoKey* _Nullable)key; +/** + * NewKeyRingFromBinary creates a new keyring with all the keys contained in the unarmored binary data. +Note that it accepts only unlocked or public keys, as KeyRing cannot contain locked keys. + */ +- (nullable instancetype)initFromBinary:(NSData* _Nullable)binKeys; +/** + * FirstKeyID as obtained from API to match salt + */ +@property (nonatomic) NSString* _Nonnull firstKeyID; +/** + * AddKey adds the given key to the keyring. + */ +- (BOOL)addKey:(CryptoKey* _Nullable)key error:(NSError* _Nullable* _Nullable)error; +/** + * CanEncrypt returns true if any of the keys in the keyring can be used for encryption. + */ +- (BOOL)canEncrypt:(int64_t)unixTime; +/** + * CanVerify returns true if any of the keys in the keyring can be used for verification. + */ +- (BOOL)canVerify:(int64_t)unixTime; +- (void)clearPrivateParams; +/** + * Copy creates a deep copy of the keyring. + */ +- (CryptoKeyRing* _Nullable)copy:(NSError* _Nullable* _Nullable)error; +/** + * CountDecryptionEntities returns the number of entities in the keyring. +Takes the current time for checking the keys in unix time format. +If the unix time is zero, time checks are ignored. + */ +- (long)countDecryptionEntities:(int64_t)unixTime; +/** + * CountEntities returns the number of entities in the keyring. + */ +- (long)countEntities; +/** + * FirstKey returns a KeyRing with only the first key of the original one. + */ +- (CryptoKeyRing* _Nullable)firstKey:(NSError* _Nullable* _Nullable)error; +/** + * GetHexKeyIDsJson returns an IDs of keys in this KeyRing as a json array. +Key ids are encoded as hexadecimal and nil is returned if an error occurs. +Helper function for go-mobile clients. + */ +- (NSData* _Nullable)getHexKeyIDsJson; +// skipped method KeyRing.GetIdentities with unsupported parameter or return types + +/** + * GetIdentitiesJson returns the list of identities associated with this key ring encoded as json. +Returns nil if an encoding error occurs. +Helper function for go-mobile clients. + */ +- (NSData* _Nullable)getIdentitiesJson; +/** + * GetKey returns the n-th openpgp key contained in this KeyRing. + */ +- (CryptoKey* _Nullable)getKey:(long)n error:(NSError* _Nullable* _Nullable)error; +// skipped method KeyRing.GetKeyIDs with unsupported parameter or return types + +// skipped method KeyRing.GetKeys with unsupported parameter or return types + +/** + * Serialize serializes a KeyRing to binary data. + */ +- (NSData* _Nullable)serialize:(NSError* _Nullable* _Nullable)error; +@end + +@interface CryptoLiteralMetadata : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +/** + * The file's latest modification time + */ +@property (nonatomic) int64_t modTime; +/** + * Filename returns the filename of the literal metadata. + */ +- (NSString* _Nonnull)filename; +/** + * IsUtf8 returns whether the literal metadata is annotated with utf-8. + */ +- (BOOL)isUtf8; +- (int64_t)time; +@end + +@interface CryptoPGPHandle : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +/** + * Decryption returns a builder to create a DecryptionHandle +for decrypting pgp messages. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)decryption; +/** + * Encryption returns a builder to create an EncryptionHandle +for encrypting messages. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)encryption; +/** + * GenerateSessionKey generates a random session key for the profile. +Use GenerateSessionKey on the encryption handle, if the PGP encryption keys are known. +This function only considers the profile to determine the session key type. + */ +- (CryptoSessionKey* _Nullable)generateSessionKey:(NSError* _Nullable* _Nullable)error; +/** + * KeyGeneration returns a builder to create a KeyGeneration handle. + */ +- (CryptoKeyGenerationBuilder* _Nullable)keyGeneration; +/** + * LockKey encrypts the private parts of a copy of the input key with the given passphrase. + */ +- (CryptoKey* _Nullable)lockKey:(CryptoKey* _Nullable)key passphrase:(NSData* _Nullable)passphrase error:(NSError* _Nullable* _Nullable)error; +/** + * Sign returns a builder to create a SignHandle +for signing messages. + */ +- (CryptoSignHandleBuilder* _Nullable)sign; +/** + * Verify returns a builder to create an VerifyHandle +for verifying signatures. + */ +- (CryptoVerifyHandleBuilder* _Nullable)verify; +@end + +/** + * PGPMessage stores a PGP-encrypted message. + */ +@interface CryptoPGPMessage : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewPGPMessage generates a new PGPMessage from the unarmored binary data. +Clones the data for go-mobile compatibility. + */ +- (nullable instancetype)init:(NSData* _Nullable)data; +/** + * NewPGPMessageFromArmored generates a new PGPMessage from an armored string ready for decryption. + */ +- (nullable instancetype)initFromArmored:(NSString* _Nullable)armored; +/** + * NewPGPMessageWithCloneFlag generates a new PGPMessage from the unarmored binary data. + */ +- (nullable instancetype)initWithCloneFlag:(NSData* _Nullable)data doClone:(BOOL)doClone; +/** + * KeyPacket references the PKESK and SKESK packets of the message + */ +@property (nonatomic) NSData* _Nullable keyPacket; +/** + * DataPacket references the SEIPD or AEAD protected packet of the message + */ +@property (nonatomic) NSData* _Nullable dataPacket; +/** + * DetachedSignature stores the encrypted detached signature. +Nil when the signature is embedded in the data packet or not present. + */ +@property (nonatomic) NSData* _Nullable detachedSignature; +/** + * Armor returns the armored message as a string. + */ +- (NSString* _Nonnull)armor:(NSError* _Nullable* _Nullable)error; +/** + * ArmorBytes returns the armored message as a string. + */ +- (NSData* _Nullable)armorBytes:(NSError* _Nullable* _Nullable)error; +/** + * ArmorWithCustomHeaders returns the armored message as a string, with +the given headers. Empty parameters are omitted from the headers. + */ +- (NSString* _Nonnull)armorWithCustomHeaders:(NSString* _Nullable)comment version:(NSString* _Nullable)version error:(NSError* _Nullable* _Nullable)error; +/** + * BinaryDataPacket returns the unarmored binary datapacket as a []byte. + */ +- (NSData* _Nullable)binaryDataPacket; +/** + * BinaryKeyPacket returns the unarmored binary keypacket as a []byte. + */ +- (NSData* _Nullable)binaryKeyPacket; +/** + * Bytes returns the unarmored binary content of the message as a []byte. + */ +- (NSData* _Nullable)bytes; +/** + * EncryptedDetachedSignature returns the encrypted detached signature of this message +as a PGPMessage where the data is the encrypted signature. +If no detached signature is present in this message, it returns nil. + */ +- (CryptoPGPMessage* _Nullable)encryptedDetachedSignature; +// skipped method PGPMessage.EncryptionKeyIDs with unsupported parameter or return types + +/** + * GetNumberOfKeyPackets returns the number of keys packets in this message. + */ +- (BOOL)getNumberOfKeyPackets:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; +// skipped method PGPMessage.HexEncryptionKeyIDs with unsupported parameter or return types + +/** + * HexEncryptionKeyIDsJson returns the key IDs of the keys to which the session key is encrypted as a JSON array. +If an error occurs it returns nil. +Helper function for go-mobile clients. + */ +- (NSData* _Nullable)hexEncryptionKeyIDsJson; +// skipped method PGPMessage.HexSignatureKeyIDs with unsupported parameter or return types + +/** + * HexSignatureKeyIDsJson returns the key IDs of the keys to which the session key is encrypted as a JSON array. +If an error occurs it returns nil. +Helper function for go-mobile clients. + */ +- (NSData* _Nullable)hexSignatureKeyIDsJson; +// skipped method PGPMessage.NewReader with unsupported parameter or return types + +/** + * PlainDetachedSignature returns the plaintext detached signature of this message. +If no plaintext detached signature is present in this message, it returns an error. + */ +- (NSData* _Nullable)plainDetachedSignature:(NSError* _Nullable* _Nullable)error; +/** + * PlainDetachedSignatureArmor returns the armored plaintext detached signature of this message. +If no plaintext detached signature is present or armoring fails it returns an error. + */ +- (NSData* _Nullable)plainDetachedSignatureArmor:(NSError* _Nullable* _Nullable)error; +// skipped method PGPMessage.SignatureKeyIDs with unsupported parameter or return types + +@end + +@interface CryptoPGPMessageBuffer : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewPGPMessageBuffer creates a message buffer. + */ +- (nullable instancetype)init; +- (id _Nullable)keys; +/** + * PGPMessage returns the PGPMessage extracted from the internal buffers. + */ +- (CryptoPGPMessage* _Nullable)pgpMessage; +/** + * PGPMessageWithOptions returns the PGPMessage extracted from the internal buffers. +The isPlain flag indicates wether the detached signature is encrypted or plaintext, if any. + */ +- (CryptoPGPMessage* _Nullable)pgpMessageWithOptions:(BOOL)isPlain omitArmorChecksum:(BOOL)omitArmorChecksum; +- (id _Nullable)signature; +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * SessionKey stores a decrypted session key. + */ +@interface CryptoSessionKey : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewSessionKeyFromToken creates a SessionKey struct with the given token and algorithm. +Clones the token for compatibility with go-mobile. + */ +- (nullable instancetype)initFromToken:(NSData* _Nullable)token algo:(NSString* _Nullable)algo; +/** + * NewSessionKeyFromTokenWithAead creates a SessionKey struct with the given token and algorithm. +If aead is set to true, the key is used with v6 PKESK or SKESK, and SEIPDv2 packets. + */ +- (nullable instancetype)initFromTokenWithAead:(NSData* _Nullable)token algo:(NSString* _Nullable)algo aead:(BOOL)aead; +/** + * Key defines the decrypted binary session key. + */ +@property (nonatomic) NSData* _Nullable key; +/** + * Algo defines the symmetric encryption algorithm used with this key. +Only present if the key was not parsed from a v6 packet. + */ +@property (nonatomic) NSString* _Nonnull algo; +- (BOOL)clear; +/** + * GetBase64Key returns the session key as base64 encoded string. + */ +- (NSString* _Nonnull)getBase64Key; +// skipped method SessionKey.GetCipherFunc with unsupported parameter or return types + +/** + * GetCipherFuncInt returns the cipher function as int8 corresponding to the algorithm used +with this SessionKey. +The int8 type is used for go-mobile clients, see constant.Cipher... + */ +- (BOOL)getCipherFuncInt:(int8_t* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; +/** + * IsV6 indicates if the session key can be used with SEIPDv2, PKESKv6/SKESKv6. + */ +- (BOOL)isV6; +@end + +/** + * SignHandleBuilder allows to configure a sign handle +to sign data with OpenPGP. + */ +@interface CryptoSignHandleBuilder : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +/** + * ArmorHeader indicates that the produced signature should be armored +with the given version and comment as header. +Note that this option only affects the method SignHandle.SigningWriter +and the headers in SignHandle.SignCleartext. + */ +- (CryptoSignHandleBuilder* _Nullable)armorHeader:(NSString* _Nullable)version comment:(NSString* _Nullable)comment; +/** + * Detached indicates if a detached signature should be produced. +The sign output will be a detached signature message without the data included. + */ +- (CryptoSignHandleBuilder* _Nullable)detached; +/** + * Error returns any errors that occurred within the builder. + */ +- (BOOL)error:(NSError* _Nullable* _Nullable)error; +/** + * New creates a SignHandle and checks that the given +combination of parameters is valid. If the parameters are invalid +an error is returned. + */ +- (id _Nullable)new:(NSError* _Nullable* _Nullable)error; +/** + * SignTime sets the internal clock to always return +the supplied unix time for signing instead of the device time. + */ +- (CryptoSignHandleBuilder* _Nullable)signTime:(int64_t)unixTime; +/** + * SigningContext provides a signing context for the signature in the message. +Triggers that each signature includes the sining context. + */ +- (CryptoSignHandleBuilder* _Nullable)signingContext:(CryptoSigningContext* _Nullable)signingContext; +/** + * SigningKey sets the signing key that is used to create signature of the message. + */ +- (CryptoSignHandleBuilder* _Nullable)signingKey:(CryptoKey* _Nullable)key; +/** + * SigningKeys sets the signing keys that are used to create signature of the message. + */ +- (CryptoSignHandleBuilder* _Nullable)signingKeys:(CryptoKeyRing* _Nullable)signingKeys; +/** + * Utf8 indicates if the plaintext should be signed with a text type +signature. If set, the plaintext is signed after +canonicalising the line endings. + */ +- (CryptoSignHandleBuilder* _Nullable)utf8; +@end + +/** + * SignatureVerificationError is returned from Decrypt and VerifyDetached +functions when signature verification fails. + */ +@interface CryptoSignatureVerificationError : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +@property (nonatomic) long status; +@property (nonatomic) NSString* _Nonnull message; +@property (nonatomic) NSError* _Nullable cause; +/** + * Error is the base method for all errors. + */ +- (NSString* _Nonnull)error; +/** + * Unwrap returns the cause of failure. + */ +- (BOOL)unwrap:(NSError* _Nullable* _Nullable)error; +@end + +/** + * SigningContext gives the context that will be +included in the signature's notation data. + */ +@interface CryptoSigningContext : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewSigningContext creates a new signing context. +The value is set to the notation data. +isCritical controls whether the notation is flagged as a critical packet. + */ +- (nullable instancetype)init:(NSString* _Nullable)value isCritical:(BOOL)isCritical; +@property (nonatomic) NSString* _Nonnull value; +@property (nonatomic) BOOL isCritical; +@end + +/** + * VerificationContext gives the context that will be +used to verify the signature. + */ +@interface CryptoVerificationContext : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewVerificationContext creates a new verification context. +The value is checked against the signature's notation data. +If isRequired is false, the signature is allowed to have no context set. +If requiredAfter is != 0, the signature is allowed to have no context set if it +was created before the unix time set in requiredAfter. + */ +- (nullable instancetype)init:(NSString* _Nullable)value isRequired:(BOOL)isRequired requiredAfter:(int64_t)requiredAfter; +@property (nonatomic) NSString* _Nonnull value; +@property (nonatomic) BOOL isRequired; +@property (nonatomic) int64_t requiredAfter; +@end + +/** + * VerifiedDataResult is a result that contains data and +the result of a potential signature verification on the data. + */ +@interface CryptoVerifiedDataResult : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field VerifiedDataResult.VerifyResult with unsupported type: github.com/ProtonMail/gopenpgp/v3/crypto.VerifyResult + +/** + * Bytes returns the result data as bytes. + */ +- (NSData* _Nullable)bytes; +- (void)constrainToTimeRange:(int64_t)unixFrom unixTo:(int64_t)unixTo; +/** + * Metadata returns the associated literal metadata of the data. + */ +- (CryptoLiteralMetadata* _Nullable)metadata; +/** + * SessionKey returns the session key the data is decrypted with. +Returns nil, if the data was not encrypted or +session key caching was not enabled. + */ +- (CryptoSessionKey* _Nullable)sessionKey; +- (NSData* _Nullable)signature:(NSError* _Nullable* _Nullable)error; +- (int64_t)signatureCreationTime; +- (BOOL)signatureError:(NSError* _Nullable* _Nullable)error; +- (CryptoSignatureVerificationError* _Nullable)signatureErrorExplicit; +- (NSData* _Nullable)signedByFingerprint; +- (CryptoKey* _Nullable)signedByKey; +// skipped method VerifiedDataResult.SignedByKeyId with unsupported parameter or return types + +- (NSString* _Nonnull)signedByKeyIdHex; +// skipped method VerifiedDataResult.SignedWithType with unsupported parameter or return types + +- (int8_t)signedWithTypeInt8; +/** + * String returns the result data as string. + */ +- (NSString* _Nonnull)string; +@end + +/** + * VerifiedSignature is a result of a signature verification. + */ +@interface CryptoVerifiedSignature : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field VerifiedSignature.Signature with unsupported type: *github.com/ProtonMail/go-crypto/openpgp/packet.Signature + +@property (nonatomic) CryptoKey* _Nullable signedBy; +@property (nonatomic) CryptoSignatureVerificationError* _Nullable signatureError; +@end + +/** + * VerifyCleartextResult is a result of a cleartext message verification. + */ +@interface CryptoVerifyCleartextResult : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field VerifyCleartextResult.VerifyResult with unsupported type: github.com/ProtonMail/gopenpgp/v3/crypto.VerifyResult + +/** + * Cleartext returns the parsed plain text of the result. + */ +- (NSData* _Nullable)cleartext; +- (void)constrainToTimeRange:(int64_t)unixFrom unixTo:(int64_t)unixTo; +- (NSData* _Nullable)signature:(NSError* _Nullable* _Nullable)error; +- (int64_t)signatureCreationTime; +- (BOOL)signatureError:(NSError* _Nullable* _Nullable)error; +- (CryptoSignatureVerificationError* _Nullable)signatureErrorExplicit; +- (NSData* _Nullable)signedByFingerprint; +- (CryptoKey* _Nullable)signedByKey; +// skipped method VerifyCleartextResult.SignedByKeyId with unsupported parameter or return types + +- (NSString* _Nonnull)signedByKeyIdHex; +// skipped method VerifyCleartextResult.SignedWithType with unsupported parameter or return types + +- (int8_t)signedWithTypeInt8; +@end + +/** + * VerifyDataReader is used for reading data that should be verified with a signature. +It further contains additional information about the parsed pgp message where the read +data stems from. + */ +@interface CryptoVerifyDataReader : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +/** + * DiscardAll reads all data from the reader and discards it. + */ +- (BOOL)discardAll:(NSError* _Nullable* _Nullable)error; +/** + * DiscardAllAndVerifySignature reads all plaintext data from the reader but discards it. +Returns a verification result for signature verification on the read data. + */ +- (CryptoVerifyResult* _Nullable)discardAllAndVerifySignature:(NSError* _Nullable* _Nullable)error; +/** + * GetMetadata returns the metadata of the literal data packet that +this reader reads from. Can be nil, if the data is not read from +a literal data packet. + */ +- (CryptoLiteralMetadata* _Nullable)getMetadata; +/** + * Read is used read data from the pgp message. +Makes VerifyDataReader implement the Reader interface. + */ +- (BOOL)read:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +/** + * ReadAll reads all plaintext data from the reader +and returns it as a byte slice. + */ +- (NSData* _Nullable)readAll:(NSError* _Nullable* _Nullable)error; +/** + * ReadAllAndVerifySignature reads all plaintext data from the reader +and tries to verify the signatures included in the message. +Returns the data in a VerifiedDataResult struct, which can be checked for signature errors. + */ +- (CryptoVerifiedDataResult* _Nullable)readAllAndVerifySignature:(NSError* _Nullable* _Nullable)error; +/** + * SessionKey returns the session key the data is decrypted with. +Returns nil, if this reader does not read from an encrypted message or +session key caching was not enabled. + */ +- (CryptoSessionKey* _Nullable)sessionKey; +/** + * VerifySignature is used to verify that the embedded signatures are valid. +This method needs to be called once all the data has been read. +It will return an error if the signature is invalid, no verifying keys are accessible, +or if the message hasn't been read entirely. + */ +- (CryptoVerifyResult* _Nullable)verifySignature:(NSError* _Nullable* _Nullable)error; +@end + +/** + * VerifyHandleBuilder configures a VerifyHandle handle. + */ +@interface CryptoVerifyHandleBuilder : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +/** + * DisableAutomaticTextSanitize indicates that automatic text sanitization should be disabled. +If not disabled, the output will be sanitized if a text signature is present. + */ +- (CryptoVerifyHandleBuilder* _Nullable)disableAutomaticTextSanitize; +/** + * DisableStrictMessageParsing disables the check that the inputs conform +to the OpenPGP message grammar. +If set, no error is thrown if the input message does not conform to the +OpenPGP specification. + */ +- (CryptoVerifyHandleBuilder* _Nullable)disableStrictMessageParsing; +/** + * DisableVerifyTimeCheck disables the check for comparing the signature expiration time +against the verification time. + */ +- (CryptoVerifyHandleBuilder* _Nullable)disableVerifyTimeCheck; +/** + * Error returns any errors that occurred within the builder. + */ +- (BOOL)error:(NSError* _Nullable* _Nullable)error; +/** + * MaxDecompressedMessageSize specifies the maximum allowed size, in bytes, +for a message after decompression within an inline-signed message. +If the decompressed message exceeds this limit, an error is returned. + */ +- (CryptoVerifyHandleBuilder* _Nullable)maxDecompressedMessageSize:(int64_t)size; +/** + * New creates a VerifyHandle and checks that the given +combination of parameters is valid. If the parameters are invalid, +an error is returned. + */ +- (id _Nullable)new:(NSError* _Nullable* _Nullable)error; +/** + * Utf8 indicates if the output plaintext is Utf8 and +should be sanitized from canonicalised line endings. +If enabled for detached verification, it canonicalises the input +before verification independent of the signature type. + */ +- (CryptoVerifyHandleBuilder* _Nullable)utf8; +/** + * VerificationContext sets a verification context for signatures of the pgp message, if any. +Only considered if VerifyKeys are set. + */ +- (CryptoVerifyHandleBuilder* _Nullable)verificationContext:(CryptoVerificationContext* _Nullable)verifyContext; +/** + * VerificationKey sets the public key for verifying the signatures. + */ +- (CryptoVerifyHandleBuilder* _Nullable)verificationKey:(CryptoKey* _Nullable)key; +/** + * VerificationKeys sets the public keys for verifying the signatures. + */ +- (CryptoVerifyHandleBuilder* _Nullable)verificationKeys:(CryptoKeyRing* _Nullable)keys; +/** + * VerifyTime sets the verification time to the provided timestamp. +If not set, the systems current time is used for signature verification. + */ +- (CryptoVerifyHandleBuilder* _Nullable)verifyTime:(int64_t)unixTime; +@end + +/** + * VerifyResult is a result of a pgp message signature verification. + */ +@interface CryptoVerifyResult : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field VerifyResult.Signatures with unsupported type: []*github.com/ProtonMail/gopenpgp/v3/crypto.VerifiedSignature + +/** + * ConstrainToTimeRange updates the signature result to only consider +signatures with a creation time within the given time frame. +unixFrom and unixTo are in unix time and are inclusive. + */ +- (void)constrainToTimeRange:(int64_t)unixFrom unixTo:(int64_t)unixTo; +/** + * Signature returns the serialized openpgp signature packet of the selected signature. + */ +- (NSData* _Nullable)signature:(NSError* _Nullable* _Nullable)error; +/** + * SignatureCreationTime returns the creation time of +the selected verified signature if found, else returns 0. + */ +- (int64_t)signatureCreationTime; +/** + * SignatureError returns nil if no signature err occurred else +the signature error. + */ +- (BOOL)signatureError:(NSError* _Nullable* _Nullable)error; +/** + * SignatureErrorExplicit returns nil if no signature err occurred else +the explicit signature error. + */ +- (CryptoSignatureVerificationError* _Nullable)signatureErrorExplicit; +/** + * SignedByFingerprint returns the key fingerprint of the key that was used to verify the selected signature, +if found, else returns nil. + */ +- (NSData* _Nullable)signedByFingerprint; +/** + * SignedByKey returns the key that was used to verify the selected signature, +if found, else returns nil. + */ +- (CryptoKey* _Nullable)signedByKey; +// skipped method VerifyResult.SignedByKeyId with unsupported parameter or return types + +/** + * SignedByKeyIdHex returns the key id of the key that was used to verify the selected signature +as a hex encoded string. +Helper for go-mobile. + */ +- (NSString* _Nonnull)signedByKeyIdHex; +// skipped method VerifyResult.SignedWithType with unsupported parameter or return types + +/** + * SignedWithTypeInt8 returns the type of the signature as int8 type if found, else returns 0. +See constants.SigType... for the different types. + */ +- (int8_t)signedWithTypeInt8; +@end + +/** + * PGPEncoding determines the message encoding. +The type is int8 for compatibility with gomobile. + */ +FOUNDATION_EXPORT const int8_t CryptoArmor; +/** + * PGPEncoding determines the message encoding. +The type is int8 for compatibility with gomobile. + */ +FOUNDATION_EXPORT const int8_t CryptoAuto; +/** + * PGPEncoding determines the message encoding. +The type is int8 for compatibility with gomobile. + */ +FOUNDATION_EXPORT const int8_t CryptoBytes; +/** + * KeyGenerationCurve25519 allows to override the output key algorithm in key generation to curve25519 (as defined in RFC9580). + */ +FOUNDATION_EXPORT const long CryptoKeyGenerationCurve25519; +/** + * KeyGenerationCurve25519Legacy allows to override the output key algorithm in key generation to curve25519 legacy (as defined in RFC4880bis). + */ +FOUNDATION_EXPORT const long CryptoKeyGenerationCurve25519Legacy; +/** + * KeyGenerationCurve448 allows to override the output key algorithm in key generation to curve448 (as defined in RFC9580). + */ +FOUNDATION_EXPORT const long CryptoKeyGenerationCurve448; +/** + * KeyGenerationRSA4096 allows to override the output key algorithm in key generation to rsa 4096. + */ +FOUNDATION_EXPORT const long CryptoKeyGenerationRSA4096; + +// skipped function FilterExpiredKeys with unsupported parameter or return types + + +/** + * GenerateSessionKeyAlgo generates a random key of the correct length for the +specified algorithm. + */ +FOUNDATION_EXPORT CryptoSessionKey* _Nullable CryptoGenerateSessionKeyAlgo(NSString* _Nullable algo, NSError* _Nullable* _Nullable error); + +/** + * IsPGPMessage checks if data if has armored PGP message format. + */ +FOUNDATION_EXPORT BOOL CryptoIsPGPMessage(NSString* _Nullable data); + +// skipped function NewConstantClock with unsupported parameter or return types + + +/** + * NewFileMetadata creates literal metadata. + */ +FOUNDATION_EXPORT CryptoLiteralMetadata* _Nullable CryptoNewFileMetadata(BOOL isUTF8, NSString* _Nullable filename, int64_t modTime); + +/** + * NewKey creates a new key from the first key in the unarmored or armored binary data. +Clones the binKeys data for go-mobile compatibility. + */ +FOUNDATION_EXPORT CryptoKey* _Nullable CryptoNewKey(NSData* _Nullable binKeys, NSError* _Nullable* _Nullable error); + +/** + * NewKeyFromArmored creates a new key from the first key in an armored string. + */ +FOUNDATION_EXPORT CryptoKey* _Nullable CryptoNewKeyFromArmored(NSString* _Nullable armored, NSError* _Nullable* _Nullable error); + +// skipped function NewKeyFromEntity with unsupported parameter or return types + + +// skipped function NewKeyFromReader with unsupported parameter or return types + + +// skipped function NewKeyFromReaderExplicit with unsupported parameter or return types + + +/** + * NewKeyRing creates a new KeyRing, empty if key is nil. + */ +FOUNDATION_EXPORT CryptoKeyRing* _Nullable CryptoNewKeyRing(CryptoKey* _Nullable key, NSError* _Nullable* _Nullable error); + +/** + * NewKeyRingFromBinary creates a new keyring with all the keys contained in the unarmored binary data. +Note that it accepts only unlocked or public keys, as KeyRing cannot contain locked keys. + */ +FOUNDATION_EXPORT CryptoKeyRing* _Nullable CryptoNewKeyRingFromBinary(NSData* _Nullable binKeys, NSError* _Nullable* _Nullable error); + +/** + * NewKeyWithCloneFlag creates a new key from the first key in the unarmored or armored binary data. + */ +FOUNDATION_EXPORT CryptoKey* _Nullable CryptoNewKeyWithCloneFlag(NSData* _Nullable binKeys, BOOL clone, NSError* _Nullable* _Nullable error); + +/** + * NewMetadata creates new default literal metadata with utf-8 set to isUTF8. + */ +FOUNDATION_EXPORT CryptoLiteralMetadata* _Nullable CryptoNewMetadata(BOOL isUTF8); + +/** + * NewPGPMessage generates a new PGPMessage from the unarmored binary data. +Clones the data for go-mobile compatibility. + */ +FOUNDATION_EXPORT CryptoPGPMessage* _Nullable CryptoNewPGPMessage(NSData* _Nullable data); + +/** + * NewPGPMessageBuffer creates a message buffer. + */ +FOUNDATION_EXPORT CryptoPGPMessageBuffer* _Nullable CryptoNewPGPMessageBuffer(void); + +/** + * NewPGPMessageFromArmored generates a new PGPMessage from an armored string ready for decryption. + */ +FOUNDATION_EXPORT CryptoPGPMessage* _Nullable CryptoNewPGPMessageFromArmored(NSString* _Nullable armored, NSError* _Nullable* _Nullable error); + +/** + * NewPGPMessageWithCloneFlag generates a new PGPMessage from the unarmored binary data. + */ +FOUNDATION_EXPORT CryptoPGPMessage* _Nullable CryptoNewPGPMessageWithCloneFlag(NSData* _Nullable data, BOOL doClone); + +/** + * NewPGPSplitMessage generates a new PGPSplitMessage from the binary unarmored keypacket and datapacket. +Clones the slices for go-mobile compatibility. + */ +FOUNDATION_EXPORT CryptoPGPMessage* _Nullable CryptoNewPGPSplitMessage(NSData* _Nullable keyPacket, NSData* _Nullable dataPacket); + +/** + * NewPGPSplitWriter creates a type that implements the PGPSplitWriter interface +for encrypting a plaintext where the output PGP packets should be written to the different streams provided. +Key packets are written to keyPackets whereas the encrypted data packets are written to encPackets. +The encrypted detached signature data is written to encSigPacket. + */ +FOUNDATION_EXPORT id _Nullable CryptoNewPGPSplitWriter(id _Nullable keyPackets, id _Nullable encPackets, id _Nullable encSigPacket); + +/** + * NewPGPSplitWriterDetachedSignature creates a type that implements the PGPSplitWriter interface +for encrypting a plaintext where the output PGP messages should be written to the different streams provided. +The encrypted data message is written to encMessage whereas the encrypted detached signature is written to +encSigMessage. + */ +FOUNDATION_EXPORT id _Nullable CryptoNewPGPSplitWriterDetachedSignature(id _Nullable encMessage, id _Nullable encSigMessage); + +/** + * NewPGPSplitWriterFromWriter creates a type that implements the PGPSplitWriter interface +for encrypting a plaintext where the output PGP messages to the provided Writer. + */ +FOUNDATION_EXPORT id _Nullable CryptoNewPGPSplitWriterFromWriter(id _Nullable writer); + +/** + * NewPGPSplitWriterKeyAndData creates a type that implements the PGPSplitWriter interface +for encrypting a plaintext where the output PGP packets should be written to the different streams provided. +Key packets are written to keyPackets whereas the encrypted data packets are written to encPackets. + */ +FOUNDATION_EXPORT id _Nullable CryptoNewPGPSplitWriterKeyAndData(id _Nullable keyPackets, id _Nullable encPackets); + +/** + * NewPrivateKeyFromArmored creates a new secret key from the first key in an armored string +and unlocks it with the password. + */ +FOUNDATION_EXPORT CryptoKey* _Nullable CryptoNewPrivateKeyFromArmored(NSString* _Nullable armored, NSData* _Nullable password, NSError* _Nullable* _Nullable error); + +/** + * NewSessionKeyFromToken creates a SessionKey struct with the given token and algorithm. +Clones the token for compatibility with go-mobile. + */ +FOUNDATION_EXPORT CryptoSessionKey* _Nullable CryptoNewSessionKeyFromToken(NSData* _Nullable token, NSString* _Nullable algo); + +/** + * NewSessionKeyFromTokenWithAead creates a SessionKey struct with the given token and algorithm. +If aead is set to true, the key is used with v6 PKESK or SKESK, and SEIPDv2 packets. + */ +FOUNDATION_EXPORT CryptoSessionKey* _Nullable CryptoNewSessionKeyFromTokenWithAead(NSData* _Nullable token, NSString* _Nullable algo, BOOL aead); + +/** + * NewSigningContext creates a new signing context. +The value is set to the notation data. +isCritical controls whether the notation is flagged as a critical packet. + */ +FOUNDATION_EXPORT CryptoSigningContext* _Nullable CryptoNewSigningContext(NSString* _Nullable value, BOOL isCritical); + +/** + * NewVerificationContext creates a new verification context. +The value is checked against the signature's notation data. +If isRequired is false, the signature is allowed to have no context set. +If requiredAfter is != 0, the signature is allowed to have no context set if it +was created before the unix time set in requiredAfter. + */ +FOUNDATION_EXPORT CryptoVerificationContext* _Nullable CryptoNewVerificationContext(NSString* _Nullable value, BOOL isRequired, int64_t requiredAfter); + +/** + * PGP creates a PGPHandle to interact with the API. +Uses the default profile for configuration. + */ +FOUNDATION_EXPORT CryptoPGPHandle* _Nullable CryptoPGP(void); + +/** + * PGPWithProfile creates a PGPHandle to interact with the API. +Uses the provided profile for configuration. + */ +FOUNDATION_EXPORT CryptoPGPHandle* _Nullable CryptoPGPWithProfile(ProfileCustom* _Nullable profile); + +/** + * RandomToken generates a random token with the specified key size. + */ +FOUNDATION_EXPORT NSData* _Nullable CryptoRandomToken(long size, NSError* _Nullable* _Nullable error); + +// skipped function SignatureHexKeyIDs with unsupported parameter or return types + + +// skipped function SignatureKeyIDs with unsupported parameter or return types + + +// skipped function ZeroClock with unsupported parameter or return types + + +@class CryptoEncryptionProfile; + +@class CryptoKeyEncryptionProfile; + +@class CryptoKeyGenerationProfile; + +@class CryptoPGPDecryption; + +@class CryptoPGPEncryption; + +@class CryptoPGPKeyGeneration; + +@class CryptoPGPSign; + +@class CryptoPGPSplitReader; + +@class CryptoPGPSplitWriter; + +@class CryptoPGPVerify; + +@class CryptoReader; + +@class CryptoSignProfile; + +@class CryptoWriteCloser; + +@class CryptoWriter; + +@interface CryptoEncryptionProfile : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +// skipped method EncryptionProfile.CompressionConfig with unsupported parameter or return types + +// skipped method EncryptionProfile.EncryptionConfig with unsupported parameter or return types + +@end + +@interface CryptoKeyEncryptionProfile : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +// skipped method KeyEncryptionProfile.KeyEncryptionConfig with unsupported parameter or return types + +@end + +@interface CryptoKeyGenerationProfile : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +// skipped method KeyGenerationProfile.KeyGenerationConfig with unsupported parameter or return types + +@end + +/** + * PGPDecryption is an interface for decrypting pgp messages with GopenPGP. +Use the DecryptionHandleBuilder to create a handle that implements PGPDecryption. + */ +@interface CryptoPGPDecryption : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * ClearPrivateParams clears all private key material contained in EncryptionHandle from memory. + */ +- (void)clearPrivateParams; +/** + * Decrypt decrypts an encrypted pgp message. +Returns a VerifiedDataResult, which can be queried for potential signature verification errors, +and the plaintext data. Note that on a signature error, the method does not return an error. +Instead, the signature error is stored within the VerifiedDataResult. +The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto +where Auto tries to detect automatically. + */ +- (CryptoVerifiedDataResult* _Nullable)decrypt:(NSData* _Nullable)pgpMessage encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * DecryptDetached provides the same functionality as Decrypt but allows +to supply an encrypted detached signature that should be decrypted and verified +against the data in the pgp message. If encDetachedSignature is nil, the behavior is similar +to Decrypt. The encoding indicates if the input message should be unarmored or not, +i.e., Bytes/Armor/Auto where Auto tries to detect automatically. + */ +- (CryptoVerifiedDataResult* _Nullable)decryptDetached:(NSData* _Nullable)pgpMessage encDetachedSignature:(NSData* _Nullable)encDetachedSignature encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * DecryptSessionKey decrypts an encrypted session key. +To decrypt a session key, the decryption handle must contain either a decryption key or a password. + */ +- (CryptoSessionKey* _Nullable)decryptSessionKey:(NSData* _Nullable)keyPackets error:(NSError* _Nullable* _Nullable)error; +/** + * DecryptingReader returns a wrapper around underlying encryptedMessage Reader, +such that any read-operation via the wrapper results in a read from the decrypted pgp message. +The returned VerifyDataReader has to be fully read before any potential signatures can be verified. +Either read the message fully end then call VerifySignature or use the helper method ReadAllAndVerifySignature. +The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto +where Auto tries to detect automatically. +If encryptedMessage is of type PGPSplitReader, the method tries to verify an encrypted detached signature +that is read from the separate reader. + */ +- (CryptoVerifyDataReader* _Nullable)decryptingReader:(id _Nullable)encryptedMessage encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * PGPEncryption is an interface for encrypting messages with GopenPGP. +Use an EncryptionHandleBuilder to create a PGPEncryption handle. + */ +@interface CryptoPGPEncryption : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * ClearPrivateParams clears all private key material contained in EncryptionHandle from memory. + */ +- (void)clearPrivateParams; +/** + * Encrypt encrypts a plaintext message. + */ +- (CryptoPGPMessage* _Nullable)encrypt:(NSData* _Nullable)message error:(NSError* _Nullable* _Nullable)error; +/** + * EncryptSessionKey encrypts a session key with the encryption handle. +To encrypt a session key, the handle must contain either recipients or a password. + */ +- (NSData* _Nullable)encryptSessionKey:(CryptoSessionKey* _Nullable)sessionKey error:(NSError* _Nullable* _Nullable)error; +/** + * EncryptingWriter returns a wrapper around underlying output Writer, +such that any write-operation via the wrapper results in a write to an encrypted pgp message. +If the output Writer is of type PGPSplitWriter, the output can be split to multiple writers +for different parts of the message. For example to write key packets and encrypted data packets +to different writers or to write a detached signature separately. +The encoding argument defines the output encoding, i.e., Bytes or Armored +The returned pgp message WriteCloser must be closed after the plaintext has been written. + */ +- (id _Nullable)encryptingWriter:(id _Nullable)output encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * GenerateSessionKey generates a random session key for the given encryption handle +considering the algorithm preferences of the recipient keys. + */ +- (CryptoSessionKey* _Nullable)generateSessionKey:(NSError* _Nullable* _Nullable)error; +@end + +/** + * PGPKeyGeneration is an interface for generating pgp keys with GopenPGP. +Use the KeyGenerationBuilder to create a handle that implements PGPKeyGeneration. + */ +@interface CryptoPGPKeyGeneration : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * GenerateKey generates a pgp key with the standard security level. + */ +- (CryptoKey* _Nullable)generateKey:(NSError* _Nullable* _Nullable)error; +/** + * GenerateKeyWithSecurity generates a pgp key with the given security level. +The argument security allows to set the security level, either standard or high. + */ +- (CryptoKey* _Nullable)generateKeyWithSecurity:(int8_t)securityLevel error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * PGPSign is an interface for creating signature messages with GopenPGP. + */ +@interface CryptoPGPSign : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * ClearPrivateParams clears all secret key material contained in the PGPSign from memory. + */ +- (void)clearPrivateParams; +/** + * Sign creates a detached or inline signature from the provided byte slice. +The encoding argument defines the output encoding, i.e., Bytes or Armored + */ +- (NSData* _Nullable)sign:(NSData* _Nullable)message encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * SignCleartext produces an armored cleartext message according to the specification. +Returns an armored message even if the PGPSign is not configured for armored output. + */ +- (NSData* _Nullable)signCleartext:(NSData* _Nullable)message error:(NSError* _Nullable* _Nullable)error; +/** + * SigningWriter returns a wrapper around underlying output Writer, +such that any write-operation via the wrapper results in a write to a detached or inline signature message. +The encoding argument defines the output encoding, i.e., Bytes or Armored +Once close is called on the returned WriteCloser the final signature is written to the output. +Thus, the returned WriteCloser must be closed after the plaintext has been written. + */ +- (id _Nullable)signingWriter:(id _Nullable)output encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +@end + +@interface CryptoPGPSplitReader : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (BOOL)read:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +- (id _Nullable)signature; +@end + +/** + * PGPSplitWriter is an interface to write different parts of a PGP message +(i.e., packets) to different streams. + */ +@interface CryptoPGPSplitWriter : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * Keys returns the Writer to which the key packets are written to. + */ +- (id _Nullable)keys; +/** + * Signature returns the Writer to which an encrypted detached signature is written to. + */ +- (id _Nullable)signature; +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * PGPVerify is an interface for verifying detached signatures with GopenPGP. + */ +@interface CryptoPGPVerify : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * VerifyCleartext verifies an armored cleartext message +and returns a VerifyCleartextResult. The VerifyCleartextResult can be checked for failure +and allows access the contained message +Note that an error is only returned if it is not a signature error. + */ +- (CryptoVerifyCleartextResult* _Nullable)verifyCleartext:(NSData* _Nullable)cleartext error:(NSError* _Nullable* _Nullable)error; +/** + * VerifyDetached verifies a detached signature pgp message +and returns a VerifyResult. The VerifyResult can be checked for failure +and allows access to information about the signatures. +Note that an error is only returned if it is not a signature error. +The encoding indicates if the input signature message should be unarmored or not, +i.e., Bytes/Armor/Auto where Auto tries to detect it automatically. + */ +- (CryptoVerifyResult* _Nullable)verifyDetached:(NSData* _Nullable)data signature:(NSData* _Nullable)signature encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * VerifyInline verifies an inline signed pgp message +and returns a VerifiedDataResult. The VerifiedDataResult can be checked for failure, +allows access to information about the signatures, and includes the plain message. +Note that an error is only returned if it is not a signature error. +The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto +where Auto tries to detect it automatically. + */ +- (CryptoVerifiedDataResult* _Nullable)verifyInline:(NSData* _Nullable)message encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * VerifyingReader wraps a reader with a signature verify reader. +Once all data is read from the returned verify reader, the signature can be verified +with (VerifyDataReader).VerifySignature(). +Note that an error is only returned if it is not a signature error. +The encoding indicates if the input signature message should be unarmored or not, +i.e., Bytes/Armor/Auto where Auto tries to detect it automatically. +If detachedData is nil, signatureMessage is treated as an inline signature message. +Thus, it is expected that signatureMessage contains the data to be verified. +If detachedData is not nil, signatureMessage must contain a detached signature, +which is verified against the detachedData. + */ +- (CryptoVerifyDataReader* _Nullable)verifyingReader:(id _Nullable)detachedData signatureMessage:(id _Nullable)signatureMessage encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +@end + +@interface CryptoReader : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (BOOL)read:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +@interface CryptoSignProfile : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +// skipped method SignProfile.SignConfig with unsupported parameter or return types + +@end + +/** + * WriteCloser replicates the io.WriteCloser interface for go-mobile. + */ +@interface CryptoWriteCloser : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (BOOL)close:(NSError* _Nullable* _Nullable)error; +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * Writer replicates the io.Writer interface for go-mobile. + */ +@interface CryptoWriter : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Gopenpgp.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Gopenpgp.h new file mode 100644 index 00000000..ee169cd5 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Gopenpgp.h @@ -0,0 +1,23 @@ + +// Objective-C API for talking to the following Go packages +// +// github.com/ProtonMail/gopenpgp/v3/crypto +// github.com/ProtonMail/gopenpgp/v3/armor +// github.com/ProtonMail/gopenpgp/v3/constants +// github.com/ProtonMail/gopenpgp/v3/mime +// github.com/ProtonMail/gopenpgp/v3/mobile +// github.com/ProtonMail/gopenpgp/v3/profile +// +// File is generated by gomobile bind. Do not edit. +#ifndef __Gopenpgp_FRAMEWORK_H__ +#define __Gopenpgp_FRAMEWORK_H__ + +#include "Crypto.objc.h" +#include "Armor.objc.h" +#include "Constants.objc.h" +#include "Mime.objc.h" +#include "Mobile.objc.h" +#include "Profile.objc.h" +#include "Universe.objc.h" + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mime.objc.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mime.objc.h new file mode 100644 index 00000000..ad2b2d51 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mime.objc.h @@ -0,0 +1,59 @@ +// Objective-C API for talking to github.com/ProtonMail/gopenpgp/v3/mime Go package. +// gobind -lang=objc github.com/ProtonMail/gopenpgp/v3/mime +// +// File is generated by gobind. Do not edit. + +#ifndef __Mime_H__ +#define __Mime_H__ + +@import Foundation; +#include "ref.h" +#include "Universe.objc.h" + +#include "Constants.objc.h" +#include "Crypto.objc.h" + +@protocol MimeMIMECallbacks; +@class MimeMIMECallbacks; + +@protocol MimeMIMECallbacks +- (void)onAttachment:(NSString* _Nullable)headers data:(NSData* _Nullable)data; +- (void)onBody:(NSString* _Nullable)body mimetype:(NSString* _Nullable)mimetype; +/** + * Encrypted headers can be in an attachment and thus be placed at the end of the mime structure. + */ +- (void)onEncryptedHeaders:(NSString* _Nullable)headers; +- (void)onError:(NSError* _Nullable)err; +- (void)onVerified:(long)verified; +@end + +/** + * Decrypt decrypts and verifies a MIME message. +messageEncoding provides the encoding of the encrypted MIME message, either crypto.Bytes or crypto.Armor. +The decryptionHandle is used to decrypt and verify the message, while +the verifyHandle is used to verify the signature contained in the decrypted mime message. +The verifyHandle can be nil. + */ +FOUNDATION_EXPORT void MimeDecrypt(NSData* _Nullable message, int8_t messageEncoding, id _Nullable decryptionHandle, id _Nullable verifyHandle, id _Nullable callbacks); + +@class MimeMIMECallbacks; + +/** + * MIMECallbacks defines callback methods to process a MIME message. + */ +@interface MimeMIMECallbacks : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (void)onAttachment:(NSString* _Nullable)headers data:(NSData* _Nullable)data; +- (void)onBody:(NSString* _Nullable)body mimetype:(NSString* _Nullable)mimetype; +/** + * Encrypted headers can be in an attachment and thus be placed at the end of the mime structure. + */ +- (void)onEncryptedHeaders:(NSString* _Nullable)headers; +- (void)onError:(NSError* _Nullable)err; +- (void)onVerified:(long)verified; +@end + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mobile.objc.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mobile.objc.h new file mode 100644 index 00000000..95663f82 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Mobile.objc.h @@ -0,0 +1,252 @@ +// Objective-C API for talking to github.com/ProtonMail/gopenpgp/v3/mobile Go package. +// gobind -lang=objc github.com/ProtonMail/gopenpgp/v3/mobile +// +// File is generated by gobind. Do not edit. + +#ifndef __Mobile_H__ +#define __Mobile_H__ + +@import Foundation; +#include "ref.h" +#include "Universe.objc.h" + +#include "Crypto.objc.h" + +@class MobileDetachedSignaturePGPSplitReader; +@class MobileGo2AndroidReader; +@class MobileGo2IOSReader; +@class MobileKeyPacketSplitWriter; +@class MobileMobile2GoReader; +@class MobileMobile2GoWriter; +@class MobileMobile2GoWriterWithSHA256; +@class MobileMobileReadResult; +@protocol MobileMobileReader; +@class MobileMobileReader; + +@protocol MobileMobileReader +- (MobileMobileReadResult* _Nullable)read:(long)max error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * DetachedSignaturePGPSplitReader implements the crypto.PGPSplitReader interface. + */ +@interface MobileDetachedSignaturePGPSplitReader : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nullable instancetype)init:(NSData* _Nullable)keyPacket dataReader:(id _Nullable)dataReader encSignature:(CryptoPGPMessage* _Nullable)encSignature; +- (BOOL)read:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +- (id _Nullable)signature; +@end + +/** + * Go2AndroidReader is used to wrap a native golang Reader in the golang runtime, +to be usable in the android app runtime (via gomobile). + */ +@interface MobileGo2AndroidReader : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewGo2AndroidReader wraps a native golang Reader to be usable in the mobile app runtime (via gomobile). +It doesn't follow the standard golang Reader behavior, and returns n = -1 on EOF. + */ +- (nullable instancetype)init:(id _Nullable)reader; +/** + * Read reads bytes into the provided buffer and returns the number of bytes read +It doesn't follow the standard golang Reader behavior, and returns n = -1 on EOF. + */ +- (BOOL)read:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * Go2IOSReader is used to wrap a native golang Reader in the golang runtime, +to be usable in the iOS app runtime (via gomobile) as a MobileReader. + */ +@interface MobileGo2IOSReader : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewGo2IOSReader wraps a native golang Reader to be usable in the ios app runtime (via gomobile). + */ +- (nullable instancetype)init:(id _Nullable)reader; +/** + * Read reads at most bytes from the wrapped Reader and returns the read data as a MobileReadResult. + */ +- (MobileMobileReadResult* _Nullable)read:(long)max error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * KeyPacketSplitWriter implements the crypto.PGPSplitWriter interface +for splitting encryptions output into different packets. +Internally buffers the key packets and potential detached encrypted signatures. + */ +@interface MobileKeyPacketSplitWriter : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nullable instancetype)init:(id _Nullable)dataWriter; +/** + * EncryptedDetachedSignature returns the internally buffered encrypted detached signature. + */ +- (CryptoPGPMessage* _Nullable)encryptedDetachedSignature; +/** + * KeyPackets returns the internally buffered key packets. + */ +- (NSData* _Nullable)keyPackets; +- (id _Nullable)keys; +- (id _Nullable)signature; +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * Mobile2GoReader is used to wrap a MobileReader in the mobile app runtime, +to be usable in the golang runtime (via gomobile) as a native Reader. + */ +@interface MobileMobile2GoReader : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewMobile2GoReader wraps a MobileReader to be usable in the golang runtime (via gomobile). + */ +- (nullable instancetype)init:(id _Nullable)reader; +/** + * Read reads data from the wrapped MobileReader and copies the read data in the provided buffer. +It also handles the conversion of EOF to an error. + */ +- (BOOL)read:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * Mobile2GoWriter is used to wrap a writer in the mobile app runtime, +to be usable in the golang runtime (via gomobile). + */ +@interface MobileMobile2GoWriter : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewMobile2GoWriter wraps a writer to be usable in the golang runtime (via gomobile). + */ +- (nullable instancetype)init:(id _Nullable)writer; +/** + * Write writes the data in the provided buffer in the wrapped writer. +It clones the provided data to prevent errors with garbage collectors. + */ +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * Mobile2GoWriterWithSHA256 is used to wrap a writer in the mobile app runtime, +to be usable in the golang runtime (via gomobile). +It also computes the SHA256 hash of the data being written on the fly. + */ +@interface MobileMobile2GoWriterWithSHA256 : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewMobile2GoWriterWithSHA256 wraps a writer to be usable in the golang runtime (via gomobile). +The wrapper also computes the SHA256 hash of the data being written on the fly. + */ +- (nullable instancetype)init:(id _Nullable)writer; +/** + * GetSHA256 returns the SHA256 hash of the data that's been written so far. + */ +- (NSData* _Nullable)getSHA256; +/** + * Write writes the data in the provided buffer in the wrapped writer. +It clones the provided data to prevent errors with garbage collectors. +It also computes the SHA256 hash of the data being written on the fly. + */ +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * MobileReadResult is what needs to be returned by MobileReader.Read. +The read data is passed as a return value rather than passed as an argument to the reader. +This avoids problems introduced by gomobile that prevent the use of native golang readers. + */ +@interface MobileMobileReadResult : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewMobileReadResult initialize a MobileReadResult with the correct values. +It clones the data to avoid the garbage collector freeing the data too early. + */ +- (nullable instancetype)init:(long)n eof:(BOOL)eof data:(NSData* _Nullable)data; +@property (nonatomic) long n; +@property (nonatomic) BOOL isEOF; +@property (nonatomic) NSData* _Nullable data; +@end + +/** + * FreeOSMemory can be used to explicitly +call the garbage collector and +return the unused memory to the OS. + */ +FOUNDATION_EXPORT void MobileFreeOSMemory(void); + +FOUNDATION_EXPORT MobileDetachedSignaturePGPSplitReader* _Nullable MobileNewDetachedSignaturePGPSplitReader(NSData* _Nullable keyPacket, id _Nullable dataReader, CryptoPGPMessage* _Nullable encSignature); + +/** + * NewGo2AndroidReader wraps a native golang Reader to be usable in the mobile app runtime (via gomobile). +It doesn't follow the standard golang Reader behavior, and returns n = -1 on EOF. + */ +FOUNDATION_EXPORT MobileGo2AndroidReader* _Nullable MobileNewGo2AndroidReader(id _Nullable reader); + +/** + * NewGo2IOSReader wraps a native golang Reader to be usable in the ios app runtime (via gomobile). + */ +FOUNDATION_EXPORT MobileGo2IOSReader* _Nullable MobileNewGo2IOSReader(id _Nullable reader); + +FOUNDATION_EXPORT MobileKeyPacketSplitWriter* _Nullable MobileNewKeyPacketSplitWriter(id _Nullable dataWriter); + +/** + * NewMobile2GoReader wraps a MobileReader to be usable in the golang runtime (via gomobile). + */ +FOUNDATION_EXPORT MobileMobile2GoReader* _Nullable MobileNewMobile2GoReader(id _Nullable reader); + +/** + * NewMobile2GoWriter wraps a writer to be usable in the golang runtime (via gomobile). + */ +FOUNDATION_EXPORT MobileMobile2GoWriter* _Nullable MobileNewMobile2GoWriter(id _Nullable writer); + +/** + * NewMobile2GoWriterWithSHA256 wraps a writer to be usable in the golang runtime (via gomobile). +The wrapper also computes the SHA256 hash of the data being written on the fly. + */ +FOUNDATION_EXPORT MobileMobile2GoWriterWithSHA256* _Nullable MobileNewMobile2GoWriterWithSHA256(id _Nullable writer); + +/** + * NewMobileReadResult initialize a MobileReadResult with the correct values. +It clones the data to avoid the garbage collector freeing the data too early. + */ +FOUNDATION_EXPORT MobileMobileReadResult* _Nullable MobileNewMobileReadResult(long n, BOOL eof, NSData* _Nullable data); + +@class MobileMobileReader; + +/** + * MobileReader is the interface that readers in the mobile runtime must use and implement. +This is a workaround to some of the gomobile limitations. + */ +@interface MobileMobileReader : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (MobileMobileReadResult* _Nullable)read:(long)max error:(NSError* _Nullable* _Nullable)error; +@end + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Profile.objc.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Profile.objc.h new file mode 100644 index 00000000..805075a7 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Profile.objc.h @@ -0,0 +1,107 @@ +// Objective-C API for talking to github.com/ProtonMail/gopenpgp/v3/profile Go package. +// gobind -lang=objc github.com/ProtonMail/gopenpgp/v3/profile +// +// File is generated by gobind. Do not edit. + +#ifndef __Profile_H__ +#define __Profile_H__ + +@import Foundation; +#include "ref.h" +#include "Universe.objc.h" + +#include "Constants.objc.h" + +@class ProfileCustom; + +/** + * Custom type represents a profile for setting algorithm +parameters for generating keys, encrypting data, and +signing data. +Use one of the pre-defined profiles if possible. +i.e., profile.Default(), profile.RFC4880(). + */ +@interface ProfileCustom : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field Custom.SetKeyAlgorithm with unsupported type: func(*github.com/ProtonMail/go-crypto/openpgp/packet.Config, int8) + +// skipped field Custom.AeadKeyEncryption with unsupported type: *github.com/ProtonMail/go-crypto/openpgp/packet.AEADConfig + +// skipped field Custom.S2kKeyEncryption with unsupported type: *github.com/ProtonMail/go-crypto/openpgp/s2k.Config + +// skipped field Custom.AeadEncryption with unsupported type: *github.com/ProtonMail/go-crypto/openpgp/packet.AEADConfig + +// skipped field Custom.S2kEncryption with unsupported type: *github.com/ProtonMail/go-crypto/openpgp/s2k.Config + +// skipped field Custom.CompressionConfiguration with unsupported type: *github.com/ProtonMail/go-crypto/openpgp/packet.CompressionConfig + +// skipped field Custom.Hash with unsupported type: crypto.Hash + +// skipped field Custom.SignHash with unsupported type: *crypto.Hash + +// skipped field Custom.CipherKeyEncryption with unsupported type: github.com/ProtonMail/go-crypto/openpgp/packet.CipherFunction + +// skipped field Custom.CipherEncryption with unsupported type: github.com/ProtonMail/go-crypto/openpgp/packet.CipherFunction + +// skipped field Custom.CompressionAlgorithm with unsupported type: github.com/ProtonMail/go-crypto/openpgp/packet.CompressionAlgo + +/** + * V6 is a flag to indicate if v6 from the crypto-refresh should be used. + */ +@property (nonatomic) BOOL v6; +/** + * AllowAllPublicKeyAlgorithms is a flag to disable all checks for deprecated public key algorithms. + */ +@property (nonatomic) BOOL allowAllPublicKeyAlgorithms; +/** + * DisableIntendedRecipients is a flag to disable the intended recipients pgp feature from the crypto-refresh. + */ +@property (nonatomic) BOOL disableIntendedRecipients; +/** + * InsecureAllowWeakRSA is a flag to disable checks for weak rsa keys. + */ +@property (nonatomic) BOOL insecureAllowWeakRSA; +/** + * InsecureAllowDecryptionWithSigningKeys is a flag to enable to decrypt with signing keys for compatibility reasons. + */ +@property (nonatomic) BOOL insecureAllowDecryptionWithSigningKeys; +/** + * MaxDecompressedMessageSize sets the maximum decompressed messages size that can be read +before throwing an error. + */ +@property (nonatomic) int64_t maxDecompressedMessageSize; +// skipped method Custom.CompressionConfig with unsupported parameter or return types + +// skipped method Custom.EncryptionConfig with unsupported parameter or return types + +// skipped method Custom.KeyEncryptionConfig with unsupported parameter or return types + +// skipped method Custom.KeyGenerationConfig with unsupported parameter or return types + +// skipped method Custom.SignConfig with unsupported parameter or return types + +@end + +/** + * Default returns a custom profile that support features +that are widely implemented. + */ +FOUNDATION_EXPORT ProfileCustom* _Nullable ProfileDefault(void); + +/** + * RFC4880 returns a custom profile for this library +that conforms with the algorithms in RFC4880. + */ +FOUNDATION_EXPORT ProfileCustom* _Nullable ProfileRFC4880(void); + +/** + * RFC9580 returns a custom profile for this library +that conforms with the algorithms in RFC9580. + */ +FOUNDATION_EXPORT ProfileCustom* _Nullable ProfileRFC9580(void); + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Universe.objc.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Universe.objc.h new file mode 100644 index 00000000..019e7502 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/Universe.objc.h @@ -0,0 +1,29 @@ +// Objective-C API for talking to Go package. +// gobind -lang=objc +// +// File is generated by gobind. Do not edit. + +#ifndef __Universe_H__ +#define __Universe_H__ + +@import Foundation; +#include "ref.h" + +@protocol Universeerror; +@class Universeerror; + +@protocol Universeerror +- (NSString* _Nonnull)error; +@end + +@class Universeerror; + +@interface Universeerror : NSError { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (NSString* _Nonnull)error; +@end + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/ref.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/ref.h new file mode 100644 index 00000000..b8036a4d --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Headers/ref.h @@ -0,0 +1,35 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef __GO_REF_HDR__ +#define __GO_REF_HDR__ + +#include + +// GoSeqRef is an object tagged with an integer for passing back and +// forth across the language boundary. A GoSeqRef may represent either +// an instance of a Go object, or an Objective-C object passed to Go. +// The explicit allocation of a GoSeqRef is used to pin a Go object +// when it is passed to Objective-C. The Go seq package maintains a +// reference to the Go object in a map keyed by the refnum along with +// a reference count. When the reference count reaches zero, the Go +// seq package will clear the corresponding entry in the map. +@interface GoSeqRef : NSObject { +} +@property(readonly) int32_t refnum; +@property(strong) id obj; // NULL when representing a Go object. + +// new GoSeqRef object to proxy a Go object. The refnum must be +// provided from Go side. +- (instancetype)initWithRefnum:(int32_t)refnum obj:(id)obj; + +- (int32_t)incNum; + +@end + +@protocol goSeqRefInterface +-(GoSeqRef*) _ref; +@end + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Info.plist b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Info.plist new file mode 100644 index 00000000..c0de7a0c --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Info.plist @@ -0,0 +1,20 @@ + + + + + CFBundleExecutable + Gopenpgp + CFBundleIdentifier + Gopenpgp + CFBundlePackageType + FMWK + CFBundleShortVersionString + 0.0.1758379400 + CFBundleVersion + 0.0.1758379400 + MinimumOSVersion + 15.5 + RCTNewArchEnabled + + + diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Modules/module.modulemap b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Modules/module.modulemap new file mode 100644 index 00000000..e7900014 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64/Gopenpgp.framework/Modules/module.modulemap @@ -0,0 +1,13 @@ +framework module "Gopenpgp" { + header "ref.h" + header "Crypto.objc.h" + header "Armor.objc.h" + header "Constants.objc.h" + header "Mime.objc.h" + header "Mobile.objc.h" + header "Profile.objc.h" + header "Universe.objc.h" + header "Gopenpgp.h" + + export * +} \ No newline at end of file diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Gopenpgp b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Gopenpgp new file mode 100644 index 00000000..8cc3a656 Binary files /dev/null and b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Gopenpgp differ diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Armor.objc.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Armor.objc.h new file mode 100644 index 00000000..3001f182 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Armor.objc.h @@ -0,0 +1,96 @@ +// Objective-C API for talking to github.com/ProtonMail/gopenpgp/v3/armor Go package. +// gobind -lang=objc github.com/ProtonMail/gopenpgp/v3/armor +// +// File is generated by gobind. Do not edit. + +#ifndef __Armor_H__ +#define __Armor_H__ + +@import Foundation; +#include "ref.h" +#include "Universe.objc.h" + +#include "Constants.objc.h" + +/** + * ArmorKey armors input as a public key. + */ +FOUNDATION_EXPORT NSString* _Nonnull ArmorArmorKey(NSData* _Nullable input, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT NSString* _Nonnull ArmorArmorPGPMessage(NSData* _Nullable signature, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT NSData* _Nullable ArmorArmorPGPMessageBytes(NSData* _Nullable signature, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT NSData* _Nullable ArmorArmorPGPMessageBytesChecksum(NSData* _Nullable signature, BOOL checksum, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT NSString* _Nonnull ArmorArmorPGPMessageChecksum(NSData* _Nullable signature, BOOL checksum, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT NSString* _Nonnull ArmorArmorPGPSignature(NSData* _Nullable signature, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT NSData* _Nullable ArmorArmorPGPSignatureBinary(NSData* _Nullable signature, NSError* _Nullable* _Nullable error); + +// skipped function ArmorReader with unsupported parameter or return types + + +/** + * ArmorWithType armors input with the given armorType. + */ +FOUNDATION_EXPORT NSString* _Nonnull ArmorArmorWithType(NSData* _Nullable input, NSString* _Nullable armorType, NSError* _Nullable* _Nullable error); + +/** + * ArmorWithTypeAndCustomHeaders armors input with the given armorType and +headers. + */ +FOUNDATION_EXPORT NSString* _Nonnull ArmorArmorWithTypeAndCustomHeaders(NSData* _Nullable input, NSString* _Nullable armorType, NSString* _Nullable version, NSString* _Nullable comment, NSError* _Nullable* _Nullable error); + +/** + * ArmorWithTypeAndCustomHeadersBytes armors input with the given armorType and +headers. + */ +FOUNDATION_EXPORT NSData* _Nullable ArmorArmorWithTypeAndCustomHeadersBytes(NSData* _Nullable input, NSString* _Nullable armorType, NSString* _Nullable version, NSString* _Nullable comment, NSError* _Nullable* _Nullable error); + +/** + * ArmorWithTypeAndCustomHeadersChecksum armors input with the given armorType and +headers and checksum option. + */ +FOUNDATION_EXPORT NSString* _Nonnull ArmorArmorWithTypeAndCustomHeadersChecksum(NSData* _Nullable input, NSString* _Nullable armorType, NSString* _Nullable version, NSString* _Nullable comment, BOOL checksum, NSError* _Nullable* _Nullable error); + +/** + * ArmorWithTypeBytes armors input with the given armorType. + */ +FOUNDATION_EXPORT NSData* _Nullable ArmorArmorWithTypeBytes(NSData* _Nullable input, NSString* _Nullable armorType, NSError* _Nullable* _Nullable error); + +/** + * ArmorWithTypeBytesChecksum armors input with the given armorType and checksum option. + */ +FOUNDATION_EXPORT NSData* _Nullable ArmorArmorWithTypeBytesChecksum(NSData* _Nullable input, NSString* _Nullable armorType, BOOL checksum, NSError* _Nullable* _Nullable error); + +/** + * ArmorWithTypeChecksum armors input with the given armorType. +The checksum option determines if an armor checksum is written at the end. + */ +FOUNDATION_EXPORT NSString* _Nonnull ArmorArmorWithTypeChecksum(NSData* _Nullable input, NSString* _Nullable armorType, BOOL checksum, NSError* _Nullable* _Nullable error); + +// skipped function ArmorWriterWithType with unsupported parameter or return types + + +// skipped function ArmorWriterWithTypeAndCustomHeaders with unsupported parameter or return types + + +// skipped function ArmorWriterWithTypeChecksum with unsupported parameter or return types + + +// skipped function IsPGPArmored with unsupported parameter or return types + + +/** + * Unarmor unarmors an armored input into a byte array. + */ +FOUNDATION_EXPORT NSData* _Nullable ArmorUnarmor(NSString* _Nullable input, NSError* _Nullable* _Nullable error); + +/** + * UnarmorBytes unarmors an armored input into a byte array. + */ +FOUNDATION_EXPORT NSData* _Nullable ArmorUnarmorBytes(NSData* _Nullable input, NSError* _Nullable* _Nullable error); + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Constants.objc.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Constants.objc.h new file mode 100644 index 00000000..3cc01507 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Constants.objc.h @@ -0,0 +1,197 @@ +// Objective-C API for talking to github.com/ProtonMail/gopenpgp/v3/constants Go package. +// gobind -lang=objc github.com/ProtonMail/gopenpgp/v3/constants +// +// File is generated by gobind. Do not edit. + +#ifndef __Constants_H__ +#define __Constants_H__ + +@import Foundation; +#include "ref.h" +#include "Universe.objc.h" + + +/** + * Cipher suite names. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsAES128; +/** + * Cipher suite names. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsAES192; +/** + * Cipher suite names. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsAES256; +/** + * ArmorChecksumEnabled defines the default behavior for adding an armor checksum +to an armored message. + +If set to true, an armor checksum is added to the message. + +If set to false, no armor checksum is added. + */ +FOUNDATION_EXPORT const BOOL ConstantsArmorChecksumEnabled; +/** + * Constants for armored data. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsArmorHeaderComment; +/** + * Constants for armored data. + */ +FOUNDATION_EXPORT const BOOL ConstantsArmorHeaderEnabled; +/** + * Constants for armored data. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsArmorHeaderVersion; +/** + * Cipher suite names. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsCAST5; +/** + * Wraps the packet.CipherFunction enum from go-crypto +for go-mobile clients. +int8 type for go-mobile support. + */ +FOUNDATION_EXPORT const int8_t ConstantsCipher3DES; +/** + * Wraps the packet.CipherFunction enum from go-crypto +for go-mobile clients. +int8 type for go-mobile support. + */ +FOUNDATION_EXPORT const int8_t ConstantsCipherAES128; +/** + * Wraps the packet.CipherFunction enum from go-crypto +for go-mobile clients. +int8 type for go-mobile support. + */ +FOUNDATION_EXPORT const int8_t ConstantsCipherAES192; +/** + * Wraps the packet.CipherFunction enum from go-crypto +for go-mobile clients. +int8 type for go-mobile support. + */ +FOUNDATION_EXPORT const int8_t ConstantsCipherAES256; +/** + * Wraps the packet.CipherFunction enum from go-crypto +for go-mobile clients. +int8 type for go-mobile support. + */ +FOUNDATION_EXPORT const int8_t ConstantsCipherCAST5; +/** + * Use compression defined by the pgp profile. + */ +FOUNDATION_EXPORT const int8_t ConstantsDefaultCompression; +/** + * HighSecurity is the high security level. + */ +FOUNDATION_EXPORT const int8_t ConstantsHighSecurity; +/** + * Use no compression (default). + */ +FOUNDATION_EXPORT const int8_t ConstantsNoCompression; +/** + * Constants for armored data. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsPGPMessageHeader; +/** + * Constants for armored data. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsPGPSignatureHeader; +/** + * Constants for armored data. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsPrivateKeyHeader; +/** + * Constants for armored data. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsPublicKeyHeader; +FOUNDATION_EXPORT const long ConstantsSIGNATURE_BAD_CONTEXT; +FOUNDATION_EXPORT const long ConstantsSIGNATURE_FAILED; +FOUNDATION_EXPORT const long ConstantsSIGNATURE_NOT_SIGNED; +FOUNDATION_EXPORT const long ConstantsSIGNATURE_NO_VERIFIER; +FOUNDATION_EXPORT const long ConstantsSIGNATURE_OK; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeBinary; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeCasualCert; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeCertificationRevocation; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeDirectSignature; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeGenericCert; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeKeyRevocation; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypePersonaCert; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypePositiveCert; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypePrimaryKeyBinding; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeSubkeyBinding; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeSubkeyRevocation; +/** + * OpenPGP signature types. +int8 type for go-mobile clients. + */ +FOUNDATION_EXPORT const int8_t ConstantsSigTypeText; +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsSignatureContextName; +/** + * StandardSecurity is the default security level. + */ +FOUNDATION_EXPORT const int8_t ConstantsStandardSecurity; +/** + * Cipher suite names. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsThreeDES; +/** + * Cipher suite names. + */ +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsTripleDES; +FOUNDATION_EXPORT NSString* _Nonnull const ConstantsVersion; +/** + * Use ZIP compression. + */ +FOUNDATION_EXPORT const int8_t ConstantsZIPCompression; +/** + * Use ZLIB compression. + */ +FOUNDATION_EXPORT const int8_t ConstantsZLIBCompression; + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Crypto.objc.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Crypto.objc.h new file mode 100644 index 00000000..9831bbea --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Crypto.objc.h @@ -0,0 +1,1963 @@ +// Objective-C API for talking to github.com/ProtonMail/gopenpgp/v3/crypto Go package. +// gobind -lang=objc github.com/ProtonMail/gopenpgp/v3/crypto +// +// File is generated by gobind. Do not edit. + +#ifndef __Crypto_H__ +#define __Crypto_H__ + +@import Foundation; +#include "ref.h" +#include "Universe.objc.h" + +#include "Profile.objc.h" +#include "Constants.objc.h" +#include "Armor.objc.h" + +@class CryptoDecryptionHandleBuilder; +@class CryptoEncryptionHandleBuilder; +@class CryptoIdentity; +@class CryptoKey; +@class CryptoKeyGenerationBuilder; +@class CryptoKeyRing; +@class CryptoLiteralMetadata; +@class CryptoPGPHandle; +@class CryptoPGPMessage; +@class CryptoPGPMessageBuffer; +@class CryptoSessionKey; +@class CryptoSignHandleBuilder; +@class CryptoSignatureVerificationError; +@class CryptoSigningContext; +@class CryptoVerificationContext; +@class CryptoVerifiedDataResult; +@class CryptoVerifiedSignature; +@class CryptoVerifyCleartextResult; +@class CryptoVerifyDataReader; +@class CryptoVerifyHandleBuilder; +@class CryptoVerifyResult; +@protocol CryptoEncryptionProfile; +@class CryptoEncryptionProfile; +@protocol CryptoKeyEncryptionProfile; +@class CryptoKeyEncryptionProfile; +@protocol CryptoKeyGenerationProfile; +@class CryptoKeyGenerationProfile; +@protocol CryptoPGPDecryption; +@class CryptoPGPDecryption; +@protocol CryptoPGPEncryption; +@class CryptoPGPEncryption; +@protocol CryptoPGPKeyGeneration; +@class CryptoPGPKeyGeneration; +@protocol CryptoPGPSign; +@class CryptoPGPSign; +@protocol CryptoPGPSplitReader; +@class CryptoPGPSplitReader; +@protocol CryptoPGPSplitWriter; +@class CryptoPGPSplitWriter; +@protocol CryptoPGPVerify; +@class CryptoPGPVerify; +@protocol CryptoReader; +@class CryptoReader; +@protocol CryptoSignProfile; +@class CryptoSignProfile; +@protocol CryptoWriteCloser; +@class CryptoWriteCloser; +@protocol CryptoWriter; +@class CryptoWriter; + +@protocol CryptoEncryptionProfile +// skipped method EncryptionProfile.CompressionConfig with unsupported parameter or return types + +// skipped method EncryptionProfile.EncryptionConfig with unsupported parameter or return types + +@end + +@protocol CryptoKeyEncryptionProfile +// skipped method KeyEncryptionProfile.KeyEncryptionConfig with unsupported parameter or return types + +@end + +@protocol CryptoKeyGenerationProfile +// skipped method KeyGenerationProfile.KeyGenerationConfig with unsupported parameter or return types + +@end + +@protocol CryptoPGPDecryption +/** + * ClearPrivateParams clears all private key material contained in EncryptionHandle from memory. + */ +- (void)clearPrivateParams; +/** + * Decrypt decrypts an encrypted pgp message. +Returns a VerifiedDataResult, which can be queried for potential signature verification errors, +and the plaintext data. Note that on a signature error, the method does not return an error. +Instead, the signature error is stored within the VerifiedDataResult. +The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto +where Auto tries to detect automatically. + */ +- (CryptoVerifiedDataResult* _Nullable)decrypt:(NSData* _Nullable)pgpMessage encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * DecryptDetached provides the same functionality as Decrypt but allows +to supply an encrypted detached signature that should be decrypted and verified +against the data in the pgp message. If encDetachedSignature is nil, the behavior is similar +to Decrypt. The encoding indicates if the input message should be unarmored or not, +i.e., Bytes/Armor/Auto where Auto tries to detect automatically. + */ +- (CryptoVerifiedDataResult* _Nullable)decryptDetached:(NSData* _Nullable)pgpMessage encDetachedSignature:(NSData* _Nullable)encDetachedSignature encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * DecryptSessionKey decrypts an encrypted session key. +To decrypt a session key, the decryption handle must contain either a decryption key or a password. + */ +- (CryptoSessionKey* _Nullable)decryptSessionKey:(NSData* _Nullable)keyPackets error:(NSError* _Nullable* _Nullable)error; +/** + * DecryptingReader returns a wrapper around underlying encryptedMessage Reader, +such that any read-operation via the wrapper results in a read from the decrypted pgp message. +The returned VerifyDataReader has to be fully read before any potential signatures can be verified. +Either read the message fully end then call VerifySignature or use the helper method ReadAllAndVerifySignature. +The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto +where Auto tries to detect automatically. +If encryptedMessage is of type PGPSplitReader, the method tries to verify an encrypted detached signature +that is read from the separate reader. + */ +- (CryptoVerifyDataReader* _Nullable)decryptingReader:(id _Nullable)encryptedMessage encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +@end + +@protocol CryptoPGPEncryption +/** + * ClearPrivateParams clears all private key material contained in EncryptionHandle from memory. + */ +- (void)clearPrivateParams; +/** + * Encrypt encrypts a plaintext message. + */ +- (CryptoPGPMessage* _Nullable)encrypt:(NSData* _Nullable)message error:(NSError* _Nullable* _Nullable)error; +/** + * EncryptSessionKey encrypts a session key with the encryption handle. +To encrypt a session key, the handle must contain either recipients or a password. + */ +- (NSData* _Nullable)encryptSessionKey:(CryptoSessionKey* _Nullable)sessionKey error:(NSError* _Nullable* _Nullable)error; +/** + * EncryptingWriter returns a wrapper around underlying output Writer, +such that any write-operation via the wrapper results in a write to an encrypted pgp message. +If the output Writer is of type PGPSplitWriter, the output can be split to multiple writers +for different parts of the message. For example to write key packets and encrypted data packets +to different writers or to write a detached signature separately. +The encoding argument defines the output encoding, i.e., Bytes or Armored +The returned pgp message WriteCloser must be closed after the plaintext has been written. + */ +- (id _Nullable)encryptingWriter:(id _Nullable)output encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * GenerateSessionKey generates a random session key for the given encryption handle +considering the algorithm preferences of the recipient keys. + */ +- (CryptoSessionKey* _Nullable)generateSessionKey:(NSError* _Nullable* _Nullable)error; +@end + +@protocol CryptoPGPKeyGeneration +/** + * GenerateKey generates a pgp key with the standard security level. + */ +- (CryptoKey* _Nullable)generateKey:(NSError* _Nullable* _Nullable)error; +/** + * GenerateKeyWithSecurity generates a pgp key with the given security level. +The argument security allows to set the security level, either standard or high. + */ +- (CryptoKey* _Nullable)generateKeyWithSecurity:(int8_t)securityLevel error:(NSError* _Nullable* _Nullable)error; +@end + +@protocol CryptoPGPSign +/** + * ClearPrivateParams clears all secret key material contained in the PGPSign from memory. + */ +- (void)clearPrivateParams; +/** + * Sign creates a detached or inline signature from the provided byte slice. +The encoding argument defines the output encoding, i.e., Bytes or Armored + */ +- (NSData* _Nullable)sign:(NSData* _Nullable)message encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * SignCleartext produces an armored cleartext message according to the specification. +Returns an armored message even if the PGPSign is not configured for armored output. + */ +- (NSData* _Nullable)signCleartext:(NSData* _Nullable)message error:(NSError* _Nullable* _Nullable)error; +/** + * SigningWriter returns a wrapper around underlying output Writer, +such that any write-operation via the wrapper results in a write to a detached or inline signature message. +The encoding argument defines the output encoding, i.e., Bytes or Armored +Once close is called on the returned WriteCloser the final signature is written to the output. +Thus, the returned WriteCloser must be closed after the plaintext has been written. + */ +- (id _Nullable)signingWriter:(id _Nullable)output encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +@end + +@protocol CryptoPGPSplitReader +- (BOOL)read:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +- (id _Nullable)signature; +@end + +@protocol CryptoPGPSplitWriter +/** + * Keys returns the Writer to which the key packets are written to. + */ +- (id _Nullable)keys; +/** + * Signature returns the Writer to which an encrypted detached signature is written to. + */ +- (id _Nullable)signature; +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +@protocol CryptoPGPVerify +/** + * VerifyCleartext verifies an armored cleartext message +and returns a VerifyCleartextResult. The VerifyCleartextResult can be checked for failure +and allows access the contained message +Note that an error is only returned if it is not a signature error. + */ +- (CryptoVerifyCleartextResult* _Nullable)verifyCleartext:(NSData* _Nullable)cleartext error:(NSError* _Nullable* _Nullable)error; +/** + * VerifyDetached verifies a detached signature pgp message +and returns a VerifyResult. The VerifyResult can be checked for failure +and allows access to information about the signatures. +Note that an error is only returned if it is not a signature error. +The encoding indicates if the input signature message should be unarmored or not, +i.e., Bytes/Armor/Auto where Auto tries to detect it automatically. + */ +- (CryptoVerifyResult* _Nullable)verifyDetached:(NSData* _Nullable)data signature:(NSData* _Nullable)signature encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * VerifyInline verifies an inline signed pgp message +and returns a VerifiedDataResult. The VerifiedDataResult can be checked for failure, +allows access to information about the signatures, and includes the plain message. +Note that an error is only returned if it is not a signature error. +The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto +where Auto tries to detect it automatically. + */ +- (CryptoVerifiedDataResult* _Nullable)verifyInline:(NSData* _Nullable)message encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * VerifyingReader wraps a reader with a signature verify reader. +Once all data is read from the returned verify reader, the signature can be verified +with (VerifyDataReader).VerifySignature(). +Note that an error is only returned if it is not a signature error. +The encoding indicates if the input signature message should be unarmored or not, +i.e., Bytes/Armor/Auto where Auto tries to detect it automatically. +If detachedData is nil, signatureMessage is treated as an inline signature message. +Thus, it is expected that signatureMessage contains the data to be verified. +If detachedData is not nil, signatureMessage must contain a detached signature, +which is verified against the detachedData. + */ +- (CryptoVerifyDataReader* _Nullable)verifyingReader:(id _Nullable)detachedData signatureMessage:(id _Nullable)signatureMessage encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +@end + +@protocol CryptoReader +- (BOOL)read:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +@protocol CryptoSignProfile +// skipped method SignProfile.SignConfig with unsupported parameter or return types + +@end + +@protocol CryptoWriteCloser +- (BOOL)close:(NSError* _Nullable* _Nullable)error; +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +@protocol CryptoWriter +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * DecryptionHandleBuilder allows to configure a decryption handle +to decrypt a pgp message. + */ +@interface CryptoDecryptionHandleBuilder : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +- (CryptoDecryptionHandleBuilder* _Nullable)decryptionKey:(CryptoKey* _Nullable)decryptionKey; +/** + * DecryptionKeys sets the secret keys for decrypting the pgp message. +Assumes that the message was encrypted towards one of the secret keys. +Triggers the hybrid decryption mode. +If not set, set another field for the type of decryption: SessionKey or Password. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)decryptionKeys:(CryptoKeyRing* _Nullable)decryptionKeyRing; +/** + * DisableAutomaticTextSanitize indicates that automatic text sanitization should be disabled. +If not disabled, the output will be sanitized if a text signature is present. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)disableAutomaticTextSanitize; +/** + * DisableIntendedRecipients indicates if the signature verification should not check if +the decryption key matches the intended recipients of the message. +If disabled, the decryption methods throw no error in a non-matching case. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)disableIntendedRecipients; +/** + * DisableStrictMessageParsing disables the check that decryption inputs conform +to the OpenPGP Message grammar. +If set, the decryption methods return no error if the message does not conform to the +OpenPGP message grammar. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)disableStrictMessageParsing; +/** + * DisableVerifyTimeCheck disables the check for comparing the signature creation time +against the verification time. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)disableVerifyTimeCheck; +- (BOOL)error:(NSError* _Nullable* _Nullable)error; +/** + * InsecureAllowDecryptionWithSigningKeys enables decryption of messages using keys +that are designated solely as signing keys. +While using the same key for both encryption and signing is discouraged +due to reduced security, this flag is useful for decrypting legacy messages. +This is because some older libraries did not respect key flags when +selecting a key for encryption. +SECURITY HAZARD: Use with care. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)insecureAllowDecryptionWithSigningKeys; +/** + * InsecureDisableUnauthenticatedMessagesCheck enables to read +encrypted messages without Modification Detection Code (MDC). +MDC is mandated by the latest standard and has long been implemented +in most OpenPGP implementations. Messages without MDC are considered unnecessarily +insecure and should be prevented whenever possible. +In case one needs to deal with messages from very old OpenPGP implementations, there +might be no other way than to tolerate the missing MDC. Setting this flag, allows this +mode of operation. It should be considered a measure of last resort. +SECURITY HAZARD: Use with care. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)insecureDisableUnauthenticatedMessagesCheck; +/** + * MaxDecompressedMessageSize defines the maximum number of bytes allowed for a message +after decompression. An error is thrown if the decompressed data exceeds this limit. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)maxDecompressedMessageSize:(int64_t)size; +/** + * New creates a DecryptionHandle and checks that the given +combination of parameters is valid. If one of the parameters are invalid +the latest error is returned. + */ +- (id _Nullable)new:(NSError* _Nullable* _Nullable)error; +/** + * Password sets a password that is used to derive a key to decrypt the pgp message. +Assumes that the message was encrypted with a key derived from the password. +Triggers the password decryption mode. +If not set, set another field for the type of decryption: DecryptionKeys or SessionKey. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)password:(NSData* _Nullable)password; +// skipped method DecryptionHandleBuilder.Passwords with unsupported parameter or return types + +/** + * PlainDetachedSignature indicates that the detached signature to verify is not decrypted +and can be verified as is. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)plainDetachedSignature; +/** + * RetrieveSessionKey sets the flag to indicate if the session key used for decryption +should be returned to the caller of the decryption function. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)retrieveSessionKey; +/** + * SessionKey sets a session key for decrypting the pgp message. +Assumes that the message was encrypted with session key provided. +Triggers the session key decryption mode. +If not set, set another field for the type of decryption: DecryptionKeys or Password. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)sessionKey:(CryptoSessionKey* _Nullable)sessionKey; +// skipped method DecryptionHandleBuilder.SessionKeys with unsupported parameter or return types + +/** + * Utf8 indicates if the output plaintext is Utf8 and +should be sanitized from canonicalised line endings. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)utf8; +/** + * VerificationContext sets a verification context for signatures of the pgp message, if any. +Only considered if VerifyKeys are set. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)verificationContext:(CryptoVerificationContext* _Nullable)verifyContext; +/** + * VerificationKey sets the public key for verifying the signatures of the pgp message, if any. +If not set, the signatures cannot be verified. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)verificationKey:(CryptoKey* _Nullable)key; +/** + * VerificationKeys sets the public keys for verifying the signatures of the pgp message, if any. +If not set, the signatures cannot be verified. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)verificationKeys:(CryptoKeyRing* _Nullable)keys; +/** + * VerifyTime sets the verification time to the provided timestamp. +If not set, the systems current time is used for signature verification. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)verifyTime:(int64_t)unixTime; +@end + +/** + * EncryptionHandleBuilder allows to configure a decryption handle to decrypt an OpenPGP message. + */ +@interface CryptoEncryptionHandleBuilder : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +/** + * Compress indicates if the plaintext should be compressed before encryption. +Compression affects security and opens the door for side-channel attacks, which +might allow to extract the plaintext data without a decryption key. +RFC9580 recommends to not use compression. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)compress; +/** + * CompressWith indicates if the plaintext should be compressed before encryption. +Compression affects security and opens the door for side-channel attacks, which +might allow to extract the plaintext data without a decryption key. +RFC9580 recommends to not use compression. +Allowed config options: +constants.NoCompression: none, constants.DefaultCompression: profile default +constants.ZIPCompression: zip, constants.ZLIBCompression: zlib. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)compressWith:(int8_t)config; +/** + * DetachedSignature indicates that the message should be signed, +but the signature should not be included in the same pgp message as the input data. +Instead the detached signature is encrypted in a separate pgp message. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)detachedSignature; +/** + * EncryptionTime allows to specify a separate time for selecting encryption keys +instead of the internal clock (also used for signing). Note that the internal clock can be changed with SignTime. +If the input unixTime is 0 no expiration checks are performed on the encryption keys. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)encryptionTime:(int64_t)unixTime; +/** + * Error returns an errors that occurred within the builder. + */ +- (BOOL)error:(NSError* _Nullable* _Nullable)error; +/** + * HiddenRecipient sets a public key to which the message should be encrypted to. +Triggers hybrid encryption with public keys of the recipients and hidden recipients. +The hidden recipients are NOT included in the intended recipient fingerprint list +of the signature, if a signature is present. +If not set, set another type of encryption: Recipients, SessionKey, or Password. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)hiddenRecipient:(CryptoKey* _Nullable)key; +/** + * HiddenRecipients sets the public keys to which the message should be encrypted to. +Triggers hybrid encryption with public keys of the recipients and hidden recipients. +The hidden recipients are NOT included in the intended recipient fingerprint list +of the signature, if a signature is present. +If not set, set another type of encryption: Recipients, SessionKey, or Password. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)hiddenRecipients:(CryptoKeyRing* _Nullable)hiddenRecipients; +/** + * IncludeExternalSignature indicates that the provided signature should be included +in the produced encrypted message. +Special feature: should not be used in normal use-cases, +can lead to broken or invalid PGP messages. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)includeExternalSignature:(NSData* _Nullable)signature; +/** + * New creates an EncryptionHandle and checks that the given +combination of parameters is valid. If the parameters are invalid +an error is returned. + */ +- (id _Nullable)new:(NSError* _Nullable* _Nullable)error; +/** + * Password sets a password the message should be encrypted with. +Triggers password based encryption with a key derived from the password. +If not set, set another the type of encryption: Recipients, HiddenRecipients, or SessionKey. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)password:(NSData* _Nullable)password; +/** + * PlainDetachedSignature indicates that the message should be signed, +but the signature should not be included in the same pgp message as the input data. +Instead the detached signature is a separate signature pgp message. +If DetachedSignature signature is set (i.e., the detached signature is encrypted), this option is ignored. +NOTE: A plaintext detached signature might reveal information about the encrypted plaintext. Thus, use with care. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)plainDetachedSignature; +/** + * Recipient sets the public key to which the message should be encrypted to. +Triggers hybrid encryption with public keys of the recipients and hidden recipients. +The recipients are included in the intended recipient fingerprint list +of the signature, if a signature is present. +If not set, set another type of encryption: HiddenRecipients, SessionKey, or Password. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)recipient:(CryptoKey* _Nullable)key; +/** + * Recipients sets the public keys to which the message should be encrypted to. +Triggers hybrid encryption with public keys of the recipients and hidden recipients. +The recipients are included in the intended recipient fingerprint list +of the signature, if a signature is present. +If not set, set another type of encryption: HiddenRecipients, SessionKey, or Password. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)recipients:(CryptoKeyRing* _Nullable)recipients; +/** + * SessionKey sets the session key the message should be encrypted with. +Triggers session key encryption with the included session key. +If not set, set another the type of encryption: Recipients, HiddenRecipients, or Password. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)sessionKey:(CryptoSessionKey* _Nullable)sessionKey; +/** + * SignTime sets the internal clock to always return +the supplied unix time for signing instead of the system time. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)signTime:(int64_t)unixTime; +/** + * SigningContext provides a signing context for the signature in the message. +Triggers that each signature includes the sining context. +SigningKeys have to be set if a SigningContext is provided. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)signingContext:(CryptoSigningContext* _Nullable)siningContext; +/** + * SigningKey sets the signing key that are used to create signature of the message. +Triggers that signatures are created for each signing key. +If not set, no signature is included. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)signingKey:(CryptoKey* _Nullable)key; +/** + * SigningKeys sets the signing keys that are used to create signature of the message. +Triggers that signatures are created for each signing key. +If not set, no signature is included. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)signingKeys:(CryptoKeyRing* _Nullable)signingKeys; +/** + * Utf8 indicates if the plaintext should be signed with a text type +signature. If set, the plaintext is signed after canonicalising the line endings. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)utf8; +@end + +/** + * Identity contains the name and the email of a key holder. + */ +@interface CryptoIdentity : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +@property (nonatomic) NSString* _Nonnull name; +@property (nonatomic) NSString* _Nonnull email; +@end + +/** + * Key contains a single private or public key. + */ +@interface CryptoKey : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewKey creates a new key from the first key in the unarmored or armored binary data. +Clones the binKeys data for go-mobile compatibility. + */ +- (nullable instancetype)init:(NSData* _Nullable)binKeys; +/** + * NewKeyFromArmored creates a new key from the first key in an armored string. + */ +- (nullable instancetype)initFromArmored:(NSString* _Nullable)armored; +// skipped constructor Key.NewKeyFromEntity with unsupported parameter or return types + +// skipped constructor Key.NewKeyFromReader with unsupported parameter or return types + +// skipped constructor Key.NewKeyFromReaderExplicit with unsupported parameter or return types + +/** + * NewKeyWithCloneFlag creates a new key from the first key in the unarmored or armored binary data. + */ +- (nullable instancetype)initWithCloneFlag:(NSData* _Nullable)binKeys clone:(BOOL)clone; +/** + * Armor returns the armored key as a string with default gopenpgp headers. + */ +- (NSString* _Nonnull)armor:(NSError* _Nullable* _Nullable)error; +/** + * ArmorWithCustomHeaders returns the armored key as a string, with +the given headers. Empty parameters are omitted from the headers. + */ +- (NSString* _Nonnull)armorWithCustomHeaders:(NSString* _Nullable)comment version:(NSString* _Nullable)version error:(NSError* _Nullable* _Nullable)error; +/** + * CanEncrypt returns true if any of the subkeys can be used for encryption. + */ +- (BOOL)canEncrypt:(int64_t)unixTime; +/** + * CanVerify returns true if any of the subkeys can be used for verification. + */ +- (BOOL)canVerify:(int64_t)unixTime; +/** + * Check verifies if the public keys match the private key parameters by +signing and verifying. +Deprecated: all keys are now checked on parsing. + */ +- (BOOL)check:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; +/** + * ClearPrivateParams zeroes the sensitive data in the key. + */ +- (BOOL)clearPrivateParams; +/** + * Copy creates a deep copy of the key. + */ +- (CryptoKey* _Nullable)copy:(NSError* _Nullable* _Nullable)error; +/** + * GetArmoredPublicKey returns the armored public keys from this keyring. + */ +- (NSString* _Nonnull)getArmoredPublicKey:(NSError* _Nullable* _Nullable)error; +/** + * GetArmoredPublicKeyWithCustomHeaders returns the armored public key as a string, with +the given headers. Empty parameters are omitted from the headers. + */ +- (NSString* _Nonnull)getArmoredPublicKeyWithCustomHeaders:(NSString* _Nullable)comment version:(NSString* _Nullable)version error:(NSError* _Nullable* _Nullable)error; +// skipped method Key.GetEntity with unsupported parameter or return types + +/** + * GetFingerprint gets the fingerprint from the key. + */ +- (NSString* _Nonnull)getFingerprint; +/** + * GetFingerprintBytes gets the fingerprint from the key as a byte slice. + */ +- (NSData* _Nullable)getFingerprintBytes; +/** + * GetHexKeyID returns the key ID, hex encoded as a string. + */ +- (NSString* _Nonnull)getHexKeyID; +/** + * GetJsonSHA256Fingerprints returns the SHA256 fingerprints of key and subkeys +encoded in JSON, for gomobile clients that cannot handle arrays. + */ +- (NSData* _Nullable)getJsonSHA256Fingerprints:(NSError* _Nullable* _Nullable)error; +// skipped method Key.GetKeyID with unsupported parameter or return types + +/** + * GetPublicKey returns the unarmored public keys from this keyring. + */ +- (NSData* _Nullable)getPublicKey:(NSError* _Nullable* _Nullable)error; +/** + * GetSHA256Fingerprint computes the SHA256 fingerprint of the primary key. + */ +- (NSString* _Nonnull)getSHA256Fingerprint; +// skipped method Key.GetSHA256Fingerprints with unsupported parameter or return types + +/** + * GetVersion returns the OpenPGP key packet version of this key. + */ +- (long)getVersion; +/** + * IsExpired checks whether the key is expired. + */ +- (BOOL)isExpired:(int64_t)unixTime; +/** + * IsLocked checks if a private key is locked. + */ +- (BOOL)isLocked:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; +/** + * IsPrivate returns true if the key is private. + */ +- (BOOL)isPrivate; +/** + * IsRevoked checks whether the key or the primary identity has a valid revocation signature. + */ +- (BOOL)isRevoked:(int64_t)unixTime; +/** + * IsUnlocked checks if a private key is unlocked. + */ +- (BOOL)isUnlocked:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; +/** + * PrintFingerprints is a debug helper function that prints the key and subkey fingerprints. + */ +- (void)printFingerprints; +- (NSData* _Nullable)serialize:(NSError* _Nullable* _Nullable)error; +/** + * ToPublic returns the corresponding public key of the given private key. + */ +- (CryptoKey* _Nullable)toPublic:(NSError* _Nullable* _Nullable)error; +/** + * Unlock unlocks a copy of the key. + */ +- (CryptoKey* _Nullable)unlock:(NSData* _Nullable)passphrase error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * KeyGenerationBuilder allows to configure a key generation handle to generate OpenPGP keys. + */ +@interface CryptoKeyGenerationBuilder : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +/** + * AddUserId adds the provided user identity to any generated key. + */ +- (CryptoKeyGenerationBuilder* _Nullable)addUserId:(NSString* _Nullable)name email:(NSString* _Nullable)email; +/** + * GenerationTime sets the key generation time to the given unixTime. + */ +- (CryptoKeyGenerationBuilder* _Nullable)generationTime:(int64_t)unixTime; +/** + * Lifetime sets the key lifetime to the given value in seconds. +The lifetime defaults to zero i.e., infinite lifetime. + */ +- (CryptoKeyGenerationBuilder* _Nullable)lifetime:(int32_t)seconds; +/** + * New creates a new key generation handle from the internal configuration +that allows to generate pgp keys. + */ +- (id _Nullable)new; +/** + * OverrideProfileAlgorithm allows to override the algorithm of the output key instead of using the profile's +algorithm with the respective security level. + +Allowed inputs (integer enum for go-mobile compatibility): +crypto.KeyGenerationRSA4096, crypto.KeyGenerationC25519, crypto.KeyGenerationC25519Refresh +crypto.KeyGenerationC448, crypto.KeyGenerationC448Refresh. + */ +- (CryptoKeyGenerationBuilder* _Nullable)overrideProfileAlgorithm:(long)algorithm; +@end + +/** + * KeyRing contains multiple private and public keys. + */ +@interface CryptoKeyRing : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewKeyRing creates a new KeyRing, empty if key is nil. + */ +- (nullable instancetype)init:(CryptoKey* _Nullable)key; +/** + * NewKeyRingFromBinary creates a new keyring with all the keys contained in the unarmored binary data. +Note that it accepts only unlocked or public keys, as KeyRing cannot contain locked keys. + */ +- (nullable instancetype)initFromBinary:(NSData* _Nullable)binKeys; +/** + * FirstKeyID as obtained from API to match salt + */ +@property (nonatomic) NSString* _Nonnull firstKeyID; +/** + * AddKey adds the given key to the keyring. + */ +- (BOOL)addKey:(CryptoKey* _Nullable)key error:(NSError* _Nullable* _Nullable)error; +/** + * CanEncrypt returns true if any of the keys in the keyring can be used for encryption. + */ +- (BOOL)canEncrypt:(int64_t)unixTime; +/** + * CanVerify returns true if any of the keys in the keyring can be used for verification. + */ +- (BOOL)canVerify:(int64_t)unixTime; +- (void)clearPrivateParams; +/** + * Copy creates a deep copy of the keyring. + */ +- (CryptoKeyRing* _Nullable)copy:(NSError* _Nullable* _Nullable)error; +/** + * CountDecryptionEntities returns the number of entities in the keyring. +Takes the current time for checking the keys in unix time format. +If the unix time is zero, time checks are ignored. + */ +- (long)countDecryptionEntities:(int64_t)unixTime; +/** + * CountEntities returns the number of entities in the keyring. + */ +- (long)countEntities; +/** + * FirstKey returns a KeyRing with only the first key of the original one. + */ +- (CryptoKeyRing* _Nullable)firstKey:(NSError* _Nullable* _Nullable)error; +/** + * GetHexKeyIDsJson returns an IDs of keys in this KeyRing as a json array. +Key ids are encoded as hexadecimal and nil is returned if an error occurs. +Helper function for go-mobile clients. + */ +- (NSData* _Nullable)getHexKeyIDsJson; +// skipped method KeyRing.GetIdentities with unsupported parameter or return types + +/** + * GetIdentitiesJson returns the list of identities associated with this key ring encoded as json. +Returns nil if an encoding error occurs. +Helper function for go-mobile clients. + */ +- (NSData* _Nullable)getIdentitiesJson; +/** + * GetKey returns the n-th openpgp key contained in this KeyRing. + */ +- (CryptoKey* _Nullable)getKey:(long)n error:(NSError* _Nullable* _Nullable)error; +// skipped method KeyRing.GetKeyIDs with unsupported parameter or return types + +// skipped method KeyRing.GetKeys with unsupported parameter or return types + +/** + * Serialize serializes a KeyRing to binary data. + */ +- (NSData* _Nullable)serialize:(NSError* _Nullable* _Nullable)error; +@end + +@interface CryptoLiteralMetadata : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +/** + * The file's latest modification time + */ +@property (nonatomic) int64_t modTime; +/** + * Filename returns the filename of the literal metadata. + */ +- (NSString* _Nonnull)filename; +/** + * IsUtf8 returns whether the literal metadata is annotated with utf-8. + */ +- (BOOL)isUtf8; +- (int64_t)time; +@end + +@interface CryptoPGPHandle : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +/** + * Decryption returns a builder to create a DecryptionHandle +for decrypting pgp messages. + */ +- (CryptoDecryptionHandleBuilder* _Nullable)decryption; +/** + * Encryption returns a builder to create an EncryptionHandle +for encrypting messages. + */ +- (CryptoEncryptionHandleBuilder* _Nullable)encryption; +/** + * GenerateSessionKey generates a random session key for the profile. +Use GenerateSessionKey on the encryption handle, if the PGP encryption keys are known. +This function only considers the profile to determine the session key type. + */ +- (CryptoSessionKey* _Nullable)generateSessionKey:(NSError* _Nullable* _Nullable)error; +/** + * KeyGeneration returns a builder to create a KeyGeneration handle. + */ +- (CryptoKeyGenerationBuilder* _Nullable)keyGeneration; +/** + * LockKey encrypts the private parts of a copy of the input key with the given passphrase. + */ +- (CryptoKey* _Nullable)lockKey:(CryptoKey* _Nullable)key passphrase:(NSData* _Nullable)passphrase error:(NSError* _Nullable* _Nullable)error; +/** + * Sign returns a builder to create a SignHandle +for signing messages. + */ +- (CryptoSignHandleBuilder* _Nullable)sign; +/** + * Verify returns a builder to create an VerifyHandle +for verifying signatures. + */ +- (CryptoVerifyHandleBuilder* _Nullable)verify; +@end + +/** + * PGPMessage stores a PGP-encrypted message. + */ +@interface CryptoPGPMessage : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewPGPMessage generates a new PGPMessage from the unarmored binary data. +Clones the data for go-mobile compatibility. + */ +- (nullable instancetype)init:(NSData* _Nullable)data; +/** + * NewPGPMessageFromArmored generates a new PGPMessage from an armored string ready for decryption. + */ +- (nullable instancetype)initFromArmored:(NSString* _Nullable)armored; +/** + * NewPGPMessageWithCloneFlag generates a new PGPMessage from the unarmored binary data. + */ +- (nullable instancetype)initWithCloneFlag:(NSData* _Nullable)data doClone:(BOOL)doClone; +/** + * KeyPacket references the PKESK and SKESK packets of the message + */ +@property (nonatomic) NSData* _Nullable keyPacket; +/** + * DataPacket references the SEIPD or AEAD protected packet of the message + */ +@property (nonatomic) NSData* _Nullable dataPacket; +/** + * DetachedSignature stores the encrypted detached signature. +Nil when the signature is embedded in the data packet or not present. + */ +@property (nonatomic) NSData* _Nullable detachedSignature; +/** + * Armor returns the armored message as a string. + */ +- (NSString* _Nonnull)armor:(NSError* _Nullable* _Nullable)error; +/** + * ArmorBytes returns the armored message as a string. + */ +- (NSData* _Nullable)armorBytes:(NSError* _Nullable* _Nullable)error; +/** + * ArmorWithCustomHeaders returns the armored message as a string, with +the given headers. Empty parameters are omitted from the headers. + */ +- (NSString* _Nonnull)armorWithCustomHeaders:(NSString* _Nullable)comment version:(NSString* _Nullable)version error:(NSError* _Nullable* _Nullable)error; +/** + * BinaryDataPacket returns the unarmored binary datapacket as a []byte. + */ +- (NSData* _Nullable)binaryDataPacket; +/** + * BinaryKeyPacket returns the unarmored binary keypacket as a []byte. + */ +- (NSData* _Nullable)binaryKeyPacket; +/** + * Bytes returns the unarmored binary content of the message as a []byte. + */ +- (NSData* _Nullable)bytes; +/** + * EncryptedDetachedSignature returns the encrypted detached signature of this message +as a PGPMessage where the data is the encrypted signature. +If no detached signature is present in this message, it returns nil. + */ +- (CryptoPGPMessage* _Nullable)encryptedDetachedSignature; +// skipped method PGPMessage.EncryptionKeyIDs with unsupported parameter or return types + +/** + * GetNumberOfKeyPackets returns the number of keys packets in this message. + */ +- (BOOL)getNumberOfKeyPackets:(long* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; +// skipped method PGPMessage.HexEncryptionKeyIDs with unsupported parameter or return types + +/** + * HexEncryptionKeyIDsJson returns the key IDs of the keys to which the session key is encrypted as a JSON array. +If an error occurs it returns nil. +Helper function for go-mobile clients. + */ +- (NSData* _Nullable)hexEncryptionKeyIDsJson; +// skipped method PGPMessage.HexSignatureKeyIDs with unsupported parameter or return types + +/** + * HexSignatureKeyIDsJson returns the key IDs of the keys to which the session key is encrypted as a JSON array. +If an error occurs it returns nil. +Helper function for go-mobile clients. + */ +- (NSData* _Nullable)hexSignatureKeyIDsJson; +// skipped method PGPMessage.NewReader with unsupported parameter or return types + +/** + * PlainDetachedSignature returns the plaintext detached signature of this message. +If no plaintext detached signature is present in this message, it returns an error. + */ +- (NSData* _Nullable)plainDetachedSignature:(NSError* _Nullable* _Nullable)error; +/** + * PlainDetachedSignatureArmor returns the armored plaintext detached signature of this message. +If no plaintext detached signature is present or armoring fails it returns an error. + */ +- (NSData* _Nullable)plainDetachedSignatureArmor:(NSError* _Nullable* _Nullable)error; +// skipped method PGPMessage.SignatureKeyIDs with unsupported parameter or return types + +@end + +@interface CryptoPGPMessageBuffer : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewPGPMessageBuffer creates a message buffer. + */ +- (nullable instancetype)init; +- (id _Nullable)keys; +/** + * PGPMessage returns the PGPMessage extracted from the internal buffers. + */ +- (CryptoPGPMessage* _Nullable)pgpMessage; +/** + * PGPMessageWithOptions returns the PGPMessage extracted from the internal buffers. +The isPlain flag indicates wether the detached signature is encrypted or plaintext, if any. + */ +- (CryptoPGPMessage* _Nullable)pgpMessageWithOptions:(BOOL)isPlain omitArmorChecksum:(BOOL)omitArmorChecksum; +- (id _Nullable)signature; +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * SessionKey stores a decrypted session key. + */ +@interface CryptoSessionKey : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewSessionKeyFromToken creates a SessionKey struct with the given token and algorithm. +Clones the token for compatibility with go-mobile. + */ +- (nullable instancetype)initFromToken:(NSData* _Nullable)token algo:(NSString* _Nullable)algo; +/** + * NewSessionKeyFromTokenWithAead creates a SessionKey struct with the given token and algorithm. +If aead is set to true, the key is used with v6 PKESK or SKESK, and SEIPDv2 packets. + */ +- (nullable instancetype)initFromTokenWithAead:(NSData* _Nullable)token algo:(NSString* _Nullable)algo aead:(BOOL)aead; +/** + * Key defines the decrypted binary session key. + */ +@property (nonatomic) NSData* _Nullable key; +/** + * Algo defines the symmetric encryption algorithm used with this key. +Only present if the key was not parsed from a v6 packet. + */ +@property (nonatomic) NSString* _Nonnull algo; +- (BOOL)clear; +/** + * GetBase64Key returns the session key as base64 encoded string. + */ +- (NSString* _Nonnull)getBase64Key; +// skipped method SessionKey.GetCipherFunc with unsupported parameter or return types + +/** + * GetCipherFuncInt returns the cipher function as int8 corresponding to the algorithm used +with this SessionKey. +The int8 type is used for go-mobile clients, see constant.Cipher... + */ +- (BOOL)getCipherFuncInt:(int8_t* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error; +/** + * IsV6 indicates if the session key can be used with SEIPDv2, PKESKv6/SKESKv6. + */ +- (BOOL)isV6; +@end + +/** + * SignHandleBuilder allows to configure a sign handle +to sign data with OpenPGP. + */ +@interface CryptoSignHandleBuilder : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +/** + * ArmorHeader indicates that the produced signature should be armored +with the given version and comment as header. +Note that this option only affects the method SignHandle.SigningWriter +and the headers in SignHandle.SignCleartext. + */ +- (CryptoSignHandleBuilder* _Nullable)armorHeader:(NSString* _Nullable)version comment:(NSString* _Nullable)comment; +/** + * Detached indicates if a detached signature should be produced. +The sign output will be a detached signature message without the data included. + */ +- (CryptoSignHandleBuilder* _Nullable)detached; +/** + * Error returns any errors that occurred within the builder. + */ +- (BOOL)error:(NSError* _Nullable* _Nullable)error; +/** + * New creates a SignHandle and checks that the given +combination of parameters is valid. If the parameters are invalid +an error is returned. + */ +- (id _Nullable)new:(NSError* _Nullable* _Nullable)error; +/** + * SignTime sets the internal clock to always return +the supplied unix time for signing instead of the device time. + */ +- (CryptoSignHandleBuilder* _Nullable)signTime:(int64_t)unixTime; +/** + * SigningContext provides a signing context for the signature in the message. +Triggers that each signature includes the sining context. + */ +- (CryptoSignHandleBuilder* _Nullable)signingContext:(CryptoSigningContext* _Nullable)signingContext; +/** + * SigningKey sets the signing key that is used to create signature of the message. + */ +- (CryptoSignHandleBuilder* _Nullable)signingKey:(CryptoKey* _Nullable)key; +/** + * SigningKeys sets the signing keys that are used to create signature of the message. + */ +- (CryptoSignHandleBuilder* _Nullable)signingKeys:(CryptoKeyRing* _Nullable)signingKeys; +/** + * Utf8 indicates if the plaintext should be signed with a text type +signature. If set, the plaintext is signed after +canonicalising the line endings. + */ +- (CryptoSignHandleBuilder* _Nullable)utf8; +@end + +/** + * SignatureVerificationError is returned from Decrypt and VerifyDetached +functions when signature verification fails. + */ +@interface CryptoSignatureVerificationError : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +@property (nonatomic) long status; +@property (nonatomic) NSString* _Nonnull message; +@property (nonatomic) NSError* _Nullable cause; +/** + * Error is the base method for all errors. + */ +- (NSString* _Nonnull)error; +/** + * Unwrap returns the cause of failure. + */ +- (BOOL)unwrap:(NSError* _Nullable* _Nullable)error; +@end + +/** + * SigningContext gives the context that will be +included in the signature's notation data. + */ +@interface CryptoSigningContext : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewSigningContext creates a new signing context. +The value is set to the notation data. +isCritical controls whether the notation is flagged as a critical packet. + */ +- (nullable instancetype)init:(NSString* _Nullable)value isCritical:(BOOL)isCritical; +@property (nonatomic) NSString* _Nonnull value; +@property (nonatomic) BOOL isCritical; +@end + +/** + * VerificationContext gives the context that will be +used to verify the signature. + */ +@interface CryptoVerificationContext : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewVerificationContext creates a new verification context. +The value is checked against the signature's notation data. +If isRequired is false, the signature is allowed to have no context set. +If requiredAfter is != 0, the signature is allowed to have no context set if it +was created before the unix time set in requiredAfter. + */ +- (nullable instancetype)init:(NSString* _Nullable)value isRequired:(BOOL)isRequired requiredAfter:(int64_t)requiredAfter; +@property (nonatomic) NSString* _Nonnull value; +@property (nonatomic) BOOL isRequired; +@property (nonatomic) int64_t requiredAfter; +@end + +/** + * VerifiedDataResult is a result that contains data and +the result of a potential signature verification on the data. + */ +@interface CryptoVerifiedDataResult : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field VerifiedDataResult.VerifyResult with unsupported type: github.com/ProtonMail/gopenpgp/v3/crypto.VerifyResult + +/** + * Bytes returns the result data as bytes. + */ +- (NSData* _Nullable)bytes; +- (void)constrainToTimeRange:(int64_t)unixFrom unixTo:(int64_t)unixTo; +/** + * Metadata returns the associated literal metadata of the data. + */ +- (CryptoLiteralMetadata* _Nullable)metadata; +/** + * SessionKey returns the session key the data is decrypted with. +Returns nil, if the data was not encrypted or +session key caching was not enabled. + */ +- (CryptoSessionKey* _Nullable)sessionKey; +- (NSData* _Nullable)signature:(NSError* _Nullable* _Nullable)error; +- (int64_t)signatureCreationTime; +- (BOOL)signatureError:(NSError* _Nullable* _Nullable)error; +- (CryptoSignatureVerificationError* _Nullable)signatureErrorExplicit; +- (NSData* _Nullable)signedByFingerprint; +- (CryptoKey* _Nullable)signedByKey; +// skipped method VerifiedDataResult.SignedByKeyId with unsupported parameter or return types + +- (NSString* _Nonnull)signedByKeyIdHex; +// skipped method VerifiedDataResult.SignedWithType with unsupported parameter or return types + +- (int8_t)signedWithTypeInt8; +/** + * String returns the result data as string. + */ +- (NSString* _Nonnull)string; +@end + +/** + * VerifiedSignature is a result of a signature verification. + */ +@interface CryptoVerifiedSignature : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field VerifiedSignature.Signature with unsupported type: *github.com/ProtonMail/go-crypto/openpgp/packet.Signature + +@property (nonatomic) CryptoKey* _Nullable signedBy; +@property (nonatomic) CryptoSignatureVerificationError* _Nullable signatureError; +@end + +/** + * VerifyCleartextResult is a result of a cleartext message verification. + */ +@interface CryptoVerifyCleartextResult : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field VerifyCleartextResult.VerifyResult with unsupported type: github.com/ProtonMail/gopenpgp/v3/crypto.VerifyResult + +/** + * Cleartext returns the parsed plain text of the result. + */ +- (NSData* _Nullable)cleartext; +- (void)constrainToTimeRange:(int64_t)unixFrom unixTo:(int64_t)unixTo; +- (NSData* _Nullable)signature:(NSError* _Nullable* _Nullable)error; +- (int64_t)signatureCreationTime; +- (BOOL)signatureError:(NSError* _Nullable* _Nullable)error; +- (CryptoSignatureVerificationError* _Nullable)signatureErrorExplicit; +- (NSData* _Nullable)signedByFingerprint; +- (CryptoKey* _Nullable)signedByKey; +// skipped method VerifyCleartextResult.SignedByKeyId with unsupported parameter or return types + +- (NSString* _Nonnull)signedByKeyIdHex; +// skipped method VerifyCleartextResult.SignedWithType with unsupported parameter or return types + +- (int8_t)signedWithTypeInt8; +@end + +/** + * VerifyDataReader is used for reading data that should be verified with a signature. +It further contains additional information about the parsed pgp message where the read +data stems from. + */ +@interface CryptoVerifyDataReader : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +/** + * DiscardAll reads all data from the reader and discards it. + */ +- (BOOL)discardAll:(NSError* _Nullable* _Nullable)error; +/** + * DiscardAllAndVerifySignature reads all plaintext data from the reader but discards it. +Returns a verification result for signature verification on the read data. + */ +- (CryptoVerifyResult* _Nullable)discardAllAndVerifySignature:(NSError* _Nullable* _Nullable)error; +/** + * GetMetadata returns the metadata of the literal data packet that +this reader reads from. Can be nil, if the data is not read from +a literal data packet. + */ +- (CryptoLiteralMetadata* _Nullable)getMetadata; +/** + * Read is used read data from the pgp message. +Makes VerifyDataReader implement the Reader interface. + */ +- (BOOL)read:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +/** + * ReadAll reads all plaintext data from the reader +and returns it as a byte slice. + */ +- (NSData* _Nullable)readAll:(NSError* _Nullable* _Nullable)error; +/** + * ReadAllAndVerifySignature reads all plaintext data from the reader +and tries to verify the signatures included in the message. +Returns the data in a VerifiedDataResult struct, which can be checked for signature errors. + */ +- (CryptoVerifiedDataResult* _Nullable)readAllAndVerifySignature:(NSError* _Nullable* _Nullable)error; +/** + * SessionKey returns the session key the data is decrypted with. +Returns nil, if this reader does not read from an encrypted message or +session key caching was not enabled. + */ +- (CryptoSessionKey* _Nullable)sessionKey; +/** + * VerifySignature is used to verify that the embedded signatures are valid. +This method needs to be called once all the data has been read. +It will return an error if the signature is invalid, no verifying keys are accessible, +or if the message hasn't been read entirely. + */ +- (CryptoVerifyResult* _Nullable)verifySignature:(NSError* _Nullable* _Nullable)error; +@end + +/** + * VerifyHandleBuilder configures a VerifyHandle handle. + */ +@interface CryptoVerifyHandleBuilder : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +/** + * DisableAutomaticTextSanitize indicates that automatic text sanitization should be disabled. +If not disabled, the output will be sanitized if a text signature is present. + */ +- (CryptoVerifyHandleBuilder* _Nullable)disableAutomaticTextSanitize; +/** + * DisableStrictMessageParsing disables the check that the inputs conform +to the OpenPGP message grammar. +If set, no error is thrown if the input message does not conform to the +OpenPGP specification. + */ +- (CryptoVerifyHandleBuilder* _Nullable)disableStrictMessageParsing; +/** + * DisableVerifyTimeCheck disables the check for comparing the signature expiration time +against the verification time. + */ +- (CryptoVerifyHandleBuilder* _Nullable)disableVerifyTimeCheck; +/** + * Error returns any errors that occurred within the builder. + */ +- (BOOL)error:(NSError* _Nullable* _Nullable)error; +/** + * MaxDecompressedMessageSize specifies the maximum allowed size, in bytes, +for a message after decompression within an inline-signed message. +If the decompressed message exceeds this limit, an error is returned. + */ +- (CryptoVerifyHandleBuilder* _Nullable)maxDecompressedMessageSize:(int64_t)size; +/** + * New creates a VerifyHandle and checks that the given +combination of parameters is valid. If the parameters are invalid, +an error is returned. + */ +- (id _Nullable)new:(NSError* _Nullable* _Nullable)error; +/** + * Utf8 indicates if the output plaintext is Utf8 and +should be sanitized from canonicalised line endings. +If enabled for detached verification, it canonicalises the input +before verification independent of the signature type. + */ +- (CryptoVerifyHandleBuilder* _Nullable)utf8; +/** + * VerificationContext sets a verification context for signatures of the pgp message, if any. +Only considered if VerifyKeys are set. + */ +- (CryptoVerifyHandleBuilder* _Nullable)verificationContext:(CryptoVerificationContext* _Nullable)verifyContext; +/** + * VerificationKey sets the public key for verifying the signatures. + */ +- (CryptoVerifyHandleBuilder* _Nullable)verificationKey:(CryptoKey* _Nullable)key; +/** + * VerificationKeys sets the public keys for verifying the signatures. + */ +- (CryptoVerifyHandleBuilder* _Nullable)verificationKeys:(CryptoKeyRing* _Nullable)keys; +/** + * VerifyTime sets the verification time to the provided timestamp. +If not set, the systems current time is used for signature verification. + */ +- (CryptoVerifyHandleBuilder* _Nullable)verifyTime:(int64_t)unixTime; +@end + +/** + * VerifyResult is a result of a pgp message signature verification. + */ +@interface CryptoVerifyResult : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field VerifyResult.Signatures with unsupported type: []*github.com/ProtonMail/gopenpgp/v3/crypto.VerifiedSignature + +/** + * ConstrainToTimeRange updates the signature result to only consider +signatures with a creation time within the given time frame. +unixFrom and unixTo are in unix time and are inclusive. + */ +- (void)constrainToTimeRange:(int64_t)unixFrom unixTo:(int64_t)unixTo; +/** + * Signature returns the serialized openpgp signature packet of the selected signature. + */ +- (NSData* _Nullable)signature:(NSError* _Nullable* _Nullable)error; +/** + * SignatureCreationTime returns the creation time of +the selected verified signature if found, else returns 0. + */ +- (int64_t)signatureCreationTime; +/** + * SignatureError returns nil if no signature err occurred else +the signature error. + */ +- (BOOL)signatureError:(NSError* _Nullable* _Nullable)error; +/** + * SignatureErrorExplicit returns nil if no signature err occurred else +the explicit signature error. + */ +- (CryptoSignatureVerificationError* _Nullable)signatureErrorExplicit; +/** + * SignedByFingerprint returns the key fingerprint of the key that was used to verify the selected signature, +if found, else returns nil. + */ +- (NSData* _Nullable)signedByFingerprint; +/** + * SignedByKey returns the key that was used to verify the selected signature, +if found, else returns nil. + */ +- (CryptoKey* _Nullable)signedByKey; +// skipped method VerifyResult.SignedByKeyId with unsupported parameter or return types + +/** + * SignedByKeyIdHex returns the key id of the key that was used to verify the selected signature +as a hex encoded string. +Helper for go-mobile. + */ +- (NSString* _Nonnull)signedByKeyIdHex; +// skipped method VerifyResult.SignedWithType with unsupported parameter or return types + +/** + * SignedWithTypeInt8 returns the type of the signature as int8 type if found, else returns 0. +See constants.SigType... for the different types. + */ +- (int8_t)signedWithTypeInt8; +@end + +/** + * PGPEncoding determines the message encoding. +The type is int8 for compatibility with gomobile. + */ +FOUNDATION_EXPORT const int8_t CryptoArmor; +/** + * PGPEncoding determines the message encoding. +The type is int8 for compatibility with gomobile. + */ +FOUNDATION_EXPORT const int8_t CryptoAuto; +/** + * PGPEncoding determines the message encoding. +The type is int8 for compatibility with gomobile. + */ +FOUNDATION_EXPORT const int8_t CryptoBytes; +/** + * KeyGenerationCurve25519 allows to override the output key algorithm in key generation to curve25519 (as defined in RFC9580). + */ +FOUNDATION_EXPORT const long CryptoKeyGenerationCurve25519; +/** + * KeyGenerationCurve25519Legacy allows to override the output key algorithm in key generation to curve25519 legacy (as defined in RFC4880bis). + */ +FOUNDATION_EXPORT const long CryptoKeyGenerationCurve25519Legacy; +/** + * KeyGenerationCurve448 allows to override the output key algorithm in key generation to curve448 (as defined in RFC9580). + */ +FOUNDATION_EXPORT const long CryptoKeyGenerationCurve448; +/** + * KeyGenerationRSA4096 allows to override the output key algorithm in key generation to rsa 4096. + */ +FOUNDATION_EXPORT const long CryptoKeyGenerationRSA4096; + +// skipped function FilterExpiredKeys with unsupported parameter or return types + + +/** + * GenerateSessionKeyAlgo generates a random key of the correct length for the +specified algorithm. + */ +FOUNDATION_EXPORT CryptoSessionKey* _Nullable CryptoGenerateSessionKeyAlgo(NSString* _Nullable algo, NSError* _Nullable* _Nullable error); + +/** + * IsPGPMessage checks if data if has armored PGP message format. + */ +FOUNDATION_EXPORT BOOL CryptoIsPGPMessage(NSString* _Nullable data); + +// skipped function NewConstantClock with unsupported parameter or return types + + +/** + * NewFileMetadata creates literal metadata. + */ +FOUNDATION_EXPORT CryptoLiteralMetadata* _Nullable CryptoNewFileMetadata(BOOL isUTF8, NSString* _Nullable filename, int64_t modTime); + +/** + * NewKey creates a new key from the first key in the unarmored or armored binary data. +Clones the binKeys data for go-mobile compatibility. + */ +FOUNDATION_EXPORT CryptoKey* _Nullable CryptoNewKey(NSData* _Nullable binKeys, NSError* _Nullable* _Nullable error); + +/** + * NewKeyFromArmored creates a new key from the first key in an armored string. + */ +FOUNDATION_EXPORT CryptoKey* _Nullable CryptoNewKeyFromArmored(NSString* _Nullable armored, NSError* _Nullable* _Nullable error); + +// skipped function NewKeyFromEntity with unsupported parameter or return types + + +// skipped function NewKeyFromReader with unsupported parameter or return types + + +// skipped function NewKeyFromReaderExplicit with unsupported parameter or return types + + +/** + * NewKeyRing creates a new KeyRing, empty if key is nil. + */ +FOUNDATION_EXPORT CryptoKeyRing* _Nullable CryptoNewKeyRing(CryptoKey* _Nullable key, NSError* _Nullable* _Nullable error); + +/** + * NewKeyRingFromBinary creates a new keyring with all the keys contained in the unarmored binary data. +Note that it accepts only unlocked or public keys, as KeyRing cannot contain locked keys. + */ +FOUNDATION_EXPORT CryptoKeyRing* _Nullable CryptoNewKeyRingFromBinary(NSData* _Nullable binKeys, NSError* _Nullable* _Nullable error); + +/** + * NewKeyWithCloneFlag creates a new key from the first key in the unarmored or armored binary data. + */ +FOUNDATION_EXPORT CryptoKey* _Nullable CryptoNewKeyWithCloneFlag(NSData* _Nullable binKeys, BOOL clone, NSError* _Nullable* _Nullable error); + +/** + * NewMetadata creates new default literal metadata with utf-8 set to isUTF8. + */ +FOUNDATION_EXPORT CryptoLiteralMetadata* _Nullable CryptoNewMetadata(BOOL isUTF8); + +/** + * NewPGPMessage generates a new PGPMessage from the unarmored binary data. +Clones the data for go-mobile compatibility. + */ +FOUNDATION_EXPORT CryptoPGPMessage* _Nullable CryptoNewPGPMessage(NSData* _Nullable data); + +/** + * NewPGPMessageBuffer creates a message buffer. + */ +FOUNDATION_EXPORT CryptoPGPMessageBuffer* _Nullable CryptoNewPGPMessageBuffer(void); + +/** + * NewPGPMessageFromArmored generates a new PGPMessage from an armored string ready for decryption. + */ +FOUNDATION_EXPORT CryptoPGPMessage* _Nullable CryptoNewPGPMessageFromArmored(NSString* _Nullable armored, NSError* _Nullable* _Nullable error); + +/** + * NewPGPMessageWithCloneFlag generates a new PGPMessage from the unarmored binary data. + */ +FOUNDATION_EXPORT CryptoPGPMessage* _Nullable CryptoNewPGPMessageWithCloneFlag(NSData* _Nullable data, BOOL doClone); + +/** + * NewPGPSplitMessage generates a new PGPSplitMessage from the binary unarmored keypacket and datapacket. +Clones the slices for go-mobile compatibility. + */ +FOUNDATION_EXPORT CryptoPGPMessage* _Nullable CryptoNewPGPSplitMessage(NSData* _Nullable keyPacket, NSData* _Nullable dataPacket); + +/** + * NewPGPSplitWriter creates a type that implements the PGPSplitWriter interface +for encrypting a plaintext where the output PGP packets should be written to the different streams provided. +Key packets are written to keyPackets whereas the encrypted data packets are written to encPackets. +The encrypted detached signature data is written to encSigPacket. + */ +FOUNDATION_EXPORT id _Nullable CryptoNewPGPSplitWriter(id _Nullable keyPackets, id _Nullable encPackets, id _Nullable encSigPacket); + +/** + * NewPGPSplitWriterDetachedSignature creates a type that implements the PGPSplitWriter interface +for encrypting a plaintext where the output PGP messages should be written to the different streams provided. +The encrypted data message is written to encMessage whereas the encrypted detached signature is written to +encSigMessage. + */ +FOUNDATION_EXPORT id _Nullable CryptoNewPGPSplitWriterDetachedSignature(id _Nullable encMessage, id _Nullable encSigMessage); + +/** + * NewPGPSplitWriterFromWriter creates a type that implements the PGPSplitWriter interface +for encrypting a plaintext where the output PGP messages to the provided Writer. + */ +FOUNDATION_EXPORT id _Nullable CryptoNewPGPSplitWriterFromWriter(id _Nullable writer); + +/** + * NewPGPSplitWriterKeyAndData creates a type that implements the PGPSplitWriter interface +for encrypting a plaintext where the output PGP packets should be written to the different streams provided. +Key packets are written to keyPackets whereas the encrypted data packets are written to encPackets. + */ +FOUNDATION_EXPORT id _Nullable CryptoNewPGPSplitWriterKeyAndData(id _Nullable keyPackets, id _Nullable encPackets); + +/** + * NewPrivateKeyFromArmored creates a new secret key from the first key in an armored string +and unlocks it with the password. + */ +FOUNDATION_EXPORT CryptoKey* _Nullable CryptoNewPrivateKeyFromArmored(NSString* _Nullable armored, NSData* _Nullable password, NSError* _Nullable* _Nullable error); + +/** + * NewSessionKeyFromToken creates a SessionKey struct with the given token and algorithm. +Clones the token for compatibility with go-mobile. + */ +FOUNDATION_EXPORT CryptoSessionKey* _Nullable CryptoNewSessionKeyFromToken(NSData* _Nullable token, NSString* _Nullable algo); + +/** + * NewSessionKeyFromTokenWithAead creates a SessionKey struct with the given token and algorithm. +If aead is set to true, the key is used with v6 PKESK or SKESK, and SEIPDv2 packets. + */ +FOUNDATION_EXPORT CryptoSessionKey* _Nullable CryptoNewSessionKeyFromTokenWithAead(NSData* _Nullable token, NSString* _Nullable algo, BOOL aead); + +/** + * NewSigningContext creates a new signing context. +The value is set to the notation data. +isCritical controls whether the notation is flagged as a critical packet. + */ +FOUNDATION_EXPORT CryptoSigningContext* _Nullable CryptoNewSigningContext(NSString* _Nullable value, BOOL isCritical); + +/** + * NewVerificationContext creates a new verification context. +The value is checked against the signature's notation data. +If isRequired is false, the signature is allowed to have no context set. +If requiredAfter is != 0, the signature is allowed to have no context set if it +was created before the unix time set in requiredAfter. + */ +FOUNDATION_EXPORT CryptoVerificationContext* _Nullable CryptoNewVerificationContext(NSString* _Nullable value, BOOL isRequired, int64_t requiredAfter); + +/** + * PGP creates a PGPHandle to interact with the API. +Uses the default profile for configuration. + */ +FOUNDATION_EXPORT CryptoPGPHandle* _Nullable CryptoPGP(void); + +/** + * PGPWithProfile creates a PGPHandle to interact with the API. +Uses the provided profile for configuration. + */ +FOUNDATION_EXPORT CryptoPGPHandle* _Nullable CryptoPGPWithProfile(ProfileCustom* _Nullable profile); + +/** + * RandomToken generates a random token with the specified key size. + */ +FOUNDATION_EXPORT NSData* _Nullable CryptoRandomToken(long size, NSError* _Nullable* _Nullable error); + +// skipped function SignatureHexKeyIDs with unsupported parameter or return types + + +// skipped function SignatureKeyIDs with unsupported parameter or return types + + +// skipped function ZeroClock with unsupported parameter or return types + + +@class CryptoEncryptionProfile; + +@class CryptoKeyEncryptionProfile; + +@class CryptoKeyGenerationProfile; + +@class CryptoPGPDecryption; + +@class CryptoPGPEncryption; + +@class CryptoPGPKeyGeneration; + +@class CryptoPGPSign; + +@class CryptoPGPSplitReader; + +@class CryptoPGPSplitWriter; + +@class CryptoPGPVerify; + +@class CryptoReader; + +@class CryptoSignProfile; + +@class CryptoWriteCloser; + +@class CryptoWriter; + +@interface CryptoEncryptionProfile : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +// skipped method EncryptionProfile.CompressionConfig with unsupported parameter or return types + +// skipped method EncryptionProfile.EncryptionConfig with unsupported parameter or return types + +@end + +@interface CryptoKeyEncryptionProfile : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +// skipped method KeyEncryptionProfile.KeyEncryptionConfig with unsupported parameter or return types + +@end + +@interface CryptoKeyGenerationProfile : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +// skipped method KeyGenerationProfile.KeyGenerationConfig with unsupported parameter or return types + +@end + +/** + * PGPDecryption is an interface for decrypting pgp messages with GopenPGP. +Use the DecryptionHandleBuilder to create a handle that implements PGPDecryption. + */ +@interface CryptoPGPDecryption : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * ClearPrivateParams clears all private key material contained in EncryptionHandle from memory. + */ +- (void)clearPrivateParams; +/** + * Decrypt decrypts an encrypted pgp message. +Returns a VerifiedDataResult, which can be queried for potential signature verification errors, +and the plaintext data. Note that on a signature error, the method does not return an error. +Instead, the signature error is stored within the VerifiedDataResult. +The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto +where Auto tries to detect automatically. + */ +- (CryptoVerifiedDataResult* _Nullable)decrypt:(NSData* _Nullable)pgpMessage encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * DecryptDetached provides the same functionality as Decrypt but allows +to supply an encrypted detached signature that should be decrypted and verified +against the data in the pgp message. If encDetachedSignature is nil, the behavior is similar +to Decrypt. The encoding indicates if the input message should be unarmored or not, +i.e., Bytes/Armor/Auto where Auto tries to detect automatically. + */ +- (CryptoVerifiedDataResult* _Nullable)decryptDetached:(NSData* _Nullable)pgpMessage encDetachedSignature:(NSData* _Nullable)encDetachedSignature encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * DecryptSessionKey decrypts an encrypted session key. +To decrypt a session key, the decryption handle must contain either a decryption key or a password. + */ +- (CryptoSessionKey* _Nullable)decryptSessionKey:(NSData* _Nullable)keyPackets error:(NSError* _Nullable* _Nullable)error; +/** + * DecryptingReader returns a wrapper around underlying encryptedMessage Reader, +such that any read-operation via the wrapper results in a read from the decrypted pgp message. +The returned VerifyDataReader has to be fully read before any potential signatures can be verified. +Either read the message fully end then call VerifySignature or use the helper method ReadAllAndVerifySignature. +The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto +where Auto tries to detect automatically. +If encryptedMessage is of type PGPSplitReader, the method tries to verify an encrypted detached signature +that is read from the separate reader. + */ +- (CryptoVerifyDataReader* _Nullable)decryptingReader:(id _Nullable)encryptedMessage encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * PGPEncryption is an interface for encrypting messages with GopenPGP. +Use an EncryptionHandleBuilder to create a PGPEncryption handle. + */ +@interface CryptoPGPEncryption : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * ClearPrivateParams clears all private key material contained in EncryptionHandle from memory. + */ +- (void)clearPrivateParams; +/** + * Encrypt encrypts a plaintext message. + */ +- (CryptoPGPMessage* _Nullable)encrypt:(NSData* _Nullable)message error:(NSError* _Nullable* _Nullable)error; +/** + * EncryptSessionKey encrypts a session key with the encryption handle. +To encrypt a session key, the handle must contain either recipients or a password. + */ +- (NSData* _Nullable)encryptSessionKey:(CryptoSessionKey* _Nullable)sessionKey error:(NSError* _Nullable* _Nullable)error; +/** + * EncryptingWriter returns a wrapper around underlying output Writer, +such that any write-operation via the wrapper results in a write to an encrypted pgp message. +If the output Writer is of type PGPSplitWriter, the output can be split to multiple writers +for different parts of the message. For example to write key packets and encrypted data packets +to different writers or to write a detached signature separately. +The encoding argument defines the output encoding, i.e., Bytes or Armored +The returned pgp message WriteCloser must be closed after the plaintext has been written. + */ +- (id _Nullable)encryptingWriter:(id _Nullable)output encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * GenerateSessionKey generates a random session key for the given encryption handle +considering the algorithm preferences of the recipient keys. + */ +- (CryptoSessionKey* _Nullable)generateSessionKey:(NSError* _Nullable* _Nullable)error; +@end + +/** + * PGPKeyGeneration is an interface for generating pgp keys with GopenPGP. +Use the KeyGenerationBuilder to create a handle that implements PGPKeyGeneration. + */ +@interface CryptoPGPKeyGeneration : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * GenerateKey generates a pgp key with the standard security level. + */ +- (CryptoKey* _Nullable)generateKey:(NSError* _Nullable* _Nullable)error; +/** + * GenerateKeyWithSecurity generates a pgp key with the given security level. +The argument security allows to set the security level, either standard or high. + */ +- (CryptoKey* _Nullable)generateKeyWithSecurity:(int8_t)securityLevel error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * PGPSign is an interface for creating signature messages with GopenPGP. + */ +@interface CryptoPGPSign : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * ClearPrivateParams clears all secret key material contained in the PGPSign from memory. + */ +- (void)clearPrivateParams; +/** + * Sign creates a detached or inline signature from the provided byte slice. +The encoding argument defines the output encoding, i.e., Bytes or Armored + */ +- (NSData* _Nullable)sign:(NSData* _Nullable)message encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * SignCleartext produces an armored cleartext message according to the specification. +Returns an armored message even if the PGPSign is not configured for armored output. + */ +- (NSData* _Nullable)signCleartext:(NSData* _Nullable)message error:(NSError* _Nullable* _Nullable)error; +/** + * SigningWriter returns a wrapper around underlying output Writer, +such that any write-operation via the wrapper results in a write to a detached or inline signature message. +The encoding argument defines the output encoding, i.e., Bytes or Armored +Once close is called on the returned WriteCloser the final signature is written to the output. +Thus, the returned WriteCloser must be closed after the plaintext has been written. + */ +- (id _Nullable)signingWriter:(id _Nullable)output encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +@end + +@interface CryptoPGPSplitReader : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (BOOL)read:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +- (id _Nullable)signature; +@end + +/** + * PGPSplitWriter is an interface to write different parts of a PGP message +(i.e., packets) to different streams. + */ +@interface CryptoPGPSplitWriter : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * Keys returns the Writer to which the key packets are written to. + */ +- (id _Nullable)keys; +/** + * Signature returns the Writer to which an encrypted detached signature is written to. + */ +- (id _Nullable)signature; +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * PGPVerify is an interface for verifying detached signatures with GopenPGP. + */ +@interface CryptoPGPVerify : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * VerifyCleartext verifies an armored cleartext message +and returns a VerifyCleartextResult. The VerifyCleartextResult can be checked for failure +and allows access the contained message +Note that an error is only returned if it is not a signature error. + */ +- (CryptoVerifyCleartextResult* _Nullable)verifyCleartext:(NSData* _Nullable)cleartext error:(NSError* _Nullable* _Nullable)error; +/** + * VerifyDetached verifies a detached signature pgp message +and returns a VerifyResult. The VerifyResult can be checked for failure +and allows access to information about the signatures. +Note that an error is only returned if it is not a signature error. +The encoding indicates if the input signature message should be unarmored or not, +i.e., Bytes/Armor/Auto where Auto tries to detect it automatically. + */ +- (CryptoVerifyResult* _Nullable)verifyDetached:(NSData* _Nullable)data signature:(NSData* _Nullable)signature encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * VerifyInline verifies an inline signed pgp message +and returns a VerifiedDataResult. The VerifiedDataResult can be checked for failure, +allows access to information about the signatures, and includes the plain message. +Note that an error is only returned if it is not a signature error. +The encoding indicates if the input message should be unarmored or not, i.e., Bytes/Armor/Auto +where Auto tries to detect it automatically. + */ +- (CryptoVerifiedDataResult* _Nullable)verifyInline:(NSData* _Nullable)message encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +/** + * VerifyingReader wraps a reader with a signature verify reader. +Once all data is read from the returned verify reader, the signature can be verified +with (VerifyDataReader).VerifySignature(). +Note that an error is only returned if it is not a signature error. +The encoding indicates if the input signature message should be unarmored or not, +i.e., Bytes/Armor/Auto where Auto tries to detect it automatically. +If detachedData is nil, signatureMessage is treated as an inline signature message. +Thus, it is expected that signatureMessage contains the data to be verified. +If detachedData is not nil, signatureMessage must contain a detached signature, +which is verified against the detachedData. + */ +- (CryptoVerifyDataReader* _Nullable)verifyingReader:(id _Nullable)detachedData signatureMessage:(id _Nullable)signatureMessage encoding:(int8_t)encoding error:(NSError* _Nullable* _Nullable)error; +@end + +@interface CryptoReader : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (BOOL)read:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +@interface CryptoSignProfile : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +// skipped method SignProfile.SignConfig with unsupported parameter or return types + +@end + +/** + * WriteCloser replicates the io.WriteCloser interface for go-mobile. + */ +@interface CryptoWriteCloser : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (BOOL)close:(NSError* _Nullable* _Nullable)error; +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * Writer replicates the io.Writer interface for go-mobile. + */ +@interface CryptoWriter : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Gopenpgp.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Gopenpgp.h new file mode 100644 index 00000000..ee169cd5 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Gopenpgp.h @@ -0,0 +1,23 @@ + +// Objective-C API for talking to the following Go packages +// +// github.com/ProtonMail/gopenpgp/v3/crypto +// github.com/ProtonMail/gopenpgp/v3/armor +// github.com/ProtonMail/gopenpgp/v3/constants +// github.com/ProtonMail/gopenpgp/v3/mime +// github.com/ProtonMail/gopenpgp/v3/mobile +// github.com/ProtonMail/gopenpgp/v3/profile +// +// File is generated by gomobile bind. Do not edit. +#ifndef __Gopenpgp_FRAMEWORK_H__ +#define __Gopenpgp_FRAMEWORK_H__ + +#include "Crypto.objc.h" +#include "Armor.objc.h" +#include "Constants.objc.h" +#include "Mime.objc.h" +#include "Mobile.objc.h" +#include "Profile.objc.h" +#include "Universe.objc.h" + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mime.objc.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mime.objc.h new file mode 100644 index 00000000..ad2b2d51 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mime.objc.h @@ -0,0 +1,59 @@ +// Objective-C API for talking to github.com/ProtonMail/gopenpgp/v3/mime Go package. +// gobind -lang=objc github.com/ProtonMail/gopenpgp/v3/mime +// +// File is generated by gobind. Do not edit. + +#ifndef __Mime_H__ +#define __Mime_H__ + +@import Foundation; +#include "ref.h" +#include "Universe.objc.h" + +#include "Constants.objc.h" +#include "Crypto.objc.h" + +@protocol MimeMIMECallbacks; +@class MimeMIMECallbacks; + +@protocol MimeMIMECallbacks +- (void)onAttachment:(NSString* _Nullable)headers data:(NSData* _Nullable)data; +- (void)onBody:(NSString* _Nullable)body mimetype:(NSString* _Nullable)mimetype; +/** + * Encrypted headers can be in an attachment and thus be placed at the end of the mime structure. + */ +- (void)onEncryptedHeaders:(NSString* _Nullable)headers; +- (void)onError:(NSError* _Nullable)err; +- (void)onVerified:(long)verified; +@end + +/** + * Decrypt decrypts and verifies a MIME message. +messageEncoding provides the encoding of the encrypted MIME message, either crypto.Bytes or crypto.Armor. +The decryptionHandle is used to decrypt and verify the message, while +the verifyHandle is used to verify the signature contained in the decrypted mime message. +The verifyHandle can be nil. + */ +FOUNDATION_EXPORT void MimeDecrypt(NSData* _Nullable message, int8_t messageEncoding, id _Nullable decryptionHandle, id _Nullable verifyHandle, id _Nullable callbacks); + +@class MimeMIMECallbacks; + +/** + * MIMECallbacks defines callback methods to process a MIME message. + */ +@interface MimeMIMECallbacks : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (void)onAttachment:(NSString* _Nullable)headers data:(NSData* _Nullable)data; +- (void)onBody:(NSString* _Nullable)body mimetype:(NSString* _Nullable)mimetype; +/** + * Encrypted headers can be in an attachment and thus be placed at the end of the mime structure. + */ +- (void)onEncryptedHeaders:(NSString* _Nullable)headers; +- (void)onError:(NSError* _Nullable)err; +- (void)onVerified:(long)verified; +@end + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mobile.objc.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mobile.objc.h new file mode 100644 index 00000000..95663f82 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Mobile.objc.h @@ -0,0 +1,252 @@ +// Objective-C API for talking to github.com/ProtonMail/gopenpgp/v3/mobile Go package. +// gobind -lang=objc github.com/ProtonMail/gopenpgp/v3/mobile +// +// File is generated by gobind. Do not edit. + +#ifndef __Mobile_H__ +#define __Mobile_H__ + +@import Foundation; +#include "ref.h" +#include "Universe.objc.h" + +#include "Crypto.objc.h" + +@class MobileDetachedSignaturePGPSplitReader; +@class MobileGo2AndroidReader; +@class MobileGo2IOSReader; +@class MobileKeyPacketSplitWriter; +@class MobileMobile2GoReader; +@class MobileMobile2GoWriter; +@class MobileMobile2GoWriterWithSHA256; +@class MobileMobileReadResult; +@protocol MobileMobileReader; +@class MobileMobileReader; + +@protocol MobileMobileReader +- (MobileMobileReadResult* _Nullable)read:(long)max error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * DetachedSignaturePGPSplitReader implements the crypto.PGPSplitReader interface. + */ +@interface MobileDetachedSignaturePGPSplitReader : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nullable instancetype)init:(NSData* _Nullable)keyPacket dataReader:(id _Nullable)dataReader encSignature:(CryptoPGPMessage* _Nullable)encSignature; +- (BOOL)read:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +- (id _Nullable)signature; +@end + +/** + * Go2AndroidReader is used to wrap a native golang Reader in the golang runtime, +to be usable in the android app runtime (via gomobile). + */ +@interface MobileGo2AndroidReader : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewGo2AndroidReader wraps a native golang Reader to be usable in the mobile app runtime (via gomobile). +It doesn't follow the standard golang Reader behavior, and returns n = -1 on EOF. + */ +- (nullable instancetype)init:(id _Nullable)reader; +/** + * Read reads bytes into the provided buffer and returns the number of bytes read +It doesn't follow the standard golang Reader behavior, and returns n = -1 on EOF. + */ +- (BOOL)read:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * Go2IOSReader is used to wrap a native golang Reader in the golang runtime, +to be usable in the iOS app runtime (via gomobile) as a MobileReader. + */ +@interface MobileGo2IOSReader : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewGo2IOSReader wraps a native golang Reader to be usable in the ios app runtime (via gomobile). + */ +- (nullable instancetype)init:(id _Nullable)reader; +/** + * Read reads at most bytes from the wrapped Reader and returns the read data as a MobileReadResult. + */ +- (MobileMobileReadResult* _Nullable)read:(long)max error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * KeyPacketSplitWriter implements the crypto.PGPSplitWriter interface +for splitting encryptions output into different packets. +Internally buffers the key packets and potential detached encrypted signatures. + */ +@interface MobileKeyPacketSplitWriter : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nullable instancetype)init:(id _Nullable)dataWriter; +/** + * EncryptedDetachedSignature returns the internally buffered encrypted detached signature. + */ +- (CryptoPGPMessage* _Nullable)encryptedDetachedSignature; +/** + * KeyPackets returns the internally buffered key packets. + */ +- (NSData* _Nullable)keyPackets; +- (id _Nullable)keys; +- (id _Nullable)signature; +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * Mobile2GoReader is used to wrap a MobileReader in the mobile app runtime, +to be usable in the golang runtime (via gomobile) as a native Reader. + */ +@interface MobileMobile2GoReader : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewMobile2GoReader wraps a MobileReader to be usable in the golang runtime (via gomobile). + */ +- (nullable instancetype)init:(id _Nullable)reader; +/** + * Read reads data from the wrapped MobileReader and copies the read data in the provided buffer. +It also handles the conversion of EOF to an error. + */ +- (BOOL)read:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * Mobile2GoWriter is used to wrap a writer in the mobile app runtime, +to be usable in the golang runtime (via gomobile). + */ +@interface MobileMobile2GoWriter : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewMobile2GoWriter wraps a writer to be usable in the golang runtime (via gomobile). + */ +- (nullable instancetype)init:(id _Nullable)writer; +/** + * Write writes the data in the provided buffer in the wrapped writer. +It clones the provided data to prevent errors with garbage collectors. + */ +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * Mobile2GoWriterWithSHA256 is used to wrap a writer in the mobile app runtime, +to be usable in the golang runtime (via gomobile). +It also computes the SHA256 hash of the data being written on the fly. + */ +@interface MobileMobile2GoWriterWithSHA256 : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewMobile2GoWriterWithSHA256 wraps a writer to be usable in the golang runtime (via gomobile). +The wrapper also computes the SHA256 hash of the data being written on the fly. + */ +- (nullable instancetype)init:(id _Nullable)writer; +/** + * GetSHA256 returns the SHA256 hash of the data that's been written so far. + */ +- (NSData* _Nullable)getSHA256; +/** + * Write writes the data in the provided buffer in the wrapped writer. +It clones the provided data to prevent errors with garbage collectors. +It also computes the SHA256 hash of the data being written on the fly. + */ +- (BOOL)write:(NSData* _Nullable)b n:(long* _Nullable)n error:(NSError* _Nullable* _Nullable)error; +@end + +/** + * MobileReadResult is what needs to be returned by MobileReader.Read. +The read data is passed as a return value rather than passed as an argument to the reader. +This avoids problems introduced by gomobile that prevent the use of native golang readers. + */ +@interface MobileMobileReadResult : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +/** + * NewMobileReadResult initialize a MobileReadResult with the correct values. +It clones the data to avoid the garbage collector freeing the data too early. + */ +- (nullable instancetype)init:(long)n eof:(BOOL)eof data:(NSData* _Nullable)data; +@property (nonatomic) long n; +@property (nonatomic) BOOL isEOF; +@property (nonatomic) NSData* _Nullable data; +@end + +/** + * FreeOSMemory can be used to explicitly +call the garbage collector and +return the unused memory to the OS. + */ +FOUNDATION_EXPORT void MobileFreeOSMemory(void); + +FOUNDATION_EXPORT MobileDetachedSignaturePGPSplitReader* _Nullable MobileNewDetachedSignaturePGPSplitReader(NSData* _Nullable keyPacket, id _Nullable dataReader, CryptoPGPMessage* _Nullable encSignature); + +/** + * NewGo2AndroidReader wraps a native golang Reader to be usable in the mobile app runtime (via gomobile). +It doesn't follow the standard golang Reader behavior, and returns n = -1 on EOF. + */ +FOUNDATION_EXPORT MobileGo2AndroidReader* _Nullable MobileNewGo2AndroidReader(id _Nullable reader); + +/** + * NewGo2IOSReader wraps a native golang Reader to be usable in the ios app runtime (via gomobile). + */ +FOUNDATION_EXPORT MobileGo2IOSReader* _Nullable MobileNewGo2IOSReader(id _Nullable reader); + +FOUNDATION_EXPORT MobileKeyPacketSplitWriter* _Nullable MobileNewKeyPacketSplitWriter(id _Nullable dataWriter); + +/** + * NewMobile2GoReader wraps a MobileReader to be usable in the golang runtime (via gomobile). + */ +FOUNDATION_EXPORT MobileMobile2GoReader* _Nullable MobileNewMobile2GoReader(id _Nullable reader); + +/** + * NewMobile2GoWriter wraps a writer to be usable in the golang runtime (via gomobile). + */ +FOUNDATION_EXPORT MobileMobile2GoWriter* _Nullable MobileNewMobile2GoWriter(id _Nullable writer); + +/** + * NewMobile2GoWriterWithSHA256 wraps a writer to be usable in the golang runtime (via gomobile). +The wrapper also computes the SHA256 hash of the data being written on the fly. + */ +FOUNDATION_EXPORT MobileMobile2GoWriterWithSHA256* _Nullable MobileNewMobile2GoWriterWithSHA256(id _Nullable writer); + +/** + * NewMobileReadResult initialize a MobileReadResult with the correct values. +It clones the data to avoid the garbage collector freeing the data too early. + */ +FOUNDATION_EXPORT MobileMobileReadResult* _Nullable MobileNewMobileReadResult(long n, BOOL eof, NSData* _Nullable data); + +@class MobileMobileReader; + +/** + * MobileReader is the interface that readers in the mobile runtime must use and implement. +This is a workaround to some of the gomobile limitations. + */ +@interface MobileMobileReader : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (MobileMobileReadResult* _Nullable)read:(long)max error:(NSError* _Nullable* _Nullable)error; +@end + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Profile.objc.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Profile.objc.h new file mode 100644 index 00000000..805075a7 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Profile.objc.h @@ -0,0 +1,107 @@ +// Objective-C API for talking to github.com/ProtonMail/gopenpgp/v3/profile Go package. +// gobind -lang=objc github.com/ProtonMail/gopenpgp/v3/profile +// +// File is generated by gobind. Do not edit. + +#ifndef __Profile_H__ +#define __Profile_H__ + +@import Foundation; +#include "ref.h" +#include "Universe.objc.h" + +#include "Constants.objc.h" + +@class ProfileCustom; + +/** + * Custom type represents a profile for setting algorithm +parameters for generating keys, encrypting data, and +signing data. +Use one of the pre-defined profiles if possible. +i.e., profile.Default(), profile.RFC4880(). + */ +@interface ProfileCustom : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field Custom.SetKeyAlgorithm with unsupported type: func(*github.com/ProtonMail/go-crypto/openpgp/packet.Config, int8) + +// skipped field Custom.AeadKeyEncryption with unsupported type: *github.com/ProtonMail/go-crypto/openpgp/packet.AEADConfig + +// skipped field Custom.S2kKeyEncryption with unsupported type: *github.com/ProtonMail/go-crypto/openpgp/s2k.Config + +// skipped field Custom.AeadEncryption with unsupported type: *github.com/ProtonMail/go-crypto/openpgp/packet.AEADConfig + +// skipped field Custom.S2kEncryption with unsupported type: *github.com/ProtonMail/go-crypto/openpgp/s2k.Config + +// skipped field Custom.CompressionConfiguration with unsupported type: *github.com/ProtonMail/go-crypto/openpgp/packet.CompressionConfig + +// skipped field Custom.Hash with unsupported type: crypto.Hash + +// skipped field Custom.SignHash with unsupported type: *crypto.Hash + +// skipped field Custom.CipherKeyEncryption with unsupported type: github.com/ProtonMail/go-crypto/openpgp/packet.CipherFunction + +// skipped field Custom.CipherEncryption with unsupported type: github.com/ProtonMail/go-crypto/openpgp/packet.CipherFunction + +// skipped field Custom.CompressionAlgorithm with unsupported type: github.com/ProtonMail/go-crypto/openpgp/packet.CompressionAlgo + +/** + * V6 is a flag to indicate if v6 from the crypto-refresh should be used. + */ +@property (nonatomic) BOOL v6; +/** + * AllowAllPublicKeyAlgorithms is a flag to disable all checks for deprecated public key algorithms. + */ +@property (nonatomic) BOOL allowAllPublicKeyAlgorithms; +/** + * DisableIntendedRecipients is a flag to disable the intended recipients pgp feature from the crypto-refresh. + */ +@property (nonatomic) BOOL disableIntendedRecipients; +/** + * InsecureAllowWeakRSA is a flag to disable checks for weak rsa keys. + */ +@property (nonatomic) BOOL insecureAllowWeakRSA; +/** + * InsecureAllowDecryptionWithSigningKeys is a flag to enable to decrypt with signing keys for compatibility reasons. + */ +@property (nonatomic) BOOL insecureAllowDecryptionWithSigningKeys; +/** + * MaxDecompressedMessageSize sets the maximum decompressed messages size that can be read +before throwing an error. + */ +@property (nonatomic) int64_t maxDecompressedMessageSize; +// skipped method Custom.CompressionConfig with unsupported parameter or return types + +// skipped method Custom.EncryptionConfig with unsupported parameter or return types + +// skipped method Custom.KeyEncryptionConfig with unsupported parameter or return types + +// skipped method Custom.KeyGenerationConfig with unsupported parameter or return types + +// skipped method Custom.SignConfig with unsupported parameter or return types + +@end + +/** + * Default returns a custom profile that support features +that are widely implemented. + */ +FOUNDATION_EXPORT ProfileCustom* _Nullable ProfileDefault(void); + +/** + * RFC4880 returns a custom profile for this library +that conforms with the algorithms in RFC4880. + */ +FOUNDATION_EXPORT ProfileCustom* _Nullable ProfileRFC4880(void); + +/** + * RFC9580 returns a custom profile for this library +that conforms with the algorithms in RFC9580. + */ +FOUNDATION_EXPORT ProfileCustom* _Nullable ProfileRFC9580(void); + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Universe.objc.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Universe.objc.h new file mode 100644 index 00000000..019e7502 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/Universe.objc.h @@ -0,0 +1,29 @@ +// Objective-C API for talking to Go package. +// gobind -lang=objc +// +// File is generated by gobind. Do not edit. + +#ifndef __Universe_H__ +#define __Universe_H__ + +@import Foundation; +#include "ref.h" + +@protocol Universeerror; +@class Universeerror; + +@protocol Universeerror +- (NSString* _Nonnull)error; +@end + +@class Universeerror; + +@interface Universeerror : NSError { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (NSString* _Nonnull)error; +@end + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/ref.h b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/ref.h new file mode 100644 index 00000000..b8036a4d --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Headers/ref.h @@ -0,0 +1,35 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef __GO_REF_HDR__ +#define __GO_REF_HDR__ + +#include + +// GoSeqRef is an object tagged with an integer for passing back and +// forth across the language boundary. A GoSeqRef may represent either +// an instance of a Go object, or an Objective-C object passed to Go. +// The explicit allocation of a GoSeqRef is used to pin a Go object +// when it is passed to Objective-C. The Go seq package maintains a +// reference to the Go object in a map keyed by the refnum along with +// a reference count. When the reference count reaches zero, the Go +// seq package will clear the corresponding entry in the map. +@interface GoSeqRef : NSObject { +} +@property(readonly) int32_t refnum; +@property(strong) id obj; // NULL when representing a Go object. + +// new GoSeqRef object to proxy a Go object. The refnum must be +// provided from Go side. +- (instancetype)initWithRefnum:(int32_t)refnum obj:(id)obj; + +- (int32_t)incNum; + +@end + +@protocol goSeqRefInterface +-(GoSeqRef*) _ref; +@end + +#endif diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Info.plist b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Info.plist new file mode 100644 index 00000000..c0de7a0c --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Info.plist @@ -0,0 +1,20 @@ + + + + + CFBundleExecutable + Gopenpgp + CFBundleIdentifier + Gopenpgp + CFBundlePackageType + FMWK + CFBundleShortVersionString + 0.0.1758379400 + CFBundleVersion + 0.0.1758379400 + MinimumOSVersion + 15.5 + RCTNewArchEnabled + + + diff --git a/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Modules/module.modulemap b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Modules/module.modulemap new file mode 100644 index 00000000..e7900014 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/Frameworks/Gopenpgp.xcframework/ios-arm64_x86_64-simulator/Gopenpgp.framework/Modules/module.modulemap @@ -0,0 +1,13 @@ +framework module "Gopenpgp" { + header "ref.h" + header "Crypto.objc.h" + header "Armor.objc.h" + header "Constants.objc.h" + header "Mime.objc.h" + header "Mobile.objc.h" + header "Profile.objc.h" + header "Universe.objc.h" + header "Gopenpgp.h" + + export * +} \ No newline at end of file diff --git a/native-modules/react-native-bundle-update/ios/ReactNativeBundleUpdate.swift b/native-modules/react-native-bundle-update/ios/ReactNativeBundleUpdate.swift new file mode 100644 index 00000000..c60f1b40 --- /dev/null +++ b/native-modules/react-native-bundle-update/ios/ReactNativeBundleUpdate.swift @@ -0,0 +1,1339 @@ +import NitroModules +import ReactNativeNativeLogger +import Foundation +import CommonCrypto +import Gopenpgp +import SSZipArchive +import MMKV + +// OneKey GPG public key for signature verification +private let GPG_PUBLIC_KEY = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGJATGwBEADL1K7b8dzYYzlSsvAGiA8mz042pygB7AAh/uFUycpNQdSzuoDE +VoXq/QsXCOsGkMdFLwlUjarRaxFX6RTV6S51LOlJFRsyGwXiMz08GSNagSafQ0YL +Gi+aoemPh6Ta5jWgYGIUWXavkjJciJYw43ACMdVmIWos94bA41Xm93dq9C3VRpl+ +EjvGAKRUMxJbH8r13TPzPmfN4vdrHLq+us7eKGJpwV/VtD9vVHAi0n48wGRq7DQw +IUDU2mKy3wmjwS38vIIu4yQyeUdl4EqwkCmGzWc7Cv2HlOG6rLcUdTAOMNBBX1IQ +iHKg9Bhh96MXYvBhEL7XHJ96S3+gTHw/LtrccBM+eiDJVHPZn+lw2HqX994DueLV +tAFDS+qf3ieX901IC97PTHsX6ztn9YZQtSGBJO3lEMBdC4ez2B7zUv4bgyfU+KvE +zHFIK9HmDehx3LoDAYc66nhZXyasiu6qGPzuxXu8/4qTY8MnhXJRBkbWz5P84fx1 +/Db5WETLE72on11XLreFWmlJnEWN4UOARrNn1Zxbwl+uxlSJyM+2GTl4yoccG+WR +uOUCmRXTgduHxejPGI1PfsNmFpVefAWBDO7SdnwZb1oUP3AFmhH5CD1GnmLnET+l +/c+7XfFLwgSUVSADBdO3GVS4Cr9ux4nIrHGJCrrroFfM2yvG8AtUVr16PQARAQAB +tCJvbmVrZXlocSBkZXZlbG9wZXIgPGRldkBvbmVrZXkuc28+iQJUBBMBCAA+FiEE +62iuVE8f3YzSZGJPs2mmepC/OHsFAmJATGwCGwMFCQeGH0QFCwkIBwIGFQoJCAsC +BBYCAwECHgECF4AACgkQs2mmepC/OHtgvg//bsWFMln08ZJjf5od/buJua7XYb3L +jWq1H5rdjJva5TP1UuQaDULuCuPqllxb+h+RB7g52yRG/1nCIrpTfveYOVtq/mYE +D12KYAycDwanbmtoUp25gcKqCrlNeSE1EXmPlBzyiNzxJutE1DGlvbY3rbuNZLQi +UTFBG3hk6JgsaXkFCwSmF95uATAaItv8aw6eY7RWv47rXhQch6PBMCir4+a/v7vs +lXxQtcpCqfLtjrloq7wvmD423yJVsUGNEa7/BrwFz6/GP6HrUZc6JgvrieuiBE4n +ttXQFm3dkOfD+67MLMO3dd7nPhxtjVEGi+43UH3/cdtmU4JFX3pyCQpKIlXTEGp2 +wqim561auKsRb1B64qroCwT7aACwH0ZTgQS8rPifG3QM8ta9QheuOsjHLlqjo8jI +fpqe0vKYUlT092joT0o6nT2MzmLmHUW0kDqD9p6JEJEZUZpqcSRE84eMTFNyu966 +xy/rjN2SMJTFzkNXPkwXYrMYoahGez1oZfLzV6SQ0+blNc3aATt9aQW6uaCZtMw1 +ibcfWW9neHVpRtTlMYCoa2reGaBGCv0Nd8pMcyFUQkVaes5cQHkh3r5Dba+YrVvp +l4P8HMbN8/LqAv7eBfj3ylPa/8eEPWVifcum2Y9TqherN1C2JDqWIpH4EsApek3k +NMK6q0lPxXjZ3Pa5Ag0EYkBMbAEQAM1R4N3bBkwKkHeYwsQASevUkHwY4eg6Ncgp +f9NbmJHcEioqXTIv0nHCQbos3P2NhXvDowj4JFkK/ZbpP9yo0p7TI4fckseVSWwI +tiF9l/8OmXvYZMtw3hHcUUZVdJnk0xrqT6ni6hyRFIfbqous6/vpqi0GG7nB/+lU +E5StGN8696ZWRyAX9MmwoRoods3ShNJP0+GCYHfIcG0XRhEDMJph+7mWPlkQUcza +4aEjxOQ4Stwwp+ZL1rXSlyJIPk1S9/FIS/Uw5GgqFJXIf5n+SCVtUZ8lGedEWwe4 +wXsoPFxxOc2Gqw5r4TrJFdgA3MptYebXmb2LGMssXQTM1AQS2LdpnWw44+X1CHvQ +0m4pEw/g2OgeoJPBurVUnu2mU/M+ARZiS4ceAR0pLZN7Yq48p1wr6EOBQdA3Usby +uc17MORG/IjRmjz4SK/luQLXjN+0jwQSoM1kcIHoRk37B8feHjVufJDKlqtw83H1 +uNu6lGwb8MxDgTuuHloDijCDQsn6m7ZKU1qqLDGtdvCUY2ovzuOUS9vv6MAhR86J +kqoU3sOBMeQhnBaTNKU0IjT4M+ERCWQ7MewlzXuPHgyb4xow1SKZny+f+fYXPy9+ +hx4/j5xaKrZKdq5zIo+GRGe4lA088l253nGeLgSnXsbSxqADqKK73d7BXLCVEZHx +f4Sa5JN7ABEBAAGJAjwEGAEIACYWIQTraK5UTx/djNJkYk+zaaZ6kL84ewUCYkBM +bAIbDAUJB4YfRAAKCRCzaaZ6kL84e0UGD/4mVWyGoQC86TyPoU4Pb5r8mynXWmiH +ZGKu2ll8qn3l5Q67OophgbA1I0GTBFsYK2f91ahgs7FEsLrmz/25E8ybcdJipITE +6869nyE1b37jVb3z3BJLYS/4MaNvugNz4VjMHWVAL52glXLN+SJBSNscmWZDKnVn +Rnrn+kBEvOWZgLbi4MpPiNVwm2PGnrtPzudTcg/NS3HOcmJTfG3mrnwwNJybTVAx +txlQPoXUpJQqJjtkPPW+CqosolpRdugQ5zpFSg05iL+vN+CMrVPkk85w87dtsidl +yZl/ZNITrLzym9d2UFVQZY2rRohNdRfx3l4rfXJFLaqQtihRvBIiMKTbUb2V0pd3 +rVLz2Ck3gJqPfPEEmCWS0Nx6rME8m0sOkNyMau3dMUUAs4j2c3pOQmsZRjKo7LAc +7/GahKFhZ2aBCQzvcTES+gPH1Z5HnivkcnUF2gnQV9x7UOr1Q/euKJsxPl5CCZtM +N9GFW10cDxFo7cO5Ch+/BkkkfebuI/4Wa1SQTzawsxTx4eikKwcemgfDsyIqRs2W +62PBrqCzs9Tg19l35sCdmvYsvMadrYFXukHXiUKEpwJMdTLAtjJ+AX84YLwuHi3+ +qZ5okRCqZH+QpSojSScT9H5ze4ZpuP0d8pKycxb8M2RfYdyOtT/eqsZ/1EQPg7kq +P2Q5dClenjjjVA== +=F0np +-----END PGP PUBLIC KEY BLOCK----- +""" + +// Public static store for AppDelegate access (called before JS starts) +@objcMembers +public class BundleUpdateStore: NSObject { + private static let bundlePrefsKey = "currentBundleVersion" + private static let nativeVersionKey = "nativeVersion" + + public static func documentDirectory() -> String { + NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + } + + public static func downloadBundleDir() -> String { + let dir = (documentDirectory() as NSString).appendingPathComponent("onekey-bundle-download") + let fm = FileManager.default + if !fm.fileExists(atPath: dir) { + try? fm.createDirectory(atPath: dir, withIntermediateDirectories: true) + } + return dir + } + + public static func bundleDir() -> String { + let dir = (documentDirectory() as NSString).appendingPathComponent("onekey-bundle") + let fm = FileManager.default + if !fm.fileExists(atPath: dir) { + try? fm.createDirectory(atPath: dir, withIntermediateDirectories: true) + } + return dir + } + + public static func ascDir() -> String { + let dir = (bundleDir() as NSString).appendingPathComponent("asc") + let fm = FileManager.default + if !fm.fileExists(atPath: dir) { + try? fm.createDirectory(atPath: dir, withIntermediateDirectories: true) + } + return dir + } + + public static func signatureFilePath(_ version: String) -> String { + return (ascDir() as NSString).appendingPathComponent("\(version)-signature.asc") + } + + public static func writeSignatureFile(_ version: String, signature: String) { + let path = signatureFilePath(version) + let existed = FileManager.default.fileExists(atPath: path) + try? signature.write(toFile: path, atomically: true, encoding: .utf8) + let fileSize = (try? FileManager.default.attributesOfItem(atPath: path)[.size] as? UInt64) ?? 0 + OneKeyLog.info("BundleUpdate", "writeSignatureFile: version=\(version), existed=\(existed), size=\(fileSize), path=\(path)") + } + + public static func readSignatureFile(_ version: String) -> String { + let path = signatureFilePath(version) + guard FileManager.default.fileExists(atPath: path) else { + OneKeyLog.debug("BundleUpdate", "readSignatureFile: not found for version=\(version)") + return "" + } + let content = (try? String(contentsOfFile: path, encoding: .utf8)) ?? "" + OneKeyLog.debug("BundleUpdate", "readSignatureFile: version=\(version), size=\(content.count)") + return content + } + + public static func deleteSignatureFile(_ version: String) { + let path = signatureFilePath(version) + try? FileManager.default.removeItem(atPath: path) + } + + public static func getCurrentNativeVersion() -> String { + Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" + } + + public static func currentBundleVersion() -> String? { + UserDefaults.standard.string(forKey: bundlePrefsKey) + } + + public static func setCurrentBundleVersion(_ version: String) { + UserDefaults.standard.set(version, forKey: bundlePrefsKey) + UserDefaults.standard.synchronize() + } + + public static func currentBundleDir() -> String? { + guard let folderName = currentBundleVersion() else { return nil } + return (bundleDir() as NSString).appendingPathComponent(folderName) + } + + public static func getWebEmbedPath() -> String { + guard let dir = currentBundleDir() else { return "" } + return (dir as NSString).appendingPathComponent("web-embed") + } + + public static func calculateSHA256(_ filePath: String) -> String? { + guard let fileHandle = FileHandle(forReadingAtPath: filePath) else { return nil } + defer { fileHandle.closeFile() } + + var context = CC_SHA256_CTX() + CC_SHA256_Init(&context) + while autoreleasepool(invoking: { + let data = fileHandle.readData(ofLength: 8192) + if data.count > 0 { + data.withUnsafeBytes { CC_SHA256_Update(&context, $0.baseAddress, CC_LONG(data.count)) } + return true + } + return false + }) {} + var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + CC_SHA256_Final(&hash, &context) + return hash.map { String(format: "%02x", $0) }.joined() + } + + public static func getNativeVersion() -> String? { + UserDefaults.standard.string(forKey: nativeVersionKey) + } + + public static func setNativeVersion(_ version: String) { + UserDefaults.standard.set(version, forKey: nativeVersionKey) + UserDefaults.standard.synchronize() + } + + public static func getMetadataFilePath(_ currentBundleVersion: String) -> String? { + let path = (bundleDir() as NSString) + .appendingPathComponent(currentBundleVersion) + let metadataPath = (path as NSString).appendingPathComponent("metadata.json") + guard FileManager.default.fileExists(atPath: metadataPath) else { return nil } + return metadataPath + } + + public static func getMetadataFileContent(_ currentBundleVersion: String) -> [String: String]? { + guard let path = getMetadataFilePath(currentBundleVersion), + let data = FileManager.default.contents(atPath: path), + let json = try? JSONSerialization.jsonObject(with: data) as? [String: String] else { return nil } + return json + } + + /// Returns true if OneKey developer mode (DevSettings) is enabled. + /// Reads the persisted value from MMKV storage written by the JS ServiceDevSetting layer. + public static func isDevSettingsEnabled() -> Bool { + // Ensure MMKV is initialized (safe to call multiple times) + MMKV.initialize(rootDir: nil) + guard let mmkv = MMKV(mmapID: "onekey-app-dev-setting") else { return false } + return mmkv.bool(forKey: "onekey_developer_mode_enabled", defaultValue: false) + } + + /// Returns true if the skip-GPG-verification toggle is enabled in developer settings. + /// Reads the persisted value from MMKV storage (key: onekey_bundle_skip_gpg_verification, + /// instance: onekey-app-dev-setting). + public static func isSkipGPGEnabled() -> Bool { + MMKV.initialize(rootDir: nil) + guard let mmkv = MMKV(mmapID: "onekey-app-dev-setting") else { return false } + return mmkv.bool(forKey: "onekey_bundle_skip_gpg_verification", defaultValue: false) + } + + public static func readMetadataFileSha256(_ signature: String) -> String? { + guard !signature.isEmpty else { return nil } + + // GPG cleartext signature verification is required + guard let sha256 = verifyGPGAndExtractSha256(signature) else { + OneKeyLog.error("BundleUpdate", "readMetadataFileSha256: GPG verification failed, rejecting unsigned content") + return nil + } + return sha256 + } + + /// Verify a PGP cleartext-signed message and extract the sha256 from the signed JSON body. + /// Uses Gopenpgp framework (vendored xcframework). + /// Returns nil if verification fails. + public static func verifyGPGAndExtractSha256(_ signature: String) -> String? { + // Check if this looks like a PGP signed message + guard signature.contains("-----BEGIN PGP SIGNED MESSAGE-----") else { + return nil + } + + // 1. Load public key + guard let pubKey = CryptoKey(fromArmored: GPG_PUBLIC_KEY) else { + OneKeyLog.error("BundleUpdate", "Failed to parse GPG public key") + return nil + } + + // 2. Get PGP handle + guard let pgp = CryptoPGP() else { + OneKeyLog.error("BundleUpdate", "Failed to create PGPHandle") + return nil + } + + // 3. Build verify handle: pgp.verify().verificationKey(pubKey).new() + guard let verifyBuilder = pgp.verify() else { + OneKeyLog.error("BundleUpdate", "Failed to get verify builder") + return nil + } + guard let builderWithKey = verifyBuilder.verificationKey(pubKey) else { + OneKeyLog.error("BundleUpdate", "Failed to set verification key") + return nil + } + + let verifyHandle: any CryptoPGPVerifyProtocol + do { + verifyHandle = try builderWithKey.new() + } catch { + OneKeyLog.error("BundleUpdate", "Failed to create verify handle: \(error.localizedDescription)") + return nil + } + + // 4. Verify cleartext + guard let signatureData = signature.data(using: .utf8) else { + return nil + } + + let cleartextResult: CryptoVerifyCleartextResult + do { + cleartextResult = try verifyHandle.verifyCleartext(signatureData) + } catch { + OneKeyLog.error("BundleUpdate", "GPG verification error: \(error.localizedDescription)") + return nil + } + + // 5. Check signature error + do { + try cleartextResult.signatureError() + } catch { + OneKeyLog.error("BundleUpdate", "GPG signature invalid: \(error.localizedDescription)") + return nil + } + + // 6. Get cleartext + guard let cleartextData = cleartextResult.cleartext() else { + OneKeyLog.error("BundleUpdate", "Failed to extract cleartext from GPG result") + return nil + } + + guard let text = String(data: cleartextData, encoding: .utf8) else { + return nil + } + + // 7. Parse JSON and extract sha256 + guard let jsonData = text.data(using: .utf8), + let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any], + let sha256 = json["sha256"] as? String else { + OneKeyLog.error("BundleUpdate", "Failed to parse cleartext JSON") + return nil + } + + OneKeyLog.info("BundleUpdate", "GPG verification succeeded, sha256: \(sha256)") + return sha256 + } + + public static func validateMetadataFileSha256(_ currentBundleVersion: String, signature: String) -> Bool { + guard let metadataFilePath = getMetadataFilePath(currentBundleVersion) else { + OneKeyLog.debug("BundleUpdate", "metadataFilePath is null") + return false + } + guard let extractedSha256 = readMetadataFileSha256(signature), !extractedSha256.isEmpty else { + return false + } + guard let calculatedSha256 = calculateSHA256(metadataFilePath) else { return false } + return calculatedSha256.secureCompare(extractedSha256) + } + + public static func validateExtractedPathSafety(_ destination: String) -> Bool { + let fm = FileManager.default + let resolvedDestination = (destination as NSString).resolvingSymlinksInPath + + guard let enumerator = fm.enumerator(atPath: destination) else { return true } + while let file = enumerator.nextObject() as? String { + let fullPath = (destination as NSString).appendingPathComponent(file) + if let attrs = enumerator.fileAttributes, + attrs[.type] as? FileAttributeType == .typeSymbolicLink { + OneKeyLog.error("BundleUpdate", "Symlink detected in extracted bundle: \(file)") + return false + } + let resolvedPath = (fullPath as NSString).resolvingSymlinksInPath + if !resolvedPath.hasPrefix(resolvedDestination) { + OneKeyLog.error("BundleUpdate", "Path traversal detected in extracted bundle: \(file)") + return false + } + } + return true + } + + public static func validateAllFilesInDir(_ dirPath: String, metadata: [String: String], appVersion: String, bundleVersion: String) -> Bool { + let parentBundleDir = bundleDir() + let folderName = "\(appVersion)-\(bundleVersion)" + let jsBundleDir = (parentBundleDir as NSString).appendingPathComponent(folderName) + "/" + let fm = FileManager.default + + guard let enumerator = fm.enumerator(atPath: dirPath) else { return false } + while let file = enumerator.nextObject() as? String { + if file.contains("metadata.json") || file.contains(".DS_Store") { continue } + let fullPath = (dirPath as NSString).appendingPathComponent(file) + var isDir: ObjCBool = false + if fm.fileExists(atPath: fullPath, isDirectory: &isDir), isDir.boolValue { continue } + + let relativePath = fullPath.replacingOccurrences(of: jsBundleDir, with: "") + guard let expectedSHA256 = metadata[relativePath] else { + OneKeyLog.error("BundleUpdate", "[bundle-verify] File on disk not found in metadata: \(relativePath)") + return false + } + guard let actualSHA256 = calculateSHA256(fullPath) else { + OneKeyLog.error("BundleUpdate", "[bundle-verify] Failed to calculate SHA256 for file: \(relativePath)") + return false + } + if !expectedSHA256.secureCompare(actualSHA256) { + OneKeyLog.error("BundleUpdate", "[bundle-verify] SHA256 mismatch for \(relativePath)") + return false + } + } + + // Verify completeness + for key in metadata.keys { + let expectedFilePath = jsBundleDir + key + if !fm.fileExists(atPath: expectedFilePath) { + OneKeyLog.error("BundleUpdate", "[bundle-verify] File listed in metadata but missing on disk: \(key)") + return false + } + } + return true + } + + public static func currentBundleMainJSBundle() -> String? { + guard let currentBundleVer = currentBundleVersion() else { + OneKeyLog.warn("BundleUpdate", "getJsBundlePath: no currentBundleVersion stored") + return nil + } + + let currentAppVersion = getCurrentNativeVersion() + guard let prevNativeVersion = getNativeVersion() else { + OneKeyLog.warn("BundleUpdate", "getJsBundlePath: prevNativeVersion is nil") + return nil + } + + OneKeyLog.info("BundleUpdate", "currentAppVersion: \(currentAppVersion), currentBundleVersion: \(currentBundleVer), prevNativeVersion: \(prevNativeVersion)") + + if currentAppVersion != prevNativeVersion { + OneKeyLog.info("BundleUpdate", "currentAppVersion is not equal to prevNativeVersion \(currentAppVersion) \(prevNativeVersion)") + let ud = UserDefaults.standard + if let cbv = ud.string(forKey: "currentBundleVersion") { + deleteSignatureFile(cbv) + ud.removeObject(forKey: "currentBundleVersion") + } + ud.synchronize() + return nil + } + + guard let folderName = currentBundleDir(), + FileManager.default.fileExists(atPath: folderName) else { + OneKeyLog.warn("BundleUpdate", "getJsBundlePath: currentBundleDir does not exist") + return nil + } + + let signature = readSignatureFile(currentBundleVer) + OneKeyLog.debug("BundleUpdate", "getJsBundlePath: signatureLength=\(signature.count)") + + let devSettingsEnabled = isDevSettingsEnabled() + if devSettingsEnabled { + OneKeyLog.warn("BundleUpdate", "Startup SHA256 validation skipped (DevSettings enabled)") + } + if !devSettingsEnabled && !validateMetadataFileSha256(currentBundleVer, signature: signature) { + OneKeyLog.warn("BundleUpdate", "getJsBundlePath: validateMetadataFileSha256 failed, signatureLength=\(signature.count)") + return nil + } + + guard let metadata = getMetadataFileContent(currentBundleVer) else { + OneKeyLog.warn("BundleUpdate", "getJsBundlePath: getMetadataFileContent returned nil") + return nil + } + + if let dashRange = currentBundleVer.range(of: "-", options: .backwards) { + let appVer = String(currentBundleVer[currentBundleVer.startIndex.. String { + let path = (bundleDir() as NSString).appendingPathComponent("fallbackUpdateBundleData.json") + if !FileManager.default.fileExists(atPath: path) { + FileManager.default.createFile(atPath: path, contents: nil) + } + return path + } + + static func readFallbackUpdateBundleDataFile() -> [[String: String]] { + let path = getFallbackUpdateBundleDataPath() + guard let content = try? String(contentsOfFile: path, encoding: .utf8), + !content.isEmpty, + let data = content.data(using: .utf8), + let arr = try? JSONSerialization.jsonObject(with: data) as? [[String: String]] else { + return [] + } + return arr + } + + static func writeFallbackUpdateBundleDataFile(_ data: [[String: String]]) { + let path = getFallbackUpdateBundleDataPath() + guard let jsonData = try? JSONSerialization.data(withJSONObject: data), + let jsonString = String(data: jsonData, encoding: .utf8) else { return } + try? jsonString.write(toFile: path, atomically: true, encoding: .utf8) + } + + public static func clearUpdateBundleData() { + let bDir = bundleDir() + let fm = FileManager.default + if fm.fileExists(atPath: bDir) { + // This also deletes asc/ directory containing all signature files + try? fm.removeItem(atPath: bDir) + } + let ud = UserDefaults.standard + // Legacy cleanup: remove signature from UserDefaults if present + if let cbv = currentBundleVersion() { + ud.removeObject(forKey: cbv) + } + ud.removeObject(forKey: bundlePrefsKey) + ud.synchronize() + } +} + +// Constant-time string comparison to prevent timing attacks on hash comparisons +private extension String { + func secureCompare(_ other: String) -> Bool { + let lhs = Array(self.utf8) + let rhs = Array(other.utf8) + guard lhs.count == rhs.count else { return false } + var result: UInt8 = 0 + for i in 0.. Void)? + + /// Continuation to bridge delegate callbacks → async/await + private var continuation: CheckedContinuation<(URL, URLResponse), Error>? + private var tempFileURL: URL? + private let lock = NSLock() + + func setContinuation(_ cont: CheckedContinuation<(URL, URLResponse), Error>) { + lock.lock() + continuation = cont + lock.unlock() + } + + // MARK: - URLSessionDownloadDelegate + + private var prevProgress: Int = -1 + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { + guard totalBytesExpectedToWrite > 0 else { return } + let progress = Int((totalBytesWritten * 100) / totalBytesExpectedToWrite) + if progress != prevProgress { + OneKeyLog.info("BundleUpdate", "download progress: \(progress)% (\(totalBytesWritten)/\(totalBytesExpectedToWrite))") + prevProgress = progress + onProgress?(progress) + } + } + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + // Copy to a temp location because the file at `location` is deleted after this method returns + let tempPath = NSTemporaryDirectory() + UUID().uuidString + let dest = URL(fileURLWithPath: tempPath) + do { + try FileManager.default.copyItem(at: location, to: dest) + tempFileURL = dest + } catch { + OneKeyLog.error("BundleUpdate", "Failed to copy downloaded file: \(error)") + } + } + + // MARK: - URLSessionTaskDelegate + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + lock.lock() + let cont = continuation + continuation = nil + lock.unlock() + + if let error = error { + cont?.resume(throwing: error) + } else if let tempURL = tempFileURL, let response = task.response { + cont?.resume(returning: (tempURL, response)) + } else { + cont?.resume(throwing: NSError(domain: "BundleUpdate", code: -1, + userInfo: [NSLocalizedDescriptionKey: "Download completed without file"])) + } + } + + func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { + if request.url?.scheme?.lowercased() != "https" { + OneKeyLog.error("BundleUpdate", "Blocked redirect to non-HTTPS URL") + completionHandler(nil) + } else { + completionHandler(request) + } + } + + /// Reset state for reuse + func reset() { + lock.lock() + continuation = nil + lock.unlock() + tempFileURL = nil + onProgress = nil + prevProgress = -1 + } +} + +// Listener support +private struct BundleListener { + let id: Double + let callback: (BundleDownloadEvent) -> Void +} + +class ReactNativeBundleUpdate: HybridReactNativeBundleUpdateSpec { + + // Serial queue protects mutable state (listeners, nextListenerId, isDownloading) + private let stateQueue = DispatchQueue(label: "so.onekey.bundleupdate.state") + private var listeners: [BundleListener] = [] + private var nextListenerId: Double = 1 + private var isDownloading = false + private var urlSession: URLSession? + private var downloadDelegate: DownloadDelegate? + private var downloadFilePath: String? + private var downloadSha256: String? + + private func createURLSession() -> URLSession { + let config = URLSessionConfiguration.default + config.tlsMinimumSupportedProtocolVersion = .TLSv12 + let delegate = DownloadDelegate() + self.downloadDelegate = delegate + return URLSession(configuration: config, delegate: delegate, delegateQueue: nil) + } + + override init() { + super.init() + urlSession = createURLSession() + } + + private func sendEvent(type: String, progress: Int = 0, message: String = "") { + let event = BundleDownloadEvent(type: type, progress: Double(progress), message: message) + let currentListeners = stateQueue.sync { self.listeners } + for listener in currentListeners { + do { + listener.callback(event) + } catch { + OneKeyLog.error("BundleUpdate", "Error sending event: \(error)") + } + } + } + + func addDownloadListener(callback: @escaping (BundleDownloadEvent) -> Void) throws -> Double { + return stateQueue.sync { + let id = nextListenerId + nextListenerId += 1 + listeners.append(BundleListener(id: id, callback: callback)) + OneKeyLog.debug("BundleUpdate", "addDownloadListener: id=\(id), totalListeners=\(listeners.count)") + return id + } + } + + func removeDownloadListener(id: Double) throws { + stateQueue.sync { + listeners.removeAll { $0.id == id } + OneKeyLog.debug("BundleUpdate", "removeDownloadListener: id=\(id), totalListeners=\(listeners.count)") + } + } + + func downloadBundle(params: BundleDownloadParams) throws -> Promise { + return Promise.async { [weak self] in + guard let self = self else { throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Module deallocated"]) } + + let alreadyDownloading = self.stateQueue.sync { () -> Bool in + if self.isDownloading { return true } + self.isDownloading = true + return false + } + guard !alreadyDownloading else { + OneKeyLog.warn("BundleUpdate", "downloadBundle: rejected, already downloading") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Already downloading"]) + } + defer { self.stateQueue.sync { self.isDownloading = false } } + + let appVersion = params.latestVersion + let bundleVersion = params.bundleVersion + let downloadUrl = params.downloadUrl + let sha256 = params.sha256 + + OneKeyLog.info("BundleUpdate", "downloadBundle: appVersion=\(appVersion), bundleVersion=\(bundleVersion), fileSize=\(params.fileSize), url=\(downloadUrl)") + + guard appVersion.isSafeVersionString, bundleVersion.isSafeVersionString else { + OneKeyLog.error("BundleUpdate", "downloadBundle: invalid version string format: appVersion=\(appVersion), bundleVersion=\(bundleVersion)") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid version string format"]) + } + + guard downloadUrl.hasPrefix("https://") else { + OneKeyLog.error("BundleUpdate", "downloadBundle: URL is not HTTPS: \(downloadUrl)") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle download URL must use HTTPS"]) + } + + let fileName = "\(appVersion)-\(bundleVersion).zip" + let filePath = (BundleUpdateStore.downloadBundleDir() as NSString).appendingPathComponent(fileName) + + let result = BundleDownloadResult( + downloadedFile: filePath, + downloadUrl: downloadUrl, + latestVersion: appVersion, + bundleVersion: bundleVersion, + sha256: sha256 + ) + + OneKeyLog.info("BundleUpdate", "downloadBundle: filePath=\(filePath)") + + // Check if file already exists and is valid + if FileManager.default.fileExists(atPath: filePath) { + OneKeyLog.info("BundleUpdate", "downloadBundle: file already exists, verifying SHA256...") + if self.verifyBundleSHA256(filePath, sha256: sha256) { + OneKeyLog.info("BundleUpdate", "downloadBundle: existing file SHA256 valid, skipping download") + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in + self?.sendEvent(type: "update/complete") + } + return result + } else { + OneKeyLog.warn("BundleUpdate", "downloadBundle: existing file SHA256 mismatch, re-downloading") + try? FileManager.default.removeItem(atPath: filePath) + } + } + + // Download the file + guard let url = URL(string: downloadUrl) else { + OneKeyLog.error("BundleUpdate", "downloadBundle: invalid URL: \(downloadUrl)") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]) + } + + guard let session = self.urlSession else { + OneKeyLog.error("BundleUpdate", "downloadBundle: URLSession not initialized") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "URLSession not initialized"]) + } + + self.sendEvent(type: "update/start") + OneKeyLog.info("BundleUpdate", "downloadBundle: starting download...") + + let request = URLRequest(url: url) + + // Use delegate-based download for real progress reporting + guard let delegate = self.downloadDelegate else { + OneKeyLog.error("BundleUpdate", "downloadBundle: download delegate not initialized") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Download delegate not initialized"]) + } + delegate.reset() + delegate.onProgress = { [weak self] progress in + self?.sendEvent(type: "update/downloading", progress: progress) + } + + let (tempURL, response) = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<(URL, URLResponse), Error>) in + delegate.setContinuation(continuation) + let task = session.downloadTask(with: request) + task.resume() + } + + // Verify HTTPS was maintained (no HTTP redirect) + if let httpResponse = response as? HTTPURLResponse, + let responseUrl = httpResponse.url, + responseUrl.scheme?.lowercased() != "https" { + OneKeyLog.error("BundleUpdate", "downloadBundle: redirected to non-HTTPS URL: \(responseUrl)") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Download was redirected to non-HTTPS URL"]) + } + + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1 + OneKeyLog.error("BundleUpdate", "downloadBundle: HTTP error, statusCode=\(statusCode)") + self.sendEvent(type: "update/error", message: "HTTP error \(statusCode)") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Download failed with HTTP \(statusCode)"]) + } + + OneKeyLog.info("BundleUpdate", "downloadBundle: download finished, HTTP 200, moving to destination...") + + // Move downloaded file to destination + let destDir = (filePath as NSString).deletingLastPathComponent + if !FileManager.default.fileExists(atPath: destDir) { + try FileManager.default.createDirectory(atPath: destDir, withIntermediateDirectories: true) + } + if FileManager.default.fileExists(atPath: filePath) { + try FileManager.default.removeItem(atPath: filePath) + } + try FileManager.default.moveItem(at: tempURL, to: URL(fileURLWithPath: filePath)) + + // Verify SHA256 + OneKeyLog.info("BundleUpdate", "downloadBundle: verifying SHA256...") + if !self.verifyBundleSHA256(filePath, sha256: sha256) { + try? FileManager.default.removeItem(atPath: filePath) + OneKeyLog.error("BundleUpdate", "downloadBundle: SHA256 verification failed after download") + self.sendEvent(type: "update/error", message: "Bundle signature verification failed") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle signature verification failed"]) + } + + self.sendEvent(type: "update/complete") + OneKeyLog.info("BundleUpdate", "downloadBundle: completed successfully, appVersion=\(appVersion), bundleVersion=\(bundleVersion)") + return result + } + } + + private func verifyBundleSHA256(_ bundlePath: String, sha256: String) -> Bool { + guard let calculated = BundleUpdateStore.calculateSHA256(bundlePath) else { + OneKeyLog.error("BundleUpdate", "verifyBundleSHA256: failed to calculate SHA256 for: \(bundlePath)") + return false + } + let isValid = calculated.secureCompare(sha256) + OneKeyLog.debug("BundleUpdate", "verifyBundleSHA256: path=\(bundlePath), expected=\(sha256.prefix(16))..., calculated=\(calculated.prefix(16))..., valid=\(isValid)") + return isValid + } + + func downloadBundleASC(params: BundleDownloadASCParams) throws -> Promise { + return Promise.async { + let appVersion = params.latestVersion + let bundleVersion = params.bundleVersion + let signature = params.signature + + OneKeyLog.info("BundleUpdate", "downloadBundleASC: appVersion=\(appVersion), bundleVersion=\(bundleVersion), signatureLength=\(signature.count)") + + let storageKey = "\(appVersion)-\(bundleVersion)" + BundleUpdateStore.writeSignatureFile(storageKey, signature: signature) + + OneKeyLog.info("BundleUpdate", "downloadBundleASC: stored signature for key=\(storageKey)") + } + } + + func verifyBundleASC(params: BundleVerifyASCParams) throws -> Promise { + return Promise.async { + let filePath = params.downloadedFile + let sha256 = params.sha256 + let appVersion = params.latestVersion + let bundleVersion = params.bundleVersion + let signature = params.signature + + OneKeyLog.info("BundleUpdate", "verifyBundleASC: appVersion=\(appVersion), bundleVersion=\(bundleVersion), file=\(filePath), signatureLength=\(signature.count)") + + // GPG verification skipped only when both DevSettings and skip-GPG toggle are enabled + let devSettings = BundleUpdateStore.isDevSettingsEnabled() + let skipGPGToggle = BundleUpdateStore.isSkipGPGEnabled() + let skipGPG = devSettings && skipGPGToggle + OneKeyLog.info("BundleUpdate", "verifyBundleASC: GPG check: devSettings=\(devSettings), skipGPGToggle=\(skipGPGToggle), skipGPG=\(skipGPG)") + + if !skipGPG { + OneKeyLog.info("BundleUpdate", "verifyBundleASC: verifying SHA256 of downloaded file...") + guard let calculated = BundleUpdateStore.calculateSHA256(filePath), + calculated.secureCompare(sha256) else { + OneKeyLog.error("BundleUpdate", "verifyBundleASC: SHA256 verification failed for file=\(filePath)") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle signature verification failed"]) + } + OneKeyLog.info("BundleUpdate", "verifyBundleASC: SHA256 verified OK") + } else { + OneKeyLog.warn("BundleUpdate", "verifyBundleASC: SHA256 + GPG verification skipped (DevSettings enabled)") + } + + let folderName = "\(appVersion)-\(bundleVersion)" + let destination = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName) + + // Check zip file size before extraction (decompression bomb protection) + let maxZipFileSize: UInt64 = 512 * 1024 * 1024 // 512 MB + if let attrs = try? FileManager.default.attributesOfItem(atPath: filePath), + let fileSize = attrs[.size] as? UInt64 { + OneKeyLog.info("BundleUpdate", "verifyBundleASC: zip file size=\(fileSize) bytes") + if fileSize > maxZipFileSize { + OneKeyLog.error("BundleUpdate", "verifyBundleASC: zip file too large (\(fileSize) > \(maxZipFileSize))") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Zip file exceeds maximum allowed size"]) + } + } + + // Unzip using SSZipArchive + OneKeyLog.info("BundleUpdate", "verifyBundleASC: extracting zip to \(destination)...") + do { + try SSZipArchive.unzipFile(atPath: filePath, toDestination: destination, overwrite: true, password: nil) + OneKeyLog.info("BundleUpdate", "verifyBundleASC: extraction completed") + } catch { + OneKeyLog.error("BundleUpdate", "verifyBundleASC: unzip failed: \(error.localizedDescription)") + try? FileManager.default.removeItem(atPath: destination) + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to unzip bundle: \(error.localizedDescription)"]) + } + + // Validate extracted paths (symlinks, path traversal) + OneKeyLog.info("BundleUpdate", "verifyBundleASC: validating extracted path safety...") + if !BundleUpdateStore.validateExtractedPathSafety(destination) { + OneKeyLog.error("BundleUpdate", "verifyBundleASC: path traversal or symlink attack detected") + try? FileManager.default.removeItem(atPath: destination) + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Path traversal or symlink attack detected"]) + } + + let metadataJsonPath = (destination as NSString).appendingPathComponent("metadata.json") + guard FileManager.default.fileExists(atPath: metadataJsonPath) else { + OneKeyLog.error("BundleUpdate", "verifyBundleASC: metadata.json not found after extraction") + try? FileManager.default.removeItem(atPath: destination) + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to read metadata.json"]) + } + + let currentBundleVersion = "\(appVersion)-\(bundleVersion)" + if !skipGPG { + OneKeyLog.info("BundleUpdate", "verifyBundleASC: validating GPG signature for metadata...") + if !BundleUpdateStore.validateMetadataFileSha256(currentBundleVersion, signature: signature) { + OneKeyLog.error("BundleUpdate", "verifyBundleASC: GPG signature verification failed") + try? FileManager.default.removeItem(atPath: destination) + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle signature verification failed"]) + } + OneKeyLog.info("BundleUpdate", "verifyBundleASC: GPG signature verified OK") + } else { + OneKeyLog.warn("BundleUpdate", "verifyBundleASC: GPG verification skipped (DevSettings enabled)") + } + + OneKeyLog.info("BundleUpdate", "verifyBundleASC: validating all extracted files against metadata...") + guard let metadata = BundleUpdateStore.getMetadataFileContent(currentBundleVersion) else { + OneKeyLog.error("BundleUpdate", "verifyBundleASC: failed to read metadata.json content") + try? FileManager.default.removeItem(atPath: destination) + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to read metadata.json after extraction"]) + } + + if !BundleUpdateStore.validateAllFilesInDir(destination, metadata: metadata, appVersion: appVersion, bundleVersion: bundleVersion) { + OneKeyLog.error("BundleUpdate", "verifyBundleASC: file integrity check failed") + try? FileManager.default.removeItem(atPath: destination) + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Extracted files verification against metadata failed"]) + } + + OneKeyLog.info("BundleUpdate", "verifyBundleASC: all verifications passed, appVersion=\(appVersion), bundleVersion=\(bundleVersion)") + } + } + + func verifyBundle(params: BundleVerifyParams) throws -> Promise { + return Promise.async { + let filePath = params.downloadedFile + let sha256 = params.sha256 + let appVersion = params.latestVersion + let bundleVersion = params.bundleVersion + + OneKeyLog.info("BundleUpdate", "verifyBundle: appVersion=\(appVersion), bundleVersion=\(bundleVersion), file=\(filePath)") + + // Verify SHA256 of the downloaded file + guard let calculated = BundleUpdateStore.calculateSHA256(filePath) else { + OneKeyLog.error("BundleUpdate", "verifyBundle: failed to calculate SHA256 for file=\(filePath)") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to calculate SHA256"]) + } + guard calculated.secureCompare(sha256) else { + OneKeyLog.error("BundleUpdate", "verifyBundle: SHA256 mismatch, expected=\(sha256.prefix(16))..., got=\(calculated.prefix(16))...") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "SHA256 verification failed"]) + } + + OneKeyLog.info("BundleUpdate", "verifyBundle: SHA256 verified OK for appVersion=\(appVersion), bundleVersion=\(bundleVersion)") + } + } + + func installBundle(params: BundleInstallParams) throws -> Promise { + return Promise.async { + let appVersion = params.latestVersion + let bundleVersion = params.bundleVersion + let signature = params.signature + + OneKeyLog.info("BundleUpdate", "installBundle: appVersion=\(appVersion), bundleVersion=\(bundleVersion), signatureLength=\(signature.count)") + + // GPG verification skipped only when both DevSettings and skip-GPG toggle are enabled + let devSettings = BundleUpdateStore.isDevSettingsEnabled() + let skipGPGToggle = BundleUpdateStore.isSkipGPGEnabled() + let skipGPG = devSettings && skipGPGToggle + OneKeyLog.info("BundleUpdate", "installBundle: GPG check: devSettings=\(devSettings), skipGPGToggle=\(skipGPGToggle), skipGPG=\(skipGPG)") + + let folderName = "\(appVersion)-\(bundleVersion)" + let currentFolderName = BundleUpdateStore.currentBundleVersion() + OneKeyLog.info("BundleUpdate", "installBundle: target=\(folderName), current=\(currentFolderName ?? "nil")") + + // Verify bundle directory exists + let bundleDirPath = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName) + guard FileManager.default.fileExists(atPath: bundleDirPath) else { + OneKeyLog.error("BundleUpdate", "installBundle: bundle directory not found: \(bundleDirPath)") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle directory not found: \(folderName)"]) + } + + let ud = UserDefaults.standard + ud.set(folderName, forKey: "currentBundleVersion") + if !signature.isEmpty { + BundleUpdateStore.writeSignatureFile(folderName, signature: signature) + } + let currentNativeVersion = BundleUpdateStore.getCurrentNativeVersion() + BundleUpdateStore.setNativeVersion(currentNativeVersion) + ud.synchronize() + + // Manage fallback data + var fallbackData = BundleUpdateStore.readFallbackUpdateBundleDataFile() + + if let current = currentFolderName, + let dashRange = current.range(of: "-", options: .backwards) { + let curAppVersion = String(current[current.startIndex.. 3 { + let shifted = fallbackData.removeFirst() + if let shiftApp = shifted["appVersion"], let shiftBundle = shifted["bundleVersion"] { + let dirName = "\(shiftApp)-\(shiftBundle)" + BundleUpdateStore.deleteSignatureFile(dirName) + let oldPath = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(dirName) + if FileManager.default.fileExists(atPath: oldPath) { + try? FileManager.default.removeItem(atPath: oldPath) + } + } + } + + BundleUpdateStore.writeFallbackUpdateBundleDataFile(fallbackData) + ud.synchronize() + + OneKeyLog.info("BundleUpdate", "installBundle: completed successfully, installed version=\(folderName), fallbackCount=\(fallbackData.count)") + } + } + + func clearBundle() throws -> Promise { + return Promise.async { [weak self] in + OneKeyLog.info("BundleUpdate", "clearBundle: clearing download directory and cancelling downloads...") + let downloadDir = BundleUpdateStore.downloadBundleDir() + if FileManager.default.fileExists(atPath: downloadDir) { + try FileManager.default.removeItem(atPath: downloadDir) + OneKeyLog.info("BundleUpdate", "clearBundle: download directory deleted") + } else { + OneKeyLog.info("BundleUpdate", "clearBundle: download directory does not exist, skipping") + } + // Cancel all in-flight downloads by invalidating the session + self?.urlSession?.invalidateAndCancel() + self?.urlSession = self?.createURLSession() + self?.stateQueue.sync { self?.isDownloading = false } + OneKeyLog.info("BundleUpdate", "clearBundle: completed") + } + } + + func clearAllJSBundleData() throws -> Promise { + return Promise.async { + OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: starting...") + let bundleDir = BundleUpdateStore.bundleDir() + if FileManager.default.fileExists(atPath: bundleDir) { + try FileManager.default.removeItem(atPath: bundleDir) + OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: deleted bundle dir") + } + let ud = UserDefaults.standard + if let cbv = ud.string(forKey: "currentBundleVersion") { + ud.removeObject(forKey: cbv) + ud.removeObject(forKey: "currentBundleVersion") + OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: removed currentBundleVersion=\(cbv)") + } + ud.removeObject(forKey: "nativeVersion") + ud.synchronize() + + OneKeyLog.info("BundleUpdate", "clearAllJSBundleData: completed successfully") + return TestResult(success: true, message: "Successfully cleared all JS bundle data") + } + } + + func getFallbackUpdateBundleData() throws -> Promise<[FallbackBundleInfo]> { + return Promise.async { + let data = BundleUpdateStore.readFallbackUpdateBundleDataFile() + let result = data.compactMap { dict -> FallbackBundleInfo? in + guard let appVersion = dict["appVersion"], + let bundleVersion = dict["bundleVersion"], + let signature = dict["signature"] else { return nil } + return FallbackBundleInfo(appVersion: appVersion, bundleVersion: bundleVersion, signature: signature) + } + OneKeyLog.info("BundleUpdate", "getFallbackUpdateBundleData: found \(result.count) fallback entries") + return result + } + } + + func setCurrentUpdateBundleData(params: BundleSwitchParams) throws -> Promise { + return Promise.async { + let bundleVersion = "\(params.appVersion)-\(params.bundleVersion)" + OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: switching to \(bundleVersion)") + + // Verify the bundle directory actually exists + let bundleDirPath = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(bundleVersion) + guard FileManager.default.fileExists(atPath: bundleDirPath) else { + OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: bundle directory not found: \(bundleDirPath)") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle directory not found"]) + } + + // Verify GPG signature is valid (skipped when both DevSettings and skip-GPG toggle are enabled) + let devSettings = BundleUpdateStore.isDevSettingsEnabled() + let skipGPGToggle = BundleUpdateStore.isSkipGPGEnabled() + OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: GPG check: devSettings=\(devSettings), skipGPGToggle=\(skipGPGToggle)") + if !(devSettings && skipGPGToggle) { + guard !params.signature.isEmpty, + BundleUpdateStore.validateMetadataFileSha256(bundleVersion, signature: params.signature) else { + OneKeyLog.error("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verification failed") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle signature verification failed"]) + } + OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verified OK") + } else { + OneKeyLog.warn("BundleUpdate", "setCurrentUpdateBundleData: GPG signature verification skipped (DevSettings + skip-GPG enabled)") + } + + let ud = UserDefaults.standard + ud.set(bundleVersion, forKey: "currentBundleVersion") + if !params.signature.isEmpty { + BundleUpdateStore.writeSignatureFile(bundleVersion, signature: params.signature) + } + ud.synchronize() + OneKeyLog.info("BundleUpdate", "setCurrentUpdateBundleData: switched to \(bundleVersion)") + } + } + + func getWebEmbedPath() throws -> String { + let path = BundleUpdateStore.getWebEmbedPath() + OneKeyLog.debug("BundleUpdate", "getWebEmbedPath: \(path)") + return path + } + + func getWebEmbedPathAsync() throws -> Promise { + return Promise.async { + let path = BundleUpdateStore.getWebEmbedPath() + OneKeyLog.debug("BundleUpdate", "getWebEmbedPathAsync: \(path)") + return path + } + } + + func getJsBundlePath() throws -> Promise { + return Promise.async { + let path = BundleUpdateStore.currentBundleMainJSBundle() ?? "" + OneKeyLog.info("BundleUpdate", "getJsBundlePath: \(path.isEmpty ? "(empty/no bundle)" : path)") + return path + } + } + + func getNativeAppVersion() throws -> Promise { + return Promise.async { + let version = BundleUpdateStore.getCurrentNativeVersion() + OneKeyLog.info("BundleUpdate", "getNativeAppVersion: \(version)") + return version + } + } + + func testVerification() throws -> Promise { + return Promise.async { + let testSignature = """ + -----BEGIN PGP SIGNED MESSAGE----- + Hash: SHA256 + + { + "fileName": "metadata.json", + "sha256": "2ada9c871104fc40649fa3de67a7d8e33faadc18e9abd587e8bb85be0a003eba", + "size": 158590, + "generatedAt": "2025-09-19T07:49:13.000Z" + } + -----BEGIN PGP SIGNATURE----- + + iQJCBAEBCAAsFiEE62iuVE8f3YzSZGJPs2mmepC/OHsFAmjNJ1IOHGRldkBvbmVr + ZXkuc28ACgkQs2mmepC/OHs6Rw/9FKHl5aNsE7V0IsFf/l+h16BYKFwVsL69alMk + CFLna8oUn0+tyECF6wKBKw5pHo5YR27o2pJfYbAER6dygDF6WTZ1lZdf5QcBMjGA + LCeXC0hzUBzSSOH4bKBTa3fHp//HdSV1F2OnkymbXqYN7WXvuQPLZ0nV6aU88hCk + HgFifcvkXAnWKoosUtj0Bban/YBRyvmQ5C2akxUPEkr4Yck1QXwzJeNRd7wMXHjH + JFK6lJcuABiB8wpJDXJkFzKs29pvHIK2B2vdOjU2rQzKOUwaKHofDi5C4+JitT2b + 2pSeYP3PAxXYw6XDOmKTOiC7fPnfLjtcPjNYNFCezVKZT6LKvZW9obnW8Q9LNJ4W + okMPgHObkabv3OqUaTA9QNVfI/X9nvggzlPnaKDUrDWTf7n3vlrdexugkLtV/tJA + uguPlI5hY7Ue5OW7ckWP46hfmq1+UaIdeUY7dEO+rPZDz6KcArpaRwBiLPBhneIr + /X3KuMzS272YbPbavgCZGN9xJR5kZsEQE5HhPCbr6Nf0qDnh+X8mg0tAB/U6F+ZE + o90sJL1ssIaYvST+VWVaGRr4V5nMDcgHzWSF9Q/wm22zxe4alDaBdvOlUseW0iaM + n2DMz6gqk326W6SFynYtvuiXo7wG4Cmn3SuIU8xfv9rJqunpZGYchMd7nZektmEJ + 91Js0rQ= + =A/Ii + -----END PGP SIGNATURE----- + """ + let result = BundleUpdateStore.verifyGPGAndExtractSha256(testSignature) + let isValid = result == "2ada9c871104fc40649fa3de67a7d8e33faadc18e9abd587e8bb85be0a003eba" + OneKeyLog.info("BundleUpdate", "testVerification: GPG verification result: \(isValid)") + return isValid + } + } + + func isBundleExists(appVersion: String, bundleVersion: String) throws -> Promise { + return Promise.async { + let folderName = "\(appVersion)-\(bundleVersion)" + let path = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName) + let exists = FileManager.default.fileExists(atPath: path) + OneKeyLog.info("BundleUpdate", "isBundleExists: appVersion=\(appVersion), bundleVersion=\(bundleVersion), exists=\(exists)") + return exists + } + } + + func verifyExtractedBundle(appVersion: String, bundleVersion: String) throws -> Promise { + return Promise.async { + OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: appVersion=\(appVersion), bundleVersion=\(bundleVersion)") + let folderName = "\(appVersion)-\(bundleVersion)" + let bundlePath = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName) + guard FileManager.default.fileExists(atPath: bundlePath) else { + OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: bundle directory not found: \(bundlePath)") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bundle directory not found"]) + } + let metadataJsonPath = (bundlePath as NSString).appendingPathComponent("metadata.json") + guard FileManager.default.fileExists(atPath: metadataJsonPath) else { + OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: metadata.json not found in \(bundlePath)") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "metadata.json not found"]) + } + guard let data = FileManager.default.contents(atPath: metadataJsonPath), + let metadata = try? JSONSerialization.jsonObject(with: data) as? [String: String] else { + OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: failed to parse metadata.json") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to parse metadata.json"]) + } + OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: parsing metadata and validating files...") + if !BundleUpdateStore.validateAllFilesInDir(bundlePath, metadata: metadata, appVersion: appVersion, bundleVersion: bundleVersion) { + OneKeyLog.error("BundleUpdate", "verifyExtractedBundle: file integrity check failed") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "File integrity check failed"]) + } + OneKeyLog.info("BundleUpdate", "verifyExtractedBundle: all files verified OK, fileCount=\(metadata.count)") + } + } + + func listLocalBundles() throws -> Promise<[LocalBundleInfo]> { + return Promise.async { + let bundleDir = BundleUpdateStore.bundleDir() + let fm = FileManager.default + guard let contents = try? fm.contentsOfDirectory(atPath: bundleDir) else { + OneKeyLog.info("BundleUpdate", "listLocalBundles: bundle directory empty or not found") + return [] + } + var results: [LocalBundleInfo] = [] + for name in contents { + let fullPath = (bundleDir as NSString).appendingPathComponent(name) + var isDir: ObjCBool = false + guard fm.fileExists(atPath: fullPath, isDirectory: &isDir), isDir.boolValue else { continue } + guard let lastDash = name.range(of: "-", options: .backwards), + lastDash.lowerBound > name.startIndex else { continue } + let appVersion = String(name[name.startIndex.. Promise<[AscFileInfo]> { + return Promise.async { + let ascDir = BundleUpdateStore.ascDir() + let fm = FileManager.default + guard let contents = try? fm.contentsOfDirectory(atPath: ascDir) else { + OneKeyLog.info("BundleUpdate", "listAscFiles: asc directory empty or not found") + return [] + } + var results: [AscFileInfo] = [] + for name in contents { + let fullPath = (ascDir as NSString).appendingPathComponent(name) + var isDir: ObjCBool = false + guard fm.fileExists(atPath: fullPath, isDirectory: &isDir), !isDir.boolValue else { continue } + let attrs = try? fm.attributesOfItem(atPath: fullPath) + let fileSize = Double((attrs?[.size] as? UInt64) ?? 0) + results.append(AscFileInfo(fileName: name, filePath: fullPath, fileSize: fileSize)) + } + OneKeyLog.info("BundleUpdate", "listAscFiles: found \(results.count) files") + return results + } + } + + func getSha256FromFilePath(filePath: String) throws -> Promise { + return Promise.async { + OneKeyLog.info("BundleUpdate", "getSha256FromFilePath: filePath=\(filePath)") + guard !filePath.isEmpty else { + OneKeyLog.warn("BundleUpdate", "getSha256FromFilePath: empty filePath") + return "" + } + + // Restrict to bundle-related directories only + let resolvedPath = (filePath as NSString).resolvingSymlinksInPath + let bundleDir = (BundleUpdateStore.bundleDir() as NSString).resolvingSymlinksInPath + let downloadDir = (BundleUpdateStore.downloadBundleDir() as NSString).resolvingSymlinksInPath + guard resolvedPath.hasPrefix(bundleDir) || resolvedPath.hasPrefix(downloadDir) else { + OneKeyLog.error("BundleUpdate", "getSha256FromFilePath: path outside allowed directories: \(resolvedPath)") + throw NSError(domain: "BundleUpdate", code: -1, userInfo: [NSLocalizedDescriptionKey: "File path outside allowed bundle directories"]) + } + + let sha256 = BundleUpdateStore.calculateSHA256(filePath) ?? "" + OneKeyLog.info("BundleUpdate", "getSha256FromFilePath: sha256=\(sha256.isEmpty ? "(empty)" : String(sha256.prefix(16)) + "...")") + return sha256 + } + } + + func testDeleteJsBundle(appVersion: String, bundleVersion: String) throws -> Promise { + return Promise.async { + OneKeyLog.info("BundleUpdate", "testDeleteJsBundle: appVersion=\(appVersion), bundleVersion=\(bundleVersion)") + let folderName = "\(appVersion)-\(bundleVersion)" + let jsBundlePath = (BundleUpdateStore.bundleDir() as NSString) + .appendingPathComponent(folderName) + let path = (jsBundlePath as NSString).appendingPathComponent("main.jsbundle.hbc") + + if FileManager.default.fileExists(atPath: path) { + try FileManager.default.removeItem(atPath: path) + OneKeyLog.info("BundleUpdate", "testDeleteJsBundle: deleted \(path)") + return TestResult(success: true, message: "Deleted jsBundle: \(path)") + } + OneKeyLog.warn("BundleUpdate", "testDeleteJsBundle: file not found: \(path)") + return TestResult(success: false, message: "jsBundle not found: \(path)") + } + } + + func testDeleteJsRuntimeDir(appVersion: String, bundleVersion: String) throws -> Promise { + return Promise.async { + OneKeyLog.info("BundleUpdate", "testDeleteJsRuntimeDir: appVersion=\(appVersion), bundleVersion=\(bundleVersion)") + let folderName = "\(appVersion)-\(bundleVersion)" + let dirPath = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName) + + if FileManager.default.fileExists(atPath: dirPath) { + try FileManager.default.removeItem(atPath: dirPath) + OneKeyLog.info("BundleUpdate", "testDeleteJsRuntimeDir: deleted \(dirPath)") + return TestResult(success: true, message: "Deleted js runtime directory: \(dirPath)") + } + OneKeyLog.warn("BundleUpdate", "testDeleteJsRuntimeDir: directory not found: \(dirPath)") + return TestResult(success: false, message: "js runtime directory not found: \(dirPath)") + } + } + + func testDeleteMetadataJson(appVersion: String, bundleVersion: String) throws -> Promise { + return Promise.async { + OneKeyLog.info("BundleUpdate", "testDeleteMetadataJson: appVersion=\(appVersion), bundleVersion=\(bundleVersion)") + let folderName = "\(appVersion)-\(bundleVersion)" + let metadataPath = (BundleUpdateStore.bundleDir() as NSString) + .appendingPathComponent(folderName) + let path = (metadataPath as NSString).appendingPathComponent("metadata.json") + + if FileManager.default.fileExists(atPath: path) { + try FileManager.default.removeItem(atPath: path) + OneKeyLog.info("BundleUpdate", "testDeleteMetadataJson: deleted \(path)") + return TestResult(success: true, message: "Deleted metadata.json: \(path)") + } + OneKeyLog.warn("BundleUpdate", "testDeleteMetadataJson: file not found: \(path)") + return TestResult(success: false, message: "metadata.json not found: \(path)") + } + } + + func testWriteEmptyMetadataJson(appVersion: String, bundleVersion: String) throws -> Promise { + return Promise.async { + OneKeyLog.info("BundleUpdate", "testWriteEmptyMetadataJson: appVersion=\(appVersion), bundleVersion=\(bundleVersion)") + let folderName = "\(appVersion)-\(bundleVersion)" + let jsRuntimeDir = (BundleUpdateStore.bundleDir() as NSString).appendingPathComponent(folderName) + let metadataPath = (jsRuntimeDir as NSString).appendingPathComponent("metadata.json") + + if !FileManager.default.fileExists(atPath: jsRuntimeDir) { + try FileManager.default.createDirectory(atPath: jsRuntimeDir, withIntermediateDirectories: true) + } + + let emptyJson: [String: Any] = [:] + let data = try JSONSerialization.data(withJSONObject: emptyJson, options: .prettyPrinted) + try data.write(to: URL(fileURLWithPath: metadataPath), options: .atomic) + + OneKeyLog.info("BundleUpdate", "testWriteEmptyMetadataJson: created \(metadataPath)") + return TestResult(success: true, message: "Created empty metadata.json: \(metadataPath)") + } + } +} diff --git a/native-modules/react-native-bundle-update/lefthook.yml b/native-modules/react-native-bundle-update/lefthook.yml new file mode 100644 index 00000000..89766b1f --- /dev/null +++ b/native-modules/react-native-bundle-update/lefthook.yml @@ -0,0 +1,10 @@ +pre-commit: + parallel: true + commands: + lint: + glob: "*.{js,ts,tsx}" + run: yarn lint {staged_files} + typecheck: + files: git diff --cached --name-only --diff-filter=ACMRTUXB + glob: "*.{js,ts,tsx}" + run: yarn typecheck diff --git a/native-modules/react-native-bundle-update/nitro.json b/native-modules/react-native-bundle-update/nitro.json new file mode 100644 index 00000000..33d405cd --- /dev/null +++ b/native-modules/react-native-bundle-update/nitro.json @@ -0,0 +1,17 @@ +{ + "cxxNamespace": ["reactnativebundleupdate"], + "ios": { + "iosModuleName": "ReactNativeBundleUpdate" + }, + "android": { + "androidNamespace": ["reactnativebundleupdate"], + "androidCxxLibName": "reactnativebundleupdate" + }, + "autolinking": { + "ReactNativeBundleUpdate": { + "swift": "ReactNativeBundleUpdate", + "kotlin": "ReactNativeBundleUpdate" + } + }, + "ignorePaths": ["node_modules"] +} diff --git a/native-modules/react-native-bundle-update/package.json b/native-modules/react-native-bundle-update/package.json new file mode 100644 index 00000000..faaa9b80 --- /dev/null +++ b/native-modules/react-native-bundle-update/package.json @@ -0,0 +1,169 @@ +{ + "name": "@onekeyfe/react-native-bundle-update", + "version": "1.1.24", + "description": "react-native-bundle-update", + "main": "./lib/module/index.js", + "types": "./lib/typescript/src/index.d.ts", + "exports": { + ".": { + "source": "./src/index.tsx", + "types": "./lib/typescript/src/index.d.ts", + "default": "./lib/module/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "src", + "lib", + "android", + "ios", + "cpp", + "nitrogen", + "nitro.json", + "*.podspec", + "react-native.config.js", + "!ios/build", + "!android/build", + "!android/gradle", + "!android/gradlew", + "!android/gradlew.bat", + "!android/local.properties", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__", + "!**/.*" + ], + "scripts": { + "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", + "prepare": "bob build", + "nitrogen": "nitrogen", + "typecheck": "tsc", + "lint": "eslint \"**/*.{js,ts,tsx}\"", + "test": "jest", + "release": "yarn prepare && npm whoami && npm publish --access public" + }, + "keywords": [ + "react-native", + "ios", + "android" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/OneKeyHQ/app-modules/react-native-bundle-update.git" + }, + "author": "onekeyfe (https://github.com/OneKeyHQ/app-modules)", + "license": "MIT", + "bugs": { + "url": "https://github.com/OneKeyHQ/app-modules/react-native-bundle-update/issues" + }, + "homepage": "https://github.com/OneKeyHQ/app-modules/react-native-bundle-update#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "devDependencies": { + "@commitlint/config-conventional": "^19.8.1", + "@eslint/compat": "^1.3.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.35.0", + "@react-native/babel-preset": "0.83.0", + "@react-native/eslint-config": "0.83.0", + "@release-it/conventional-changelog": "^10.0.1", + "@types/jest": "^29.5.14", + "@types/react": "^19.2.0", + "commitlint": "^19.8.1", + "del-cli": "^6.0.0", + "eslint": "^9.35.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "jest": "^29.7.0", + "lefthook": "^2.0.3", + "nitrogen": "0.31.10", + "prettier": "^2.8.8", + "react": "19.2.0", + "react-native": "0.83.0", + "react-native-builder-bob": "^0.40.13", + "react-native-nitro-modules": "0.33.2", + "release-it": "^19.0.4", + "turbo": "^2.5.6", + "typescript": "^5.9.2" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-nitro-modules": "0.33.2" + }, + "react-native-builder-bob": { + "source": "src", + "output": "lib", + "targets": [ + [ + "custom", + { + "script": "nitrogen", + "clean": "nitrogen/" + } + ], + [ + "module", + { + "esm": true + } + ], + [ + "typescript", + { + "project": "tsconfig.build.json" + } + ] + ] + }, + "prettier": { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + }, + "jest": { + "preset": "react-native", + "modulePathIgnorePatterns": [ + "/example/node_modules", + "/lib/" + ] + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "release-it": { + "git": { + "commitMessage": "chore: release ${version}", + "tagName": "v${version}" + }, + "npm": { + "publish": true + }, + "github": { + "release": true + }, + "plugins": { + "@release-it/conventional-changelog": { + "preset": { + "name": "angular" + } + } + } + }, + "create-react-native-library": { + "type": "nitro-module", + "languages": "kotlin-swift", + "tools": [ + "eslint", + "jest", + "lefthook", + "release-it" + ], + "version": "0.56.0" + } +} diff --git a/native-modules/react-native-bundle-update/src/ReactNativeBundleUpdate.nitro.ts b/native-modules/react-native-bundle-update/src/ReactNativeBundleUpdate.nitro.ts new file mode 100644 index 00000000..a7b06f69 --- /dev/null +++ b/native-modules/react-native-bundle-update/src/ReactNativeBundleUpdate.nitro.ts @@ -0,0 +1,143 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +export interface BundleDownloadParams { + downloadUrl: string; + latestVersion: string; + bundleVersion: string; + fileSize: number; + sha256: string; +} + +export interface BundleDownloadResult { + downloadedFile: string; + downloadUrl: string; + latestVersion: string; + bundleVersion: string; + sha256: string; +} + +export interface BundleVerifyParams { + downloadedFile: string; + sha256: string; + latestVersion: string; + bundleVersion: string; +} + +export interface BundleVerifyASCParams { + downloadedFile: string; + sha256: string; + latestVersion: string; + bundleVersion: string; + signature: string; +} + +export interface BundleDownloadASCParams { + downloadUrl: string; + downloadedFile: string; + signature: string; + latestVersion: string; + bundleVersion: string; + sha256: string; +} + +export interface BundleInstallParams { + downloadedFile: string; + latestVersion: string; + bundleVersion: string; + signature: string; +} + +export interface BundleSwitchParams { + appVersion: string; + bundleVersion: string; + signature: string; +} + +export interface BundleDownloadEvent { + type: string; + progress: number; + message: string; +} + +export interface TestResult { + success: boolean; + message: string; +} + +export interface LocalBundleInfo { + appVersion: string; + bundleVersion: string; +} + +export interface FallbackBundleInfo { + appVersion: string; + bundleVersion: string; + signature: string; +} + +export interface AscFileInfo { + fileName: string; + filePath: string; + fileSize: number; +} + +export interface ReactNativeBundleUpdate + extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { + // Download + downloadBundle(params: BundleDownloadParams): Promise; + + // Verify + verifyBundle(params: BundleVerifyParams): Promise; + verifyBundleASC(params: BundleVerifyASCParams): Promise; + downloadBundleASC(params: BundleDownloadASCParams): Promise; + + // Install + installBundle(params: BundleInstallParams): Promise; + + // Clear + clearBundle(): Promise; + clearAllJSBundleData(): Promise; + + // Bundle data + getFallbackUpdateBundleData(): Promise; + setCurrentUpdateBundleData(params: BundleSwitchParams): Promise; + + // Paths + getWebEmbedPath(): string; + getWebEmbedPathAsync(): Promise; + getJsBundlePath(): Promise; + getNativeAppVersion(): Promise; + + // Verification & testing + testVerification(): Promise; + isBundleExists(appVersion: string, bundleVersion: string): Promise; + verifyExtractedBundle( + appVersion: string, + bundleVersion: string, + ): Promise; + listLocalBundles(): Promise; + listAscFiles(): Promise; + getSha256FromFilePath(filePath: string): Promise; + + // Test/debug methods + testDeleteJsBundle( + appVersion: string, + bundleVersion: string, + ): Promise; + testDeleteJsRuntimeDir( + appVersion: string, + bundleVersion: string, + ): Promise; + testDeleteMetadataJson( + appVersion: string, + bundleVersion: string, + ): Promise; + testWriteEmptyMetadataJson( + appVersion: string, + bundleVersion: string, + ): Promise; + + // Events + addDownloadListener(callback: (event: BundleDownloadEvent) => void): number; + removeDownloadListener(id: number): void; +} diff --git a/native-modules/react-native-bundle-update/src/index.tsx b/native-modules/react-native-bundle-update/src/index.tsx new file mode 100644 index 00000000..747a3d73 --- /dev/null +++ b/native-modules/react-native-bundle-update/src/index.tsx @@ -0,0 +1,8 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import type { ReactNativeBundleUpdate as ReactNativeBundleUpdateType } from './ReactNativeBundleUpdate.nitro'; + +const ReactNativeBundleUpdateHybridObject = + NitroModules.createHybridObject('ReactNativeBundleUpdate'); + +export const ReactNativeBundleUpdate = ReactNativeBundleUpdateHybridObject; +export type * from './ReactNativeBundleUpdate.nitro'; diff --git a/native-modules/react-native-bundle-update/tsconfig.build.json b/native-modules/react-native-bundle-update/tsconfig.build.json new file mode 100644 index 00000000..45777014 --- /dev/null +++ b/native-modules/react-native-bundle-update/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "declarationMap": true + }, + "exclude": ["**/__tests__/**/*", "**/__fixtures__/**/*", "**/__mocks__/**/*"] +} diff --git a/native-modules/react-native-bundle-update/tsconfig.json b/native-modules/react-native-bundle-update/tsconfig.json new file mode 100644 index 00000000..cc7b7b24 --- /dev/null +++ b/native-modules/react-native-bundle-update/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "rootDir": ".", + "paths": { + "react-native-bundle-update": ["./src/index"] + }, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "customConditions": ["react-native-strict-api"], + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "lib": ["ESNext"], + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext", + "verbatimModuleSyntax": true + } +} diff --git a/native-modules/react-native-bundle-update/turbo.json b/native-modules/react-native-bundle-update/turbo.json new file mode 100644 index 00000000..08b9676e --- /dev/null +++ b/native-modules/react-native-bundle-update/turbo.json @@ -0,0 +1,17 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "outputs": [ + "lib/**", + "nitrogen/**" + ], + "inputs": [ + "src/**", + "android/src/**", + "ios/**", + "cpp/**" + ] + } + } +} diff --git a/native-modules/react-native-check-biometric-auth-changed/README.md b/native-modules/react-native-check-biometric-auth-changed/README.md deleted file mode 100644 index 99a62d36..00000000 --- a/native-modules/react-native-check-biometric-auth-changed/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# @onekeyfe/react-native-check-biometric-auth-changed - -react-native-check-biometric-auth-changed - -## Installation - - -```sh -npm install @onekeyfe/react-native-check-biometric-auth-changed react-native-nitro-modules - -> `react-native-nitro-modules` is required as this library relies on [Nitro Modules](https://nitro.margelo.com/). -``` - - -## Usage - - -```js -import { multiply } from '@onekeyfe/react-native-check-biometric-auth-changed'; - -// ... - -const result = multiply(3, 7); -``` - - -## Contributing - -- [Development workflow](CONTRIBUTING.md#development-workflow) -- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request) -- [Code of conduct](CODE_OF_CONDUCT.md) - -## License - -MIT - ---- - -Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) diff --git a/native-modules/react-native-check-biometric-auth-changed/ReactNativeCheckBiometricAuthChanged.podspec b/native-modules/react-native-check-biometric-auth-changed/ReactNativeCheckBiometricAuthChanged.podspec index 1605e5ec..5bbbba32 100644 --- a/native-modules/react-native-check-biometric-auth-changed/ReactNativeCheckBiometricAuthChanged.podspec +++ b/native-modules/react-native-check-biometric-auth-changed/ReactNativeCheckBiometricAuthChanged.podspec @@ -21,6 +21,7 @@ Pod::Spec.new do |s| s.dependency 'React-jsi' s.dependency 'React-callinvoker' + s.dependency 'ReactNativeNativeLogger' load 'nitrogen/generated/ios/ReactNativeCheckBiometricAuthChanged+autolinking.rb' add_nitrogen_files(s) diff --git a/native-modules/react-native-check-biometric-auth-changed/ios/ReactNativeCheckBiometricAuthChanged.swift b/native-modules/react-native-check-biometric-auth-changed/ios/ReactNativeCheckBiometricAuthChanged.swift index ac173172..9f7dea96 100644 --- a/native-modules/react-native-check-biometric-auth-changed/ios/ReactNativeCheckBiometricAuthChanged.swift +++ b/native-modules/react-native-check-biometric-auth-changed/ios/ReactNativeCheckBiometricAuthChanged.swift @@ -1,21 +1,73 @@ import NitroModules import LocalAuthentication +import Security +import ReactNativeNativeLogger class ReactNativeCheckBiometricAuthChanged: HybridReactNativeCheckBiometricAuthChangedSpec { + + private static let keychainKey = "com.onekey.biometricAuthState" + + private static func loadDomainState() -> Data? { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: keychainKey, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne + ] + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + if status == errSecSuccess { + return result as? Data + } + return nil + } + + private static func saveDomainState(_ data: Data?) { + guard let data = data else { return } + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: keychainKey, + kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly + ] + let attrs: [String: Any] = [ + kSecValueData as String: data + ] + let status = SecItemUpdate(query as CFDictionary, attrs as CFDictionary) + if status == errSecItemNotFound { + var addQuery = query + addQuery[kSecValueData as String] = data + SecItemAdd(addQuery as CFDictionary, nil) + } + } + func checkChanged() throws -> Promise { return Promise.async { var changed = false let context = LAContext() _ = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) let domainState = context.evaluatedPolicyDomainState - let defaults = UserDefaults.standard - let oldDomainState = defaults.data(forKey: "biometricAuthState") + if domainState == nil { + OneKeyLog.warn("Biometric", "evaluatedPolicyDomainState is nil, cannot determine biometric changes") + return false + } + let oldDomainState = Self.loadDomainState() if let oldDomainState = oldDomainState { changed = oldDomainState != domainState + } else { + // Migrate from UserDefaults if exists + let defaults = UserDefaults.standard + if let legacyState = defaults.data(forKey: "biometricAuthState") { + changed = legacyState != domainState + defaults.removeObject(forKey: "biometricAuthState") + } else { + OneKeyLog.info("Biometric", "No previous biometric state stored, saving initial state") + } + } + Self.saveDomainState(domainState) + if changed { + OneKeyLog.info("Biometric", "Biometric auth change detected") } - defaults.set(domainState, forKey: "biometricAuthState") - defaults.synchronize() return changed } } diff --git a/native-modules/react-native-check-biometric-auth-changed/package.json b/native-modules/react-native-check-biometric-auth-changed/package.json index 88b6fdc6..28818406 100644 --- a/native-modules/react-native-check-biometric-auth-changed/package.json +++ b/native-modules/react-native-check-biometric-auth-changed/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-check-biometric-auth-changed", - "version": "1.1.19", + "version": "1.1.24", "description": "react-native-check-biometric-auth-changed", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-cloud-kit-module/CloudKitModule.podspec b/native-modules/react-native-cloud-kit-module/CloudKitModule.podspec index 81c0593b..0a4628e6 100644 --- a/native-modules/react-native-cloud-kit-module/CloudKitModule.podspec +++ b/native-modules/react-native-cloud-kit-module/CloudKitModule.podspec @@ -21,6 +21,7 @@ Pod::Spec.new do |s| s.dependency 'React-jsi' s.dependency 'React-callinvoker' + s.dependency 'ReactNativeNativeLogger' load 'nitrogen/generated/ios/CloudKitModule+autolinking.rb' add_nitrogen_files(s) diff --git a/native-modules/react-native-cloud-kit-module/README.md b/native-modules/react-native-cloud-kit-module/README.md deleted file mode 100644 index dd360b3d..00000000 --- a/native-modules/react-native-cloud-kit-module/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# react-native-cloud-kit-module - -react-native-cloud-kit-module - -## Installation - - -```sh -npm install react-native-cloud-kit-module react-native-nitro-modules - -> `react-native-nitro-modules` is required as this library relies on [Nitro Modules](https://nitro.margelo.com/). -``` - - -## Usage - - -```js -import { multiply } from 'react-native-cloud-kit-module'; - -// ... - -const result = multiply(3, 7); -``` - - -## Contributing - -- [Development workflow](CONTRIBUTING.md#development-workflow) -- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request) -- [Code of conduct](CODE_OF_CONDUCT.md) - -## License - -MIT - ---- - -Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) diff --git a/native-modules/react-native-cloud-kit-module/ios/CloudKitModule.swift b/native-modules/react-native-cloud-kit-module/ios/CloudKitModule.swift index 7c349431..5a182d80 100644 --- a/native-modules/react-native-cloud-kit-module/ios/CloudKitModule.swift +++ b/native-modules/react-native-cloud-kit-module/ios/CloudKitModule.swift @@ -1,6 +1,7 @@ import Foundation import CloudKit import NitroModules +import ReactNativeNativeLogger class CloudKitModule: HybridCloudKitModuleSpec { @@ -8,11 +9,31 @@ class CloudKitModule: HybridCloudKitModuleSpec { private let container = CKContainer.default() private lazy var database = container.privateCloudDatabase + // MARK: - Input Validation + + private static let recordIDMaxLength = 255 + private static let recordTypePattern = try! NSRegularExpression(pattern: "^[a-zA-Z][a-zA-Z0-9_]{0,254}$") + private static let queryResultsLimit = 200 + + private func validateRecordID(_ recordID: String) throws { + guard !recordID.isEmpty, recordID.count <= Self.recordIDMaxLength else { + throw NSError(domain: "CloudKitModule", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid record ID"]) + } + } + + private func validateRecordType(_ recordType: String) throws { + let range = NSRange(recordType.startIndex..., in: recordType) + guard Self.recordTypePattern.firstMatch(in: recordType, range: range) != nil else { + throw NSError(domain: "CloudKitModule", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid record type"]) + } + } + // MARK: - Check Availability public func isAvailable() throws -> Promise { return Promise.async { let status = try await self.container.accountStatus() + OneKeyLog.info("CloudKit", "Account status: \(status.rawValue), available: \(status == .available)") return status == .available } } @@ -28,6 +49,7 @@ class CloudKitModule: HybridCloudKitModuleSpec { let userRecordID = try await self.container.userRecordID() userId = userRecordID.recordName } catch { + OneKeyLog.warn("CloudKit", "Failed to get user record ID: \(error.localizedDescription)") userId = nil } } @@ -50,7 +72,10 @@ class CloudKitModule: HybridCloudKitModuleSpec { // MARK: - Save Record public func saveRecord(params: SaveRecordParams) throws -> Promise { + try validateRecordID(params.recordID) + try validateRecordType(params.recordType) return Promise.async { + OneKeyLog.debug("CloudKit", "Saving record: \(params.recordID), type: \(params.recordType)") let ckRecordID = CKRecord.ID(recordName: params.recordID) let recordToSave: CKRecord do { @@ -65,6 +90,7 @@ class CloudKitModule: HybridCloudKitModuleSpec { recordToSave[CloudKitConstants.recordDataField] = params.data as CKRecordValue recordToSave[CloudKitConstants.recordMetaField] = params.meta as CKRecordValue let savedRecord = try await self.database.save(recordToSave) + OneKeyLog.debug("CloudKit", "Record saved: \(savedRecord.recordID.recordName)") let createdAt = Int64((savedRecord.creationDate?.timeIntervalSince1970 ?? 0) * 1000) let result = SaveRecordResult( recordID: savedRecord.recordID.recordName, @@ -77,11 +103,13 @@ class CloudKitModule: HybridCloudKitModuleSpec { // MARK: - Fetch Record public func fetchRecord(params: FetchRecordParams) throws -> Promise { + try validateRecordID(params.recordID) return Promise.async { let ckRecordID = CKRecord.ID(recordName: params.recordID) do { let record = try await self.database.record(for: ckRecordID) + OneKeyLog.debug("CloudKit", "Record fetched: \(params.recordID)") let data = record[CloudKitConstants.recordDataField] as? String ?? "" let meta = record[CloudKitConstants.recordMetaField] as? String ?? "" @@ -97,6 +125,7 @@ class CloudKitModule: HybridCloudKitModuleSpec { modifiedAt: Double(modifiedAt) )) } catch let error as CKError where error.code == .unknownItem { + OneKeyLog.debug("CloudKit", "Record not found: \(params.recordID)") return Variant_NullType_RecordResult.first(NullType.null) } } @@ -105,14 +134,16 @@ class CloudKitModule: HybridCloudKitModuleSpec { // MARK: - Delete Record public func deleteRecord(params: DeleteRecordParams) throws -> Promise { + try validateRecordID(params.recordID) return Promise.async { let ckRecordID = CKRecord.ID(recordName: params.recordID) do { _ = try await self.database.deleteRecord(withID: ckRecordID) + OneKeyLog.debug("CloudKit", "Record deleted: \(params.recordID)") return Void() } catch let error as CKError where error.code == .unknownItem { - // Item not found is considered success for delete + OneKeyLog.debug("CloudKit", "Record not found for delete (OK): \(params.recordID)") return Void() } } @@ -121,6 +152,7 @@ class CloudKitModule: HybridCloudKitModuleSpec { // MARK: - Record Exists public func recordExists(params: RecordExistsParams) throws -> Promise { + try validateRecordID(params.recordID) return Promise.async { let ckRecordID = CKRecord.ID(recordName: params.recordID) @@ -136,6 +168,7 @@ class CloudKitModule: HybridCloudKitModuleSpec { // MARK: - Query Records public func queryRecords(params: QueryRecordsParams) throws -> Promise { + try validateRecordType(params.recordType) return Promise.async { let predicate = NSPredicate(value: true) let query = CKQuery(recordType: params.recordType, predicate: predicate) @@ -145,7 +178,7 @@ class CloudKitModule: HybridCloudKitModuleSpec { var results: [RecordResult] = [] let operation = CKQueryOperation(query: query) operation.desiredKeys = [CloudKitConstants.recordMetaField] - // operation.resultsLimit = 500 // Optional: tune as needed + operation.resultsLimit = Self.queryResultsLimit operation.recordMatchedBlock = { _, result in switch result { @@ -172,9 +205,11 @@ class CloudKitModule: HybridCloudKitModuleSpec { case .success: // Sort by modification time descending to return latest first let sorted = results.sorted { $0.modifiedAt > $1.modifiedAt } + OneKeyLog.info("CloudKit", "Query returned \(sorted.count) records for type: \(params.recordType)") let queryResult = QueryRecordsResult(records: sorted) continuation.resume(returning: queryResult) case .failure(let error): + OneKeyLog.error("CloudKit", "Query failed for type \(params.recordType): \(error.localizedDescription)") continuation.resume(throwing: error) } } diff --git a/native-modules/react-native-cloud-kit-module/package.json b/native-modules/react-native-cloud-kit-module/package.json index fd41c5d7..534a8ed3 100644 --- a/native-modules/react-native-cloud-kit-module/package.json +++ b/native-modules/react-native-cloud-kit-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-cloud-kit-module", - "version": "1.1.19", + "version": "1.1.24", "description": "react-native-cloud-kit-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-device-utils/README.md b/native-modules/react-native-device-utils/README.md deleted file mode 100644 index a3641e82..00000000 --- a/native-modules/react-native-device-utils/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# react-native-device-utils - -react-native-device-utils - -## Installation - -```sh -npm install react-native-device-utils react-native-nitro-modules - -> `react-native-nitro-modules` is required as this library relies on [Nitro Modules](https://nitro.margelo.com/). -``` - -## Usage - -```js -import { ReactNativeDeviceUtils } from 'react-native-device-utils'; - -// ... - -const result = await ReactNativeDeviceUtils.hello({ message: 'World' }); -console.log(result); // { success: true, data: 'Hello, World!' } -``` - -## Contributing - -- [Development workflow](CONTRIBUTING.md#development-workflow) -- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request) -- [Code of conduct](CODE_OF_CONDUCT.md) - -## License - -MIT - ---- - -Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) diff --git a/native-modules/react-native-device-utils/ReactNativeDeviceUtils.podspec b/native-modules/react-native-device-utils/ReactNativeDeviceUtils.podspec index 28afd20f..134f717f 100644 --- a/native-modules/react-native-device-utils/ReactNativeDeviceUtils.podspec +++ b/native-modules/react-native-device-utils/ReactNativeDeviceUtils.podspec @@ -21,6 +21,7 @@ Pod::Spec.new do |s| s.dependency 'React-jsi' s.dependency 'React-callinvoker' + s.dependency 'ReactNativeNativeLogger' load 'nitrogen/generated/ios/ReactNativeDeviceUtils+autolinking.rb' add_nitrogen_files(s) diff --git a/native-modules/react-native-device-utils/android/build.gradle b/native-modules/react-native-device-utils/android/build.gradle index 23302caf..b387ec6f 100644 --- a/native-modules/react-native-device-utils/android/build.gradle +++ b/native-modules/react-native-device-utils/android/build.gradle @@ -133,4 +133,9 @@ dependencies { implementation "androidx.appcompat:appcompat:1.7.1" // AndroidX Preference for PreferenceManager implementation "androidx.preference:preference-ktx:1.2.1" + + implementation project(":onekeyfe_react-native-native-logger") + + // Google Play Services for availability check + implementation "com.google.android.gms:play-services-base:18.5.0" } diff --git a/native-modules/react-native-device-utils/android/src/main/java/com/margelo/nitro/reactnativedeviceutils/ReactNativeDeviceUtils.kt b/native-modules/react-native-device-utils/android/src/main/java/com/margelo/nitro/reactnativedeviceutils/ReactNativeDeviceUtils.kt index 519995e5..9a3864ae 100644 --- a/native-modules/react-native-device-utils/android/src/main/java/com/margelo/nitro/reactnativedeviceutils/ReactNativeDeviceUtils.kt +++ b/native-modules/react-native-device-utils/android/src/main/java/com/margelo/nitro/reactnativedeviceutils/ReactNativeDeviceUtils.kt @@ -6,7 +6,7 @@ import android.content.pm.PackageManager import android.graphics.Color import android.graphics.Rect import android.os.Build -import android.util.Log +import com.margelo.nitro.nativelogger.OneKeyLog import androidx.preference.PreferenceManager import androidx.core.content.ContextCompat import androidx.core.util.Consumer @@ -38,6 +38,15 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven companion object { private const val PREF_KEY_FOLDABLE = "1k_fold" private const val PREF_KEY_UI_STYLE = "1k_user_interface_style" + private const val PREF_KEY_DEVICE_TOKEN = "1k_device_token" + + @JvmStatic + var staticStartupTime: Long? = null + + @JvmStatic + fun saveStartupTimeStatic(startupTime: Long) { + staticStartupTime = startupTime + } // Xiaomi foldable models private val XIAOMI_FOLDABLE_MODELS = setOf( @@ -216,7 +225,7 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven ) } - private var windowLayoutInfo: WindowLayoutInfo? = null + @Volatile private var windowLayoutInfo: WindowLayoutInfo? = null private var isSpanning = false private var layoutInfoConsumer: Consumer? = null private var windowInfoTracker: WindowInfoTracker? = null @@ -237,10 +246,14 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven try { val context = NitroModules.applicationContext ?: return val prefs = PreferenceManager.getDefaultSharedPreferences(context) - val style = prefs.getString(PREF_KEY_UI_STYLE, null) ?: return + val style = prefs.getString(PREF_KEY_UI_STYLE, null) ?: run { + OneKeyLog.debug("DeviceUtils", "No saved UI style found") + return + } + OneKeyLog.info("DeviceUtils", "Restored UI style: $style") applyUserInterfaceStyle(style) } catch (e: Exception) { - // Ignore restore errors + OneKeyLog.warn("DeviceUtils", "Failed to restore UI style: ${e.message}") } } @@ -284,6 +297,7 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven saveFoldableStatus(true) } isDualScreenDeviceDetected = hasFolding + OneKeyLog.info("DeviceUtils", "Foldable detected: $hasFolding (${Build.MANUFACTURER} ${Build.MODEL})") return isDualScreenDeviceDetected!! } isDualScreenDeviceDetected = false @@ -576,7 +590,7 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven // Check device model name to determine if it's a foldable device return isFoldableDeviceByName() } catch (e: Exception) { - // WindowManager library not available or device doesn't support foldables + OneKeyLog.warn("DeviceUtils", "Foldable detection failed: ${e.message}") return false } } @@ -705,7 +719,7 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven try { listener.callback(isSpanning) } catch (e: Exception) { - Log.e("OneKey", "Error in spanning listener callback", e) + OneKeyLog.error("DeviceUtils", "Error in spanning listener callback: ${e.message}") } } } @@ -754,7 +768,7 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven layoutInfoConsumer!! ) } catch (e: Exception) { - // Window tracking not supported on this device/API level, ignore + OneKeyLog.warn("DeviceUtils", "Window tracking setup failed: ${e.message}") } } } @@ -787,6 +801,7 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven // Emit event if spanning state changed if (wasSpanning != this.isSpanning) { + OneKeyLog.info("DeviceUtils", "Spanning state changed: $wasSpanning -> ${this.isSpanning}") this.callSpanningChangedListeners(this.isSpanning) } } @@ -799,6 +814,7 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven UserInterfaceStyle.DARK -> "dark" UserInterfaceStyle.UNSPECIFIED -> "unspecified" } + OneKeyLog.info("DeviceUtils", "Set UI style: $styleString") try { val context = NitroModules.applicationContext if (context != null) { @@ -826,7 +842,7 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven val rootView = activity.window.decorView rootView.rootView.setBackgroundColor(Color.argb(alpha, red, green, blue)) } catch (e: Exception) { - e.printStackTrace() + OneKeyLog.error("DeviceUtils", "Failed to change background color: ${e.message}") } } } @@ -842,4 +858,133 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven override fun onHostDestroy() { stopObservingLayoutChanges() } + + // MARK: - LaunchOptionsManager + + override fun getLaunchOptions(): Promise { + return Promise.async { + OneKeyLog.debug("DeviceUtils", "getLaunchOptions") + LaunchOptions(launchType = "normal", deepLink = null) + } + } + + override fun clearLaunchOptions(): Promise { + return Promise.async { + OneKeyLog.info("DeviceUtils", "clearLaunchOptions") + true + } + } + + override fun getDeviceToken(): Promise { + return Promise.async { + val context = NitroModules.applicationContext ?: return@async "" + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + prefs.getString(PREF_KEY_DEVICE_TOKEN, "") ?: "" + } + } + + override fun saveDeviceToken(token: String): Promise { + return Promise.async { + val context = NitroModules.applicationContext + if (context != null) { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + prefs.edit().putString(PREF_KEY_DEVICE_TOKEN, token).apply() + } + OneKeyLog.info("DeviceUtils", "saveDeviceToken: token saved") + } + } + + override fun registerDeviceToken(): Promise { + return Promise.async { true } + } + + override fun getStartupTime(): Promise { + return Promise.async { + (staticStartupTime ?: 0L).toDouble() + } + } + + // MARK: - ExitModule + + override fun exitApp() { + OneKeyLog.info("DeviceUtils", "exitApp") + android.os.Process.killProcess(android.os.Process.myPid()) + } + + // MARK: - WebView & Play Services + + override fun getCurrentWebViewPackageInfo(): Promise { + return Promise.async { + val context = NitroModules.applicationContext + ?: throw Exception("Application context unavailable") + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val webViewPackage = android.webkit.WebView.getCurrentWebViewPackage() + if (webViewPackage != null) { + OneKeyLog.info( + "DeviceUtils", + "WebView: ${webViewPackage.packageName} ${webViewPackage.versionName} ${webViewPackage.versionCode}" + ) + return@async WebViewPackageInfo( + packageName = webViewPackage.packageName, + versionName = webViewPackage.versionName ?: "", + versionCode = webViewPackage.versionCode.toLong().toDouble() + ) + } + } + + // Fallback for API < 26: try common WebView package names + val pm = context.packageManager + val candidates = listOf( + "com.google.android.webview", + "com.android.webview", + "com.android.chrome" + ) + for (candidate in candidates) { + try { + val pInfo = pm.getPackageInfo(candidate, 0) + OneKeyLog.info( + "DeviceUtils", + "WebView (fallback): ${pInfo.packageName} ${pInfo.versionName} ${pInfo.versionCode}" + ) + return@async WebViewPackageInfo( + packageName = pInfo.packageName, + versionName = pInfo.versionName ?: "", + versionCode = pInfo.versionCode.toDouble() + ) + } catch (_: Exception) { + // Try next candidate + } + } + throw Exception("No WebView package found") + } + } + + override fun isGooglePlayServicesAvailable(): Promise { + return Promise.async { + val context = NitroModules.applicationContext + ?: throw Exception("Application context unavailable") + try { + val googleApiAvailability = + com.google.android.gms.common.GoogleApiAvailability.getInstance() + val status = googleApiAvailability.isGooglePlayServicesAvailable(context) + val isSuccess = + status == com.google.android.gms.common.ConnectionResult.SUCCESS + OneKeyLog.info( + "DeviceUtils", + "Play Services status=$status isAvailable=$isSuccess" + ) + GooglePlayServicesStatus( + status = status.toDouble(), + isAvailable = isSuccess + ) + } catch (e: Exception) { + OneKeyLog.error( + "DeviceUtils", + "Play Services check failed: ${e.message}" + ) + GooglePlayServicesStatus(status = -1.0, isAvailable = false) + } + } + } } diff --git a/native-modules/react-native-device-utils/ios/ReactNativeDeviceUtils.swift b/native-modules/react-native-device-utils/ios/ReactNativeDeviceUtils.swift index ca527ac0..2826f91d 100644 --- a/native-modules/react-native-device-utils/ios/ReactNativeDeviceUtils.swift +++ b/native-modules/react-native-device-utils/ios/ReactNativeDeviceUtils.swift @@ -1,5 +1,31 @@ import NitroModules import UIKit +import ReactNativeNativeLogger + +@objcMembers +public class LaunchOptionsStore: NSObject { + public static let shared = LaunchOptionsStore() + + public var launchOptions: [AnyHashable: Any]? + public var deviceToken: Data? + public var startupTime: TimeInterval = 0 + + private static let deviceTokenKey = "1k_device_token" + + public func getDeviceTokenString() -> String { + // Prefer the JS-saved token (persisted across launches) + if let saved = UserDefaults.standard.string(forKey: LaunchOptionsStore.deviceTokenKey), !saved.isEmpty { + return saved + } + // Fall back to the native APNs token set by AppDelegate + guard let token = deviceToken else { return "" } + return token.map { String(format: "%02.2hhx", $0) }.joined() + } + + public func saveDeviceToken(_ token: String) { + UserDefaults.standard.set(token, forKey: LaunchOptionsStore.deviceTokenKey) + } +} class ReactNativeDeviceUtils: HybridReactNativeDeviceUtilsSpec { @@ -12,8 +38,10 @@ class ReactNativeDeviceUtils: HybridReactNativeDeviceUtilsSpec { private func restoreUserInterfaceStyle() { guard let style = UserDefaults.standard.string(forKey: ReactNativeDeviceUtils.userInterfaceStyleKey) else { + OneKeyLog.debug("DeviceUtils", "No saved UI style found") return } + OneKeyLog.info("DeviceUtils", "Restored UI style: \(style)") applyUserInterfaceStyle(style) } @@ -70,6 +98,7 @@ class ReactNativeDeviceUtils: HybridReactNativeDeviceUtilsSpec { public func setUserInterfaceStyle(style: UserInterfaceStyle) throws -> Void { + OneKeyLog.info("DeviceUtils", "Set UI style: \(style.stringValue)") UserDefaults.standard.set(style.stringValue, forKey: ReactNativeDeviceUtils.userInterfaceStyleKey) applyUserInterfaceStyle(style.stringValue) } @@ -91,4 +120,84 @@ class ReactNativeDeviceUtils: HybridReactNativeDeviceUtilsSpec { } } } + + // MARK: - LaunchOptionsManager + + func getLaunchOptions() throws -> Promise { + return Promise.async { + let store = LaunchOptionsStore.shared + guard let opts = store.launchOptions else { + OneKeyLog.debug("DeviceUtils", "getLaunchOptions: no launch options") + return LaunchOptions(launchType: "normal", deepLink: nil) + } + + if opts[UIApplication.LaunchOptionsKey.remoteNotification] != nil { + return LaunchOptions(launchType: "remoteNotification", deepLink: nil) + } + + if opts[UIApplication.LaunchOptionsKey.localNotification] != nil { + return LaunchOptions(launchType: "localNotification", deepLink: nil) + } + + let deepLink = (opts[UIApplication.LaunchOptionsKey.url] as? URL)?.absoluteString + return LaunchOptions(launchType: "normal", deepLink: deepLink) + } + } + + func clearLaunchOptions() throws -> Promise { + return Promise.async { + LaunchOptionsStore.shared.launchOptions = nil + OneKeyLog.info("DeviceUtils", "Cleared launch options") + return true + } + } + + func getDeviceToken() throws -> Promise { + return Promise.async { + return LaunchOptionsStore.shared.getDeviceTokenString() + } + } + + func saveDeviceToken(token: String) throws -> Promise { + return Promise.async { + LaunchOptionsStore.shared.saveDeviceToken(token) + OneKeyLog.info("DeviceUtils", "saveDeviceToken: token saved") + } + } + + func registerDeviceToken() throws -> Promise { + return Promise.async { + OneKeyLog.info("DeviceUtils", "registerDeviceToken") + return true + } + } + + func getStartupTime() throws -> Promise { + return Promise.async { + return LaunchOptionsStore.shared.startupTime * 1000.0 + } + } + + // MARK: - ExitModule + + func exitApp() throws { + // No-op on iOS: Apple prohibits programmatic app termination (App Store guideline 2.4.5). + } + + // MARK: - WebView & Play Services + + func getCurrentWebViewPackageInfo() throws -> Promise { + return Promise.resolved(withResult: WebViewPackageInfo( + packageName: "com.apple.WebKit", + versionName: UIDevice.current.systemVersion, + versionCode: 0 + )) + } + + func isGooglePlayServicesAvailable() throws -> Promise { + return Promise.resolved(withResult: GooglePlayServicesStatus( + status: -1, + isAvailable: false + )) + } } diff --git a/native-modules/react-native-device-utils/package.json b/native-modules/react-native-device-utils/package.json index 00448f3b..96cd55e5 100644 --- a/native-modules/react-native-device-utils/package.json +++ b/native-modules/react-native-device-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-device-utils", - "version": "1.1.19", + "version": "1.1.24", "description": "react-native-device-utils", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-device-utils/src/ReactNativeDeviceUtils.nitro.ts b/native-modules/react-native-device-utils/src/ReactNativeDeviceUtils.nitro.ts index de027d7e..995aeb20 100644 --- a/native-modules/react-native-device-utils/src/ReactNativeDeviceUtils.nitro.ts +++ b/native-modules/react-native-device-utils/src/ReactNativeDeviceUtils.nitro.ts @@ -10,6 +10,22 @@ export interface DualScreenInfoRect { height: number; } +export interface LaunchOptions { + launchType: string; + deepLink?: string; +} + +export interface WebViewPackageInfo { + packageName: string; + versionName: string; + versionCode: number; +} + +export interface GooglePlayServicesStatus { + status: number; + isAvailable: boolean; +} + export interface ReactNativeDeviceUtils extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { initEventListeners(): void; @@ -21,4 +37,19 @@ export interface ReactNativeDeviceUtils addSpanningChangedListener(callback: (isSpanning: boolean) => void): number; removeSpanningChangedListener(id: number): void; setUserInterfaceStyle(style: UserInterfaceStyle): void; + + // LaunchOptionsManager + getLaunchOptions(): Promise; + clearLaunchOptions(): Promise; + getDeviceToken(): Promise; + saveDeviceToken(token: string): Promise; + registerDeviceToken(): Promise; + getStartupTime(): Promise; + + // ExitModule + exitApp(): void; + + // WebView & Play Services + getCurrentWebViewPackageInfo(): Promise; + isGooglePlayServicesAvailable(): Promise; } diff --git a/native-modules/react-native-get-random-values/README.md b/native-modules/react-native-get-random-values/README.md deleted file mode 100644 index 987d3aae..00000000 --- a/native-modules/react-native-get-random-values/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# react-native-get-random-values - -react-native-get-random-values - -## Installation - -```sh -npm install react-native-get-random-values react-native-nitro-modules - -> `react-native-nitro-modules` is required as this library relies on [Nitro Modules](https://nitro.margelo.com/). -``` - -## Usage - -```js -import { ReactNativeGetRandomValues } from 'react-native-get-random-values'; - -// ... - -const result = await ReactNativeGetRandomValues.hello({ message: 'World' }); -console.log(result); // { success: true, data: 'Hello, World!' } -``` - -## Contributing - -- [Development workflow](CONTRIBUTING.md#development-workflow) -- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request) -- [Code of conduct](CODE_OF_CONDUCT.md) - -## License - -MIT - ---- - -Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) diff --git a/native-modules/react-native-get-random-values/ReactNativeGetRandomValues.podspec b/native-modules/react-native-get-random-values/ReactNativeGetRandomValues.podspec index 18c630cd..69db3d9d 100644 --- a/native-modules/react-native-get-random-values/ReactNativeGetRandomValues.podspec +++ b/native-modules/react-native-get-random-values/ReactNativeGetRandomValues.podspec @@ -21,6 +21,7 @@ Pod::Spec.new do |s| s.dependency 'React-jsi' s.dependency 'React-callinvoker' + s.dependency 'ReactNativeNativeLogger' load 'nitrogen/generated/ios/ReactNativeGetRandomValues+autolinking.rb' add_nitrogen_files(s) diff --git a/native-modules/react-native-get-random-values/android/build.gradle b/native-modules/react-native-get-random-values/android/build.gradle index 430e0c8d..f2fe6879 100644 --- a/native-modules/react-native-get-random-values/android/build.gradle +++ b/native-modules/react-native-get-random-values/android/build.gradle @@ -125,4 +125,6 @@ dependencies { implementation "com.facebook.react:react-android" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation project(":react-native-nitro-modules") + + implementation project(":onekeyfe_react-native-native-logger") } diff --git a/native-modules/react-native-get-random-values/android/src/main/java/com/margelo/nitro/reactnativegetrandomvalues/ReactNativeGetRandomValues.kt b/native-modules/react-native-get-random-values/android/src/main/java/com/margelo/nitro/reactnativegetrandomvalues/ReactNativeGetRandomValues.kt index 5d7a31ad..0d6612ea 100644 --- a/native-modules/react-native-get-random-values/android/src/main/java/com/margelo/nitro/reactnativegetrandomvalues/ReactNativeGetRandomValues.kt +++ b/native-modules/react-native-get-random-values/android/src/main/java/com/margelo/nitro/reactnativegetrandomvalues/ReactNativeGetRandomValues.kt @@ -1,15 +1,31 @@ package com.margelo.nitro.reactnativegetrandomvalues import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.nativelogger.OneKeyLog @DoNotStrip class ReactNativeGetRandomValues : HybridReactNativeGetRandomValuesSpec() { - override fun getRandomBase64(byteLength: Double): String { - val data = ByteArray(byteLength.toInt()) - val random = java.security.SecureRandom() - - random.nextBytes(data) + companion object { + private const val MAX_BYTE_LENGTH = 65536 // 64 KB upper bound + } - return android.util.Base64.encodeToString(data, android.util.Base64.NO_WRAP) + override fun getRandomBase64(byteLength: Double): String { + if (byteLength.isNaN() || byteLength.isInfinite() || byteLength != kotlin.math.floor(byteLength)) { + OneKeyLog.warn("RandomValues", "byteLength must be an integer value, got: $byteLength") + throw IllegalArgumentException("byteLength must be an integer value") + } + val length = byteLength.toInt() + if (length <= 0 || length > MAX_BYTE_LENGTH) { + OneKeyLog.warn("RandomValues", "Invalid byteLength: $byteLength, must be 1..$MAX_BYTE_LENGTH") + throw IllegalArgumentException("Invalid byteLength: must be 1...$MAX_BYTE_LENGTH") + } + try { + val data = ByteArray(length) + java.security.SecureRandom().nextBytes(data) + return android.util.Base64.encodeToString(data, android.util.Base64.NO_WRAP) + } catch (e: Exception) { + OneKeyLog.error("RandomValues", "SecureRandom failed: ${e.message}\n${e.stackTraceToString()}") + throw e + } } } \ No newline at end of file diff --git a/native-modules/react-native-get-random-values/ios/ReactNativeGetRandomValues.swift b/native-modules/react-native-get-random-values/ios/ReactNativeGetRandomValues.swift index fb14118d..d3055b76 100644 --- a/native-modules/react-native-get-random-values/ios/ReactNativeGetRandomValues.swift +++ b/native-modules/react-native-get-random-values/ios/ReactNativeGetRandomValues.swift @@ -1,12 +1,28 @@ import NitroModules +import Security +import ReactNativeNativeLogger class ReactNativeGetRandomValues: HybridReactNativeGetRandomValuesSpec { + private static let maxByteLength = 65536 // 64 KB upper bound + func getRandomBase64(byteLength: Double) throws -> String { - let data = NSMutableData(length: Int(byteLength))! - let result = SecRandomCopyBytes(kSecRandomDefault, Int(byteLength), data.mutableBytes) + guard byteLength.rounded() == byteLength, !byteLength.isNaN, !byteLength.isInfinite else { + OneKeyLog.warn("RandomValues", "byteLength must be an integer value, got: \(byteLength)") + throw NSError(domain: "ReactNativeGetRandomValues", code: -1, + userInfo: [NSLocalizedDescriptionKey: "byteLength must be an integer value"]) + } + let length = Int(byteLength) + guard length > 0, length <= ReactNativeGetRandomValues.maxByteLength, + let data = NSMutableData(length: length) else { + OneKeyLog.warn("RandomValues", "Invalid byteLength: \(byteLength), must be 1...\(ReactNativeGetRandomValues.maxByteLength)") + throw NSError(domain: "ReactNativeGetRandomValues", code: -1, + userInfo: [NSLocalizedDescriptionKey: "Invalid byteLength: must be 1...\(ReactNativeGetRandomValues.maxByteLength)"]) + } + let result = SecRandomCopyBytes(kSecRandomDefault, length, data.mutableBytes) if result != errSecSuccess { + OneKeyLog.error("RandomValues", "SecRandomCopyBytes failed with status: \(result)") throw NSError(domain: "ReactNativeGetRandomValues", code: Int(result), userInfo: nil) } - return data.base64EncodedString(options: .lineLength64Characters) + return data.base64EncodedString(options: []) } } diff --git a/native-modules/react-native-get-random-values/package.json b/native-modules/react-native-get-random-values/package.json index 51bda887..dbf0e124 100644 --- a/native-modules/react-native-get-random-values/package.json +++ b/native-modules/react-native-get-random-values/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-get-random-values", - "version": "1.1.19", + "version": "1.1.24", "description": "react-native-get-random-values", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-keychain-module/KeychainModule.podspec b/native-modules/react-native-keychain-module/KeychainModule.podspec index a8b2a68e..916cbb72 100644 --- a/native-modules/react-native-keychain-module/KeychainModule.podspec +++ b/native-modules/react-native-keychain-module/KeychainModule.podspec @@ -21,6 +21,7 @@ Pod::Spec.new do |s| s.dependency 'React-jsi' s.dependency 'React-callinvoker' + s.dependency 'ReactNativeNativeLogger' load 'nitrogen/generated/ios/KeychainModule+autolinking.rb' add_nitrogen_files(s) diff --git a/native-modules/react-native-keychain-module/README.md b/native-modules/react-native-keychain-module/README.md deleted file mode 100644 index acdfe19d..00000000 --- a/native-modules/react-native-keychain-module/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# react-native-keychain-module - -react-native-keychain-module - -## Installation - - -```sh -npm install react-native-keychain-module react-native-nitro-modules - -> `react-native-nitro-modules` is required as this library relies on [Nitro Modules](https://nitro.margelo.com/). -``` - - -## Usage - - -```js -import { multiply } from 'react-native-keychain-module'; - -// ... - -const result = multiply(3, 7); -``` - - -## Contributing - -- [Development workflow](CONTRIBUTING.md#development-workflow) -- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request) -- [Code of conduct](CODE_OF_CONDUCT.md) - -## License - -MIT - ---- - -Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) diff --git a/native-modules/react-native-keychain-module/ios/KeychainModule.swift b/native-modules/react-native-keychain-module/ios/KeychainModule.swift index eda8ea43..1cd0851d 100644 --- a/native-modules/react-native-keychain-module/ios/KeychainModule.swift +++ b/native-modules/react-native-keychain-module/ios/KeychainModule.swift @@ -1,13 +1,12 @@ import NitroModules class KeychainModule: HybridKeychainModuleSpec { - + private let moduleCore = KeychainModuleCore() - + public func setItem(params: SetItemParams) throws -> Promise { - let typedParams = params do { - try moduleCore.setItem(params: typedParams) + try moduleCore.setItem(params: params) return Promise.resolved(withResult: Void()) } catch let error as KeychainModuleError { switch error { @@ -22,11 +21,10 @@ class KeychainModule: HybridKeychainModuleSpec { return Promise.rejected(withError: NSError(domain: "keychain_set_error", code: -1000, userInfo: [NSLocalizedDescriptionKey: "Failed to set keychain item", NSUnderlyingErrorKey: error])) } } - + public func getItem(params : GetItemParams) throws -> Promise { - let typedParams = params do { - if let result = try moduleCore.getItem(params: typedParams) { + if let result = try moduleCore.getItem(params: params) { return Promise.resolved(withResult: Variant_NullType_GetItemResult.second(result)) } else { return Promise.resolved(withResult: Variant_NullType_GetItemResult.first(NullType.null)) @@ -42,11 +40,10 @@ class KeychainModule: HybridKeychainModuleSpec { return Promise.rejected(withError: NSError(domain: "keychain_get_error", code: -1000, userInfo: [NSLocalizedDescriptionKey: "Failed to get keychain item", NSUnderlyingErrorKey: error])) } } - + public func removeItem(params: RemoveItemParams) throws -> Promise { - let typedParams = params do { - try moduleCore.removeItem(params: typedParams) + try moduleCore.removeItem(params: params) return Promise.resolved(withResult: Void()) } catch let error as KeychainModuleError { switch error { diff --git a/native-modules/react-native-keychain-module/ios/KeychainModuleCore.swift b/native-modules/react-native-keychain-module/ios/KeychainModuleCore.swift index 9d2bc4b1..ea5bc949 100644 --- a/native-modules/react-native-keychain-module/ios/KeychainModuleCore.swift +++ b/native-modules/react-native-keychain-module/ios/KeychainModuleCore.swift @@ -8,6 +8,7 @@ import Foundation import Security +import ReactNativeNativeLogger // MARK: - Constants @@ -33,6 +34,7 @@ class KeychainModuleCore { func setItem(params: SetItemParams) throws { guard let valueData = params.value.data(using: .utf8) else { + OneKeyLog.error("Keychain", "setItem: failed to encode value") throw KeychainModuleError.encodingFailed } @@ -57,14 +59,40 @@ class KeychainModuleCore { query[kSecAttrDescription as String] = description } - // Delete existing item if it exists - SecItemDelete(query as CFDictionary) - - // Add new item + // Try to add new item first; if it already exists, update it let status = SecItemAdd(query as CFDictionary, nil) - guard status == errSecSuccess else { - throw KeychainModuleError.operationFailed(status) + if status == errSecDuplicateItem { + // Item exists - update it instead of delete+add (avoids race condition window) + let searchQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: KeychainConstants.serviceIdentifier ?? "", + kSecAttrAccount as String: params.key, + kSecAttrSynchronizable as String: kSecAttrSynchronizableAny + ] + var updateAttrs: [String: Any] = [ + kSecValueData as String: valueData, + kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked, + kSecAttrSynchronizable as String: enableSync + ] + if let label = params.label { + updateAttrs[kSecAttrLabel as String] = label + } + if let description = params.description { + updateAttrs[kSecAttrDescription as String] = description + } + let updateStatus = SecItemUpdate(searchQuery as CFDictionary, updateAttrs as CFDictionary) + guard updateStatus == errSecSuccess else { + OneKeyLog.error("Keychain", "setItem update: failed, OSStatus: \(updateStatus)") + throw KeychainModuleError.operationFailed(updateStatus) + } + OneKeyLog.info("Keychain", "setItem: updated existing") + } else { + guard status == errSecSuccess else { + OneKeyLog.error("Keychain", "setItem: failed, OSStatus: \(status)") + throw KeychainModuleError.operationFailed(status) + } + OneKeyLog.info("Keychain", "setItem: success") } } @@ -86,12 +114,15 @@ class KeychainModuleCore { if status == errSecSuccess { if let valueData = result as? Data, let value = String(data: valueData, encoding: .utf8) { + OneKeyLog.debug("Keychain", "getItem: found") return GetItemResult(key: params.key, value: value) } return nil } else if status == errSecItemNotFound { + OneKeyLog.debug("Keychain", "getItem: not found") return nil } else { + OneKeyLog.error("Keychain", "getItem: failed, OSStatus: \(status)") throw KeychainModuleError.operationFailed(status) } } @@ -110,8 +141,10 @@ class KeychainModuleCore { // Both success and item not found are acceptable for delete guard status == errSecSuccess || status == errSecItemNotFound else { + OneKeyLog.error("Keychain", "removeItem: failed, OSStatus: \(status)") throw KeychainModuleError.operationFailed(status) } + OneKeyLog.info("Keychain", "removeItem: success") } // MARK: - Check Item Existence @@ -126,6 +159,7 @@ class KeychainModuleCore { ] let status = SecItemCopyMatching(query as CFDictionary, nil) + OneKeyLog.debug("Keychain", "hasItem: \(status == errSecSuccess)") return status == errSecSuccess } @@ -169,6 +203,8 @@ class KeychainModuleCore { // Common error codes: // errSecMissingEntitlement (-34018): Missing iCloud Keychain entitlement // errSecNotAvailable (-25291): iCloud Keychain not available/signed out - return addStatus == errSecSuccess + let enabled = addStatus == errSecSuccess + OneKeyLog.info("Keychain", "iCloud sync check result: \(enabled)") + return enabled } } diff --git a/native-modules/react-native-keychain-module/package.json b/native-modules/react-native-keychain-module/package.json index dd02f42c..54e9a725 100644 --- a/native-modules/react-native-keychain-module/package.json +++ b/native-modules/react-native-keychain-module/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-keychain-module", - "version": "1.1.19", + "version": "1.1.24", "description": "react-native-keychain-module", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-lite-card/README.md b/native-modules/react-native-lite-card/README.md deleted file mode 100644 index e52a9ecd..00000000 --- a/native-modules/react-native-lite-card/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# @onekeyfe/react-native-lite-card - -lite card - -## Installation - - -```sh -npm install @onekeyfe/react-native-lite-card -``` - - -## Usage - - -```js -import { multiply } from '@onekeyfe/react-native-lite-card'; - -// ... - -const result = multiply(3, 7); -``` - - -## Contributing - -- [Development workflow](CONTRIBUTING.md#development-workflow) -- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request) -- [Code of conduct](CODE_OF_CONDUCT.md) - -## License - -MIT - ---- - -Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) diff --git a/native-modules/react-native-lite-card/ReactNativeLiteCard.podspec b/native-modules/react-native-lite-card/ReactNativeLiteCard.podspec index 1e37c486..62a13dd4 100644 --- a/native-modules/react-native-lite-card/ReactNativeLiteCard.podspec +++ b/native-modules/react-native-lite-card/ReactNativeLiteCard.podspec @@ -18,5 +18,7 @@ Pod::Spec.new do |s| s.private_header_files = "ios/**/*.h" + s.dependency 'ReactNativeNativeLogger' + install_modules_dependencies(s) end diff --git a/native-modules/react-native-lite-card/android/build.gradle b/native-modules/react-native-lite-card/android/build.gradle index 7c1df77e..ecbb5db9 100644 --- a/native-modules/react-native-lite-card/android/build.gradle +++ b/native-modules/react-native-lite-card/android/build.gradle @@ -82,4 +82,5 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'com.google.code.gson:gson:2.10.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' + implementation project(":onekeyfe_react-native-native-logger") } diff --git a/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/LoggerManager.kt b/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/LoggerManager.kt index 26c84a27..6aac5f81 100644 --- a/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/LoggerManager.kt +++ b/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/LoggerManager.kt @@ -1,9 +1,9 @@ package com.onekeyfe.reactnativelitecard -import android.util.Log import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod +import com.margelo.nitro.nativelogger.OneKeyLog import com.onekeyfe.reactnativelitecard.utils.sendEvent class LoggerManager(private val context: ReactApplicationContext) : @@ -29,7 +29,7 @@ class LoggerManager(private val context: ReactApplicationContext) : @ReactMethod public fun logInfo(message: String) { - Log.d(TAG, message) + OneKeyLog.info("LiteCard", message) sendEvent(context, LOG_EVENT_INFO, message) } } diff --git a/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/ReactNativeLiteCardModule.kt b/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/ReactNativeLiteCardModule.kt index 90ecd0e2..c59477e7 100644 --- a/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/ReactNativeLiteCardModule.kt +++ b/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/ReactNativeLiteCardModule.kt @@ -4,7 +4,7 @@ import android.content.Intent import android.nfc.NfcAdapter import android.nfc.Tag import android.nfc.tech.IsoDep -import android.util.Log +import com.margelo.nitro.nativelogger.OneKeyLog import androidx.annotation.IntDef import androidx.fragment.app.FragmentActivity import kotlinx.coroutines.* @@ -90,18 +90,18 @@ class ReactNativeLiteCardModule(val reactContext: ReactApplicationContext) : putString("type", "unknown") } emitOnNFCActiveConnection(dataMap.copy()) - Log.d(TAG, "Unknown device") + OneKeyLog.debug("LiteCard","Unknown device") return } - Log.d(TAG, isoDep.toString()) + OneKeyLog.debug("LiteCard",isoDep.toString()) launch(Dispatchers.IO) { mNFCConnectedChannel.trySend(isoDep) try { // 处理主动触发 NFC delay(100) if (!mNFCConnectedChannel.isEmpty) { - Log.e(TAG, "There is no way to use NFC") + OneKeyLog.error("LiteCard","There is no way to use NFC") // mNFCConnectedChannel.receive() val startRequest = OneKeyLiteCard.initRequest(isoDep) val dataMap = Arguments.createMap().apply { @@ -114,7 +114,7 @@ class ReactNativeLiteCardModule(val reactContext: ReactApplicationContext) : emitOnNFCActiveConnection(dataMap.copy()) } } catch (e: Exception) { - e.printStackTrace() + OneKeyLog.error("LiteCard", "NFC connection failed: ${e.message}") // 未知设备或连接失败 val dataMap = Arguments.createMap().apply { putInt("code", -1) @@ -208,12 +208,12 @@ class ReactNativeLiteCardModule(val reactContext: ReactApplicationContext) : val topActivity = Utils.getTopActivity() ?: return NfcPermissionUtils.checkPermission(topActivity) { try { - Log.d(TAG, "NFC permission check success") + OneKeyLog.debug("LiteCard","NFC permission check success") val isoDep = acquireDevice() ?: return val executeResult = execute(isoDep) callback?.invoke(null, executeResult, mCurrentCardState.createArguments()) } catch (e: NFCExceptions) { - Log.e(TAG, "NFC device execute error", e) + OneKeyLog.error("LiteCard", "NFC device execute error: ${e.message}") callback?.invoke(e.createArguments(), null, mCurrentCardState.createArguments()) } finally { releaseDevice() @@ -221,7 +221,7 @@ class ReactNativeLiteCardModule(val reactContext: ReactApplicationContext) : return } // 没有 NFC 使用权限 - Log.d(TAG, "NFC device not permission") + OneKeyLog.debug("LiteCard","NFC device not permission") callback?.invoke(NFCExceptions.NotNFCPermission().createArguments(), null, null) } @@ -245,11 +245,11 @@ class ReactNativeLiteCardModule(val reactContext: ReactApplicationContext) : override fun getLiteInfo(callback: Callback?) { launch { - Log.d(TAG, "getLiteInfo") + OneKeyLog.debug("LiteCard","getLiteInfo") handleOperation(callback) { isoDep -> - Log.e(TAG, "getLiteInfo Obtain the device") + OneKeyLog.debug("LiteCard","getLiteInfo Obtain the device") val cardInfo = OneKeyLiteCard.getCardInfo(isoDep) - Log.e(TAG, "getLiteInfo result $cardInfo") + OneKeyLog.debug("LiteCard","getLiteInfo result obtained") cardInfo.createArguments() } } @@ -264,7 +264,7 @@ class ReactNativeLiteCardModule(val reactContext: ReactApplicationContext) : val isNfcExists = NfcUtils.isNfcExits(topActivity) if (!isNfcExists) { // 没有 NFC 设备 - Log.d(TAG, "NFC device not found") + OneKeyLog.debug("LiteCard","NFC device not found") callback?.invoke(NFCExceptions.NotExistsNFC().createArguments(), null, null) return } @@ -272,7 +272,7 @@ class ReactNativeLiteCardModule(val reactContext: ReactApplicationContext) : val isNfcEnable = NfcUtils.isNfcEnable(topActivity) if (!isNfcEnable) { // 没有打开 NFC 开关 - Log.d(TAG, "NFC device not enable") + OneKeyLog.debug("LiteCard","NFC device not enable") callback?.invoke(NFCExceptions.NotEnableNFC().createArguments(), null, null) return } @@ -281,7 +281,7 @@ class ReactNativeLiteCardModule(val reactContext: ReactApplicationContext) : return } // 没有 NFC 使用权限 - Log.d(TAG, "NFC device not permission") + OneKeyLog.debug("LiteCard","NFC device not permission") callback?.invoke(NFCExceptions.NotNFCPermission().createArguments(), null, null) } @@ -291,9 +291,17 @@ class ReactNativeLiteCardModule(val reactContext: ReactApplicationContext) : overwrite: Boolean, callback: Callback? ) { + if (pwd.isNullOrEmpty() || pwd.length != 6 || !pwd.all { it.isDigit() }) { + callback?.invoke(NFCExceptions.InputPasswordEmptyException().createArguments(), null, null) + return + } + if (mnemonic.isNullOrEmpty()) { + callback?.invoke(NFCExceptions.ExecFailureException().createArguments(), null, null) + return + } launch { handleOperation(callback) { isoDep -> - Log.e(TAG, "setMnemonic Obtain the device") + OneKeyLog.debug("LiteCard","setMnemonic Obtain the device") val isSuccess = OneKeyLiteCard.setMnemonic( mCurrentCardState, @@ -303,7 +311,7 @@ class ReactNativeLiteCardModule(val reactContext: ReactApplicationContext) : overwrite ) if (!isSuccess) throw NFCExceptions.ExecFailureException() - Log.e(TAG, "setMnemonic result success") + OneKeyLog.debug("LiteCard","setMnemonic result success") true } } @@ -313,10 +321,14 @@ class ReactNativeLiteCardModule(val reactContext: ReactApplicationContext) : pwd: String, callback: Callback? ) { + if (pwd.isEmpty() || pwd.length != 6 || !pwd.all { it.isDigit() }) { + callback?.invoke(NFCExceptions.InputPasswordEmptyException().createArguments(), null, null) + return + } launch { - Log.d(TAG, "getMnemonicWithPin") + OneKeyLog.debug("LiteCard","getMnemonicWithPin") handleOperation(callback) { isoDep -> - Log.e(TAG, "getMnemonicWithPin Obtain the device") + OneKeyLog.debug("LiteCard","getMnemonicWithPin Obtain the device") OneKeyLiteCard.getMnemonicWithPin(mCurrentCardState, isoDep, pwd) } } @@ -328,9 +340,9 @@ class ReactNativeLiteCardModule(val reactContext: ReactApplicationContext) : callback: Callback? ) { launch { - Log.d(TAG, "changePin") + OneKeyLog.debug("LiteCard","changePin") handleOperation(callback) { isoDep -> - Log.e(TAG, "changePin Obtain the device") + OneKeyLog.debug("LiteCard","changePin Obtain the device") OneKeyLiteCard.changPin(mCurrentCardState, isoDep, oldPin, newPin) } } @@ -338,12 +350,12 @@ class ReactNativeLiteCardModule(val reactContext: ReactApplicationContext) : override fun reset(callback: Callback?) { launch { - Log.d(TAG, "reset") + OneKeyLog.debug("LiteCard","reset") handleOperation(callback) { isoDep -> - Log.e(TAG, "reset Obtain the device") + OneKeyLog.debug("LiteCard","reset Obtain the device") val isSuccess = OneKeyLiteCard.reset(isoDep) if (!isSuccess) throw NFCExceptions.ExecFailureException() - Log.e(TAG, "reset result success") + OneKeyLog.debug("LiteCard","reset result success") true } } @@ -357,7 +369,7 @@ class ReactNativeLiteCardModule(val reactContext: ReactApplicationContext) : override fun intoSetting() { launch { - Log.d(TAG, "intoSetting") + OneKeyLog.debug("LiteCard","intoSetting") Utils.getTopActivity()?.let { NfcUtils.intentToNfcSetting(it) } @@ -401,10 +413,10 @@ class ReactNativeLiteCardModule(val reactContext: ReactApplicationContext) : try { OneKeyLiteCard.startNfc(it) { mNFCState.set(NFCState.Started) - Log.d(TAG, "NFC starting success") + OneKeyLog.debug("LiteCard","NFC starting success") } } catch (e: Exception) { - Log.e(TAG, "startNfc failed", e) + OneKeyLog.error("LiteCard", "startNfc failed: ${e.message}") } } } @@ -416,9 +428,9 @@ class ReactNativeLiteCardModule(val reactContext: ReactApplicationContext) : try { OneKeyLiteCard.stopNfc(it as FragmentActivity) mNFCState.set(NFCState.Dead) - Log.d(TAG, "NFC 已关闭") + OneKeyLog.debug("LiteCard","NFC 已关闭") } catch (e: Exception) { - Log.e(TAG, "stopNfc failed", e) + OneKeyLog.error("LiteCard", "stopNfc failed: ${e.message}") } } } diff --git a/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/nfc/broadcast/NfcStatusChangeBroadcastReceiver.kt b/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/nfc/broadcast/NfcStatusChangeBroadcastReceiver.kt index 4dbd1ec8..32564af5 100644 --- a/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/nfc/broadcast/NfcStatusChangeBroadcastReceiver.kt +++ b/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/nfc/broadcast/NfcStatusChangeBroadcastReceiver.kt @@ -5,7 +5,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.nfc.NfcAdapter -import android.util.Log +import com.margelo.nitro.nativelogger.OneKeyLog open class NfcStatusChangeBroadcastReceiver : BroadcastReceiver() { @@ -15,7 +15,7 @@ open class NfcStatusChangeBroadcastReceiver : BroadcastReceiver() { val state = intent.getIntExtra(NfcAdapter.EXTRA_ADAPTER_STATE, NfcAdapter.STATE_OFF) - Log.e(TAG, "nfc state broadcast receiver, state is $state") + OneKeyLog.debug("LiteCard", "nfc state broadcast receiver, state is $state") when (state) { NfcAdapter.STATE_OFF -> onNfcOff() NfcAdapter.STATE_ON -> onNfcOn() diff --git a/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/onekeyLite/OnekeyLiteCard.kt b/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/onekeyLite/OnekeyLiteCard.kt index c6871c32..40ceced2 100644 --- a/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/onekeyLite/OnekeyLiteCard.kt +++ b/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/onekeyLite/OnekeyLiteCard.kt @@ -2,8 +2,8 @@ package com.onekeyfe.reactnativelitecard.onekeyLite import android.app.Activity import android.nfc.tech.IsoDep -import android.util.Log import androidx.fragment.app.FragmentActivity +import com.margelo.nitro.nativelogger.OneKeyLog import androidx.lifecycle.Lifecycle import com.google.gson.Gson import kotlinx.coroutines.Dispatchers @@ -20,9 +20,10 @@ import com.onekeyfe.reactnativelitecard.utils.NfcPermissionUtils object OneKeyLiteCard { const val TAG = "OneKeyLiteCard" - private val mCommandGenerator by lazy(LazyThreadSafetyMode.NONE) { + private val mCommandGenerator by lazy { CommandGenerator() } + @Volatile private var mCardConnection: Connection? = null suspend fun startNfc(activity: FragmentActivity, callback: ((Boolean) -> Unit)? = null) = @@ -52,7 +53,7 @@ object OneKeyLiteCard { callback?.invoke(true) return@withContext } - Log.e(TAG, "startNfc Not NFC permission") + OneKeyLog.error("LiteCard","startNfc Not NFC permission") callback?.invoke(false) } @@ -110,7 +111,7 @@ object OneKeyLiteCard { ): Boolean { if (cardState == null) throw NFCExceptions.ConnectionFailException() - printLog(TAG, "--> setMnemonic: cardState:${Gson().toJson(cardState)}") + printLog(TAG, "--> setMnemonic: isNewCard:${cardState?.isNewCard}, hasBackup:${cardState?.hasBackup}") if (!overwrite) { // 不是覆写要验证是否已经已经存有备份 @@ -146,7 +147,8 @@ object OneKeyLiteCard { } } - return mCardConnection?.backupData(mnemonic ?: "") == true + if (mnemonic.isNullOrEmpty()) throw NFCExceptions.ExecFailureException() + return mCardConnection?.backupData(mnemonic) == true } @Throws(NFCExceptions::class) diff --git a/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/onekeyLite/nfc/Connection.kt b/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/onekeyLite/nfc/Connection.kt index 86ebc9f7..7bb2603a 100644 --- a/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/onekeyLite/nfc/Connection.kt +++ b/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/onekeyLite/nfc/Connection.kt @@ -140,7 +140,7 @@ class Connection(val isoDep: IsoDep, private val mCommandGenerator: CommandGener printLog(TAG, "0. ---> getDeviceCertificate begin") val certInfo = getDeviceCertificate() ?: return false - printLog(TAG, "0. <--- getDeviceCertificate end: ${certInfo.subjectID}") + printLog(TAG, "0. <--- getDeviceCertificate end") printLog(TAG, "1. ---> nativeGPCInitialize begin") val param = SecureChanelParam.objectFromData( @@ -303,7 +303,7 @@ class Connection(val isoDep: IsoDep, private val mCommandGenerator: CommandGener mCardType, CommandType.SETUP_NEW_PIN, hasOpenSafeChannel, "DFFE0B8204080006$pinHex" ) val res = command.send(this) - printLog(TAG, "<--- setupNewPin end: ${res.getCode()} ${res.data} area:${mCommandArea}") + printLog(TAG, "<--- setupNewPin end: ${res.getCode()} area:${mCommandArea}") if (!res.isSuccess()) { return false @@ -326,7 +326,7 @@ class Connection(val isoDep: IsoDep, private val mCommandGenerator: CommandGener ) val res = command.send(this) - printLog(TAG, "<--- changePin end: ${res.getCode()} ${res.data} area:${mCommandArea}") + printLog(TAG, "<--- changePin end: ${res.getCode()} area:${mCommandArea}") return if (res.isConnectFailure()) { NfcConstant.INTERRUPT_STATUS @@ -356,7 +356,7 @@ class Connection(val isoDep: IsoDep, private val mCommandGenerator: CommandGener mCardType, CommandType.VERIFY_PIN, hasOpenSafeChannel, "06$pinHex" ) val res = command.send(this) - printLog(TAG, "<--- startVerifyPin end: ${res.getCode()} ${res.data} area:${mCommandArea}") + printLog(TAG, "<--- startVerifyPin end: ${res.getCode()} area:${mCommandArea}") return if (res.isConnectFailure()) { NfcConstant.INTERRUPT_STATUS @@ -385,7 +385,7 @@ class Connection(val isoDep: IsoDep, private val mCommandGenerator: CommandGener mCardType, CommandType.BACKUP_DATA, hasOpenSafeChannel, mnemonic ) val res = command.send(this) - printLog(TAG, "<--- backupData end: ${res.getCode()} ${res.data} area:${mCommandArea}") + printLog(TAG, "<--- backupData end: ${res.getCode()} area:${mCommandArea}") if (!res.isSuccess()) { return false diff --git a/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/utils/LogUtil.kt b/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/utils/LogUtil.kt index a64f7c0e..58d43ad4 100644 --- a/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/utils/LogUtil.kt +++ b/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/utils/LogUtil.kt @@ -1,11 +1,25 @@ package com.onekeyfe.reactnativelitecard.utils -import com.onekeyfe.reactnativelitecard.onekeyLite.NfcConstant -import com.onekeyfe.reactnativelitecard.LoggerManager +import android.content.pm.ApplicationInfo +import com.margelo.nitro.nativelogger.OneKeyLog object LogUtil { + // Only enable detailed NFC/APDU logging in debug builds + // Uses FLAG_DEBUGGABLE from the host app's ApplicationInfo at runtime, + // rather than the library's BuildConfig which won't reflect the host's build type. + private val isDebug: Boolean by lazy { + try { + val app = Utils.getApp() + app != null && (app.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 + } catch (_: Exception) { + false + } + } + @JvmStatic fun printLog(tag: String, msg: String) { - if (NfcConstant.DEBUG) LoggerManager.getInstance()?.logInfo("$tag: $msg") + if (isDebug) { + OneKeyLog.debug(tag, msg) + } } } diff --git a/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/utils/MiUtil.kt b/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/utils/MiUtil.kt index a80952ef..58fedc2e 100644 --- a/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/utils/MiUtil.kt +++ b/native-modules/react-native-lite-card/android/src/main/java/com/onekeyfe/reactnativelitecard/utils/MiUtil.kt @@ -6,8 +6,8 @@ import android.content.Intent import android.net.Uri import android.os.Build import android.provider.Settings -import android.util.Log import androidx.annotation.IntDef +import com.margelo.nitro.nativelogger.OneKeyLog import androidx.annotation.RequiresApi import com.onekeyfe.reactnativelitecard.utils.MiUtil.PermissionResult.Companion.PERMISSION_ASK import com.onekeyfe.reactnativelitecard.utils.MiUtil.PermissionResult.Companion.PERMISSION_DENIED @@ -59,13 +59,13 @@ object MiUtil { //the ops of NFC is 10016,check /data/system/appops/xxx.xml val invoke = checkOpNoThrowMethod.invoke(mAppOps, 10016, uid, pkg) if (invoke == null) { - Log.d(TAG, + OneKeyLog.debug("LiteCard", "MIUI check permission checkOpNoThrowMethod(AppOpsManager) invoke result is null" ) return PERMISSION_UNKNOWN } val result = invoke.toString() - Log.d(TAG, + OneKeyLog.debug("LiteCard", "MIUI check permission checkOpNoThrowMethod(AppOpsManager) invoke result = $result" ) when (result) { @@ -74,7 +74,7 @@ object MiUtil { "5" -> return PERMISSION_ASK } } catch (e: Exception) { - Log.d(TAG, "check nfc permission fail ${e.message}", e) + OneKeyLog.debug("LiteCard", "check nfc permission fail: ${e.message}\n${e.stackTraceToString()}") } return PERMISSION_UNKNOWN } @@ -86,7 +86,7 @@ object MiUtil { context.startActivity(intent) true } catch (e: Exception) { - Log.d(TAG, "open app setting fail ${e.message}", e) + OneKeyLog.debug("LiteCard", "open app setting fail: ${e.message}\n${e.stackTraceToString()}") false } } diff --git a/native-modules/react-native-lite-card/ios/Classes/OKNFCBridge.mm b/native-modules/react-native-lite-card/ios/Classes/OKNFCBridge.mm index 71a68579..7891c4e7 100644 --- a/native-modules/react-native-lite-card/ios/Classes/OKNFCBridge.mm +++ b/native-modules/react-native-lite-card/ios/Classes/OKNFCBridge.mm @@ -17,6 +17,7 @@ #import "OKNFCBridge.h" +#import "LCLogger.h" @implementation OKNFCBridge @@ -44,6 +45,7 @@ + (NFCISO7816APDU *)buildAPDUWith_cla:(unsigned long)cla } NSString *hexStr = [NSString stringWithCString:c_apdu encoding:NSUTF8StringEncoding]; + JUB_FreeMemory(c_apdu); NSData *APDUData = [hexStr dataFromHexString]; NFCISO7816APDU *apdu = [[NFCISO7816APDU alloc] initWithData:APDUData]; return apdu; @@ -84,6 +86,8 @@ + (BOOL)parseAPDUResponse:(NSData *)data { return NO; } + JUB_FreeMemory(value); + JUB_UINT16 wRet = 0; JUB_CHAR_PTR pDecResp = nullptr; rv = JUB_GPC_ParseAPDUResponse(c_data,&wRet, &pDecResp); @@ -130,7 +134,9 @@ + (NSString *)mutualAuthData { return nil; } - return [NSString stringWithFormat:@"%s", mutualAuthData]; + NSString *result = [NSString stringWithFormat:@"%s", mutualAuthData]; + JUB_FreeMemory(mutualAuthData); + return result; } + (BOOL)openChannel:(NSData *)authRes { @@ -139,10 +145,10 @@ + (BOOL)openChannel:(NSData *)authRes { rv = JUB_GPC_OpenSecureChannel(authResp); if (JUBR_OK != rv) { - NSLog(@"error: JUB_GPC_OpenSecureChannel"); + [LCLogger error:@"JUB_GPC_OpenSecureChannel failed"]; return NO; } - NSLog(@"OKNFC: openChannel success."); + [LCLogger debug:@"openChannel success"]; return YES; } @@ -161,10 +167,11 @@ + (BOOL)JUB_GPC_Initialize:(NSData *)cert { JUB_CHAR_PTR subjectID = nullptr; rv = JUB_GPC_ParseCertificate(value, &sn, &subjectID); if (JUBR_OK != rv) { + JUB_FreeMemory(value); return NO; } - NSLog(@"OKNFC: subjectID: %s",subjectID); + [LCLogger debug:@"subjectID parsed"]; GPC_SCP11_SHAREDINFO shareInfo; shareInfo.scpID = (char *)"1107"; shareInfo.keyUsage = (char *)"3C"; @@ -178,11 +185,15 @@ + (BOOL)JUB_GPC_Initialize:(NSData *)cert { rv = JUB_GPC_Initialize(shareInfo, [NFCConfig envFor:@"crt"].UTF8String, sk); + JUB_FreeMemory(value); + JUB_FreeMemory(sn); + JUB_FreeMemory(subjectID); + if (JUBR_OK != rv) { return NO; } - NSLog(@"OKNFC: JUB_GPC_Initialize OK."); + [LCLogger debug:@"JUB_GPC_Initialize OK"]; return YES; } @@ -205,6 +216,7 @@ + (BOOL)verifySN:(NSString *)cardSN withCert:(NSData *)cert { JUB_CHAR_PTR subjectID = nullptr; rv = JUB_GPC_ParseCertificate(value, &sn, &subjectID); if (JUBR_OK != rv) { + JUB_FreeMemory(value); return NO; } @@ -212,7 +224,11 @@ + (BOOL)verifySN:(NSString *)cardSN withCert:(NSData *)cert { BOOL identical = [certSN isEqualToString:cardSN]; - NSLog(@"OKNFC: certSN: %@; cardSN: %@; identical: %@",certSN, cardSN, identical ? @"YES✅" : @"NO❌"); + JUB_FreeMemory(value); + JUB_FreeMemory(sn); + JUB_FreeMemory(subjectID); + + [LCLogger debug:[NSString stringWithFormat:@"certSN verification: %@", identical ? @"PASS" : @"FAIL"]]; return identical; } diff --git a/native-modules/react-native-lite-card/ios/Classes/OKNFTLite/OKLiteV1.m b/native-modules/react-native-lite-card/ios/Classes/OKNFTLite/OKLiteV1.m index 52a3c7bc..befcd077 100644 --- a/native-modules/react-native-lite-card/ios/Classes/OKNFTLite/OKLiteV1.m +++ b/native-modules/react-native-lite-card/ios/Classes/OKNFTLite/OKLiteV1.m @@ -8,6 +8,7 @@ #import "OKLiteV1.h" #import "OKNFCUtility.h" #import "OKLiteCommandModal.h" +#import "LCLogger.h" @interface OKLiteV1() @@ -214,7 +215,7 @@ - (BOOL)resetSync { dispatch_semaphore_signal(sema); }]; - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); return success; } @@ -344,7 +345,7 @@ - (BOOL)selectNFCAppWith:(OKLiteCommand)command { selectSuccess = sw1 == OKNFC_SW1_OK; dispatch_semaphore_signal(sema); }]; - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); if (selectSuccess) { self.selectNFCApp = app; } else { @@ -371,7 +372,7 @@ - (BOOL)openSecureChannel { dispatch_semaphore_signal(sema); }]; }]; - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); return success; } @@ -389,7 +390,7 @@ - (NSString *)getSN { SN = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]; dispatch_semaphore_signal(sema); }]; - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); return SN; } @@ -425,7 +426,7 @@ - (OKNFCLitePINVerifyResult)verifyPin:(NSString *)pin { result = OKNFCLitePINVerifyResultPass; dispatch_semaphore_signal(sema); }]; - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); return result; } @@ -450,26 +451,26 @@ - (BOOL)verifyCert { isVerificationPass = YES; if (sw1 != OKNFC_SW1_OK) { - NSLog(@"OKNFC: 获取证书失败"); + [LCLogger error:@"获取证书失败"]; isVerificationPass = NO; dispatch_semaphore_signal(sema); return; } if (![OKNFCBridge verifySN:self.SN withCert:certData]) { - NSLog(@"OKNFC: 验证证书失败"); + [LCLogger error:@"验证证书失败"]; isVerificationPass = NO; } if (![OKNFCBridge JUB_GPC_Initialize:certData]) { - NSLog(@"OKNFC: 初始化安全通道失败"); + [LCLogger error:@"初始化安全通道失败"]; isVerificationPass = NO; } dispatch_semaphore_signal(sema); }]; - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); self.certVerified = isVerificationPass; return isVerificationPass; @@ -486,12 +487,12 @@ - (NSInteger)getPINStatus { modal:modal completionHandler:^(NSData * _Nonnull responseData, uint8_t sw1, uint8_t sw2, NSError * _Nullable error, NSString * _Nonnull parseRespon) { if (sw1 != OKNFC_SW1_OK) { - NSLog(@"OKNFC: 获取 PIN 状态失败"); + [LCLogger error:@"获取 PIN 状态失败"]; dispatch_semaphore_signal(sema); return; } if (responseData.toHexString.intValue == 2) { - NSLog(@"OKNFC: 未设置 PIN"); + [LCLogger debug:@"未设置 PIN"]; pinStatus = OKNFC_PIN_UNSET; dispatch_semaphore_signal(sema); return; @@ -509,7 +510,7 @@ - (NSInteger)getPINStatus { } }]; - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); return pinStatus; } @@ -527,7 +528,7 @@ - (BOOL)setMnc:(NSString *)mnc { dispatch_semaphore_signal(sema); }]; - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); return success; } @@ -551,7 +552,7 @@ - (NSString *)getMnc { dispatch_semaphore_signal(sema); }]; - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); return mnc; } @@ -568,11 +569,11 @@ - (BOOL)setPin:(NSString *)pin { return; } success = YES; - NSLog(@"OKNFC: 成功设置 PIN = %@", pin); + [LCLogger debug:@"成功设置 PIN"]; dispatch_semaphore_signal(sema); }]; - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); return success; } @@ -597,11 +598,11 @@ - (OKNFCLiteChangePinResult)setNewPin:(NSString *)newPin withOldPin:(NSString *) return; } result = OKNFCLiteChangePinResultPass; - NSLog(@"OKNFC: 成功修改 PIN = %@", newPin); + [LCLogger debug:@"成功修改 PIN"]; dispatch_semaphore_signal(sema); }]; - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); return result; } diff --git a/native-modules/react-native-lite-card/ios/Classes/OKNFTLite/OKNFCManager.m b/native-modules/react-native-lite-card/ios/Classes/OKNFTLite/OKNFCManager.m index 7e3c2909..6f21903f 100644 --- a/native-modules/react-native-lite-card/ios/Classes/OKNFTLite/OKNFCManager.m +++ b/native-modules/react-native-lite-card/ios/Classes/OKNFTLite/OKNFCManager.m @@ -9,6 +9,7 @@ #import "OKNFCBridge.h" #import "OKNFCUtility.h" #import +#import "LCLogger.h" //#import "OKNFCHintViewController.h" //#import "OKMnemonic.h" #import "NSString+OKAdd.h" @@ -82,15 +83,15 @@ - (void)beginNewNFCSession { #pragma mark - NFCTagReaderSessionDelegate - (void)tagReaderSession:(NFCTagReaderSession *)session didDetectTags:(NSArray<__kindof id> *)tags { - NSLog(@"OKNFC tagReaderSession didDetectTags %@", tags); + [LCLogger debug:@"tagReaderSession didDetectTags"]; id tag = [tags.firstObject asNFCISO7816Tag]; if (!tag) { return; } [session connectToTag:tag completionHandler:^(NSError * _Nullable error) { if (error) { - NSString *errMsg = [NSString stringWithFormat:@"OKNFC connectToTag %@", error]; - NSLog(@"%@", errMsg); + NSString *errMsg = [NSString stringWithFormat:@"connectToTag error: %@", error.localizedDescription]; + [LCLogger error:errMsg]; // [kTools debugTipMessage:errMsg]; [self endNFCSessionWithError:YES]; return; @@ -100,7 +101,7 @@ - (void)tagReaderSession:(NFCTagReaderSession *)session didDetectTags:(NSArray<_ } - (void)tagReaderSession:(NFCTagReaderSession *)session didInvalidateWithError:(NSError *)error { - NSLog(@"OKNFC tagReaderSession didInvalidateWithError %@", error); + [LCLogger debug:[NSString stringWithFormat:@"tagReaderSession didInvalidateWithError: %@", error.localizedDescription]]; if (error.code == 200 || error.code == 6) { switch (self.sessionType) { case OKNFCLiteSessionTypeGetInfo: @@ -144,7 +145,7 @@ - (void)tagReaderSession:(NFCTagReaderSession *)session didInvalidateWithError:( } - (void)tagReaderSessionDidBecomeActive:(NFCTagReaderSession *)session { - NSLog(@"OKNFC tagReaderSessionDidBecomeActive %@", session); + [LCLogger debug:@"tagReaderSessionDidBecomeActive"]; } #pragma mark - Tasks @@ -237,7 +238,12 @@ - (void)setMnemonic:(NSString *)mnemonic - (void)_setMnemonic:(BOOL)force { SetMnemonicCallback callback = [_completionBlocks objectForKey:kSetMnemonicBlock]; - [self.lite setMnemonic:self.exportMnemonic withPin:self.pin overwrite:force complete:callback]; + NSString *mnemonic = self.exportMnemonic; + NSString *pin = self.pin; + // Clear sensitive data from properties immediately + self.exportMnemonic = nil; + self.pin = nil; + [self.lite setMnemonic:mnemonic withPin:pin overwrite:force complete:callback]; } #pragma mark - getMnemonic @@ -254,7 +260,10 @@ - (void)getMnemonicWithPin:(NSString *)pin complete:(GetMnemonicCallback)complet - (void)_getMnemonic { GetMnemonicCallback callback = [_completionBlocks objectForKey:kGetMnemonicBlock]; - [self.lite getMnemonicWithPin:self.pin complete:callback]; + NSString *pin = self.pin; + // Clear sensitive data from property immediately + self.pin = nil; + [self.lite getMnemonicWithPin:pin complete:callback]; } #pragma mark - changePin @@ -269,7 +278,12 @@ - (void)changePin:(NSString *)oldPin to:(NSString *)newPin complete:(ChangePinCa - (void)_changePin { ChangePinCallback callback = [_completionBlocks objectForKey:kChangePinBlock]; - [self.lite changePin:self.pin to:self.neoPin complete:callback]; + NSString *oldPin = self.pin; + NSString *newPin = self.neoPin; + // Clear sensitive data from properties immediately + self.pin = nil; + self.neoPin = nil; + [self.lite changePin:oldPin to:newPin complete:callback]; } #pragma mark - reset @@ -306,7 +320,7 @@ -(BOOL)checkLiteVersion { dispatch_semaphore_signal(sema); }]; - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); if(success) { self.lite = [[OKLiteV1 alloc] initWithDelegate:self]; @@ -317,7 +331,7 @@ -(BOOL)checkLiteVersion { success = sw1 == OKNFC_SW1_OK; dispatch_semaphore_signal(sema); }]; - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); if (success) { self.lite = [[OKLiteV2 alloc] initWithDelegate:self]; } diff --git a/native-modules/react-native-lite-card/ios/Classes/Utils/LCLogger.h b/native-modules/react-native-lite-card/ios/Classes/Utils/LCLogger.h new file mode 100644 index 00000000..9e4153e2 --- /dev/null +++ b/native-modules/react-native-lite-card/ios/Classes/Utils/LCLogger.h @@ -0,0 +1,16 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Lightweight logging wrapper that dynamically dispatches to OneKeyLog. +/// Avoids `@import ReactNativeNativeLogger` which fails when C++ modules are disabled. +@interface LCLogger : NSObject + ++ (void)debug:(NSString *)message; ++ (void)info:(NSString *)message; ++ (void)warn:(NSString *)message; ++ (void)error:(NSString *)message; + +@end + +NS_ASSUME_NONNULL_END diff --git a/native-modules/react-native-lite-card/ios/Classes/Utils/LCLogger.m b/native-modules/react-native-lite-card/ios/Classes/Utils/LCLogger.m new file mode 100644 index 00000000..96673bd6 --- /dev/null +++ b/native-modules/react-native-lite-card/ios/Classes/Utils/LCLogger.m @@ -0,0 +1,42 @@ +#import "LCLogger.h" + +static NSString *const kTag = @"LiteCard"; + +@implementation LCLogger + ++ (void)debug:(NSString *)message { + [self _log:@"debug::" message:message]; +} + ++ (void)info:(NSString *)message { + [self _log:@"info::" message:message]; +} + ++ (void)warn:(NSString *)message { + [self _log:@"warn::" message:message]; +} + ++ (void)error:(NSString *)message { + [self _log:@"error::" message:message]; +} + +#pragma mark - Private + ++ (void)_log:(NSString *)selectorName message:(NSString *)message { + Class logClass = NSClassFromString(@"ReactNativeNativeLogger.OneKeyLog"); + if (!logClass) { + logClass = NSClassFromString(@"OneKeyLog"); + } + if (!logClass) { + return; + } + SEL sel = NSSelectorFromString(selectorName); + if (![logClass respondsToSelector:sel]) { + return; + } + typedef void (*LogFunc)(id, SEL, NSString *, NSString *); + LogFunc func = (LogFunc)[logClass methodForSelector:sel]; + func(logClass, sel, kTag, message); +} + +@end diff --git a/native-modules/react-native-lite-card/ios/Classes/Utils/OKNFCUtility.m b/native-modules/react-native-lite-card/ios/Classes/Utils/OKNFCUtility.m index f07cf4aa..2766b244 100644 --- a/native-modules/react-native-lite-card/ios/Classes/Utils/OKNFCUtility.m +++ b/native-modules/react-native-lite-card/ios/Classes/Utils/OKNFCUtility.m @@ -6,16 +6,17 @@ // #import "OKNFCUtility.h" +#import "LCLogger.h" @implementation OKNFCUtility + (void)logAPDU:(NSString *)name response:(NSData *)responseData sw1:(uint8_t)sw1 sw2:(uint8_t)sw2 error:(NSError *)error { BOOL ok = sw1 == OKNFC_SW1_OK; - NSString *sw1e = ok ? @"成功 ✅" : @"失败 ❌"; - NSString *msg = [NSString stringWithFormat:@"OKNFC: Log %@: %@ \n-> sw1: %d(%x) sw2: %d(%x) err:%@ \n-> responseData: %@", name, sw1e, sw1, sw1, sw2, sw2, error, responseData]; - NSLog(@"%@", msg); - if (!ok) { -// [kTools debugTipMessage:msg]; + NSString *msg = [NSString stringWithFormat:@"APDU %@: %@ sw1:%d(%x) sw2:%d(%x)", name, ok ? @"OK" : @"FAIL", sw1, sw1, sw2, sw2]; + if (ok) { + [LCLogger debug:msg]; + } else { + [LCLogger error:msg]; } } diff --git a/native-modules/react-native-lite-card/ios/ReactNativeLiteCard.mm b/native-modules/react-native-lite-card/ios/ReactNativeLiteCard.mm index 4bd1c9b0..c846e606 100644 --- a/native-modules/react-native-lite-card/ios/ReactNativeLiteCard.mm +++ b/native-modules/react-native-lite-card/ios/ReactNativeLiteCard.mm @@ -3,6 +3,7 @@ #import "NFCConfig.h" #import "OKNFCManager.h" #import "OKLiteV1.h" +#import "LCLogger.h" typedef NS_ENUM(NSInteger, NFCLiteExceptions) { NFCLiteExceptionsInitChannel = 1000,// 初始化异常 @@ -34,6 +35,7 @@ - (NSNumber *)multiply:(double)a b:(double)b { - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { + [LCLogger info:@"ReactNativeLiteCard module initialized"]; return std::make_shared(params); } diff --git a/native-modules/react-native-lite-card/package.json b/native-modules/react-native-lite-card/package.json index a036df2d..7c4f54c8 100644 --- a/native-modules/react-native-lite-card/package.json +++ b/native-modules/react-native-lite-card/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-lite-card", - "version": "1.1.19", + "version": "1.1.24", "description": "lite card", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/native-modules/react-native-perf-memory/.gitignore b/native-modules/react-native-perf-memory/.gitignore new file mode 100644 index 00000000..357a2f9a --- /dev/null +++ b/native-modules/react-native-perf-memory/.gitignore @@ -0,0 +1,89 @@ +# OSX +# +.DS_Store + +# XDE +.expo/ + +# VSCode +.vscode/ +jsconfig.json + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace +**/.xcode.env.local + +# Android/IJ +# +.classpath +.cxx +.gradle +.idea +.project +.settings +local.properties +android.iml + +# Cocoapods +# +example/ios/Pods + +# Ruby +example/vendor/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +android/app/libs +android/keystores/debug.keystore + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Expo +.expo/ + +# Turborepo +.turbo/ + +# generated by bob +lib/ + +# React Native Codegen +ios/generated +android/generated + +# React Native Nitro Modules +nitrogen/ + + + diff --git a/native-modules/react-native-perf-memory/LICENSE b/native-modules/react-native-perf-memory/LICENSE new file mode 100644 index 00000000..24ee4114 --- /dev/null +++ b/native-modules/react-native-perf-memory/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 OneKey + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/native-modules/react-native-perf-memory/ReactNativePerfMemory.podspec b/native-modules/react-native-perf-memory/ReactNativePerfMemory.podspec new file mode 100644 index 00000000..37f4fb18 --- /dev/null +++ b/native-modules/react-native-perf-memory/ReactNativePerfMemory.podspec @@ -0,0 +1,30 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "ReactNativePerfMemory" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => min_ios_version_supported } + s.source = { :git => "https://github.com/OneKeyHQ/app-modules/react-native-perf-memory.git", :tag => "#{s.version}" } + + s.source_files = [ + "ios/**/*.{swift}", + "ios/**/*.{m,mm}", + "cpp/**/*.{hpp,cpp}", + ] + + s.dependency 'React-jsi' + s.dependency 'React-callinvoker' + s.dependency 'ReactNativeNativeLogger' + + load 'nitrogen/generated/ios/ReactNativePerfMemory+autolinking.rb' + add_nitrogen_files(s) + + install_modules_dependencies(s) +end diff --git a/native-modules/react-native-perf-memory/android/CMakeLists.txt b/native-modules/react-native-perf-memory/android/CMakeLists.txt new file mode 100644 index 00000000..7291ddda --- /dev/null +++ b/native-modules/react-native-perf-memory/android/CMakeLists.txt @@ -0,0 +1,24 @@ +project(reactnativeperfmemory) +cmake_minimum_required(VERSION 3.9.0) + +set(PACKAGE_NAME reactnativeperfmemory) +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_CXX_STANDARD 20) + +# Define C++ library and add all sources +add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp) + +# Add Nitrogen specs :) +include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/reactnativeperfmemory+autolinking.cmake) + +# Set up local includes +include_directories("src/main/cpp" "../cpp") + +find_library(LOG_LIB log) + +# Link all libraries together +target_link_libraries( + ${PACKAGE_NAME} + ${LOG_LIB} + android # <-- Android core +) diff --git a/native-modules/react-native-perf-memory/android/build.gradle b/native-modules/react-native-perf-memory/android/build.gradle new file mode 100644 index 00000000..854ecf5d --- /dev/null +++ b/native-modules/react-native-perf-memory/android/build.gradle @@ -0,0 +1,130 @@ +buildscript { + ext.getExtOrDefault = {name -> + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['ReactNativePerfMemory_' + name] + } + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:8.7.2" + // noinspection DifferentKotlinGradleVersion + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}" + } +} + +def reactNativeArchitectures() { + def value = rootProject.getProperties().get("reactNativeArchitectures") + return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" +apply from: '../nitrogen/generated/android/reactnativeperfmemory+autolinking.gradle' + +apply plugin: "com.facebook.react" + +def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ReactNativePerfMemory_" + name]).toInteger() +} + +android { + namespace "com.margelo.nitro.reactnativeperfmemory" + + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + + defaultConfig { + minSdkVersion getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + + externalNativeBuild { + cmake { + cppFlags "-frtti -fexceptions -Wall -fstack-protector-all" + arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" + abiFilters (*reactNativeArchitectures()) + + buildTypes { + debug { + cppFlags "-O1 -g" + } + release { + cppFlags "-O2" + } + } + } + } + } + + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } + } + + packagingOptions { + excludes = [ + "META-INF", + "META-INF/**", + "**/libc++_shared.so", + "**/libfbjni.so", + "**/libjsi.so", + "**/libfolly_json.so", + "**/libfolly_runtime.so", + "**/libglog.so", + "**/libhermes.so", + "**/libhermes-executor-debug.so", + "**/libhermes_executor.so", + "**/libreactnative.so", + "**/libreactnativejni.so", + "**/libturbomodulejsijni.so", + "**/libreact_nativemodule_core.so", + "**/libjscexecutor.so" + ] + } + + buildFeatures { + buildConfig true + prefab true + } + + buildTypes { + release { + minifyEnabled false + } + } + + lintOptions { + disable "GradleCompatible" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs += [ + "generated/java", + "generated/jni" + ] + } + } +} + +repositories { + mavenCentral() + google() +} + +def kotlin_version = getExtOrDefault("kotlinVersion") + +dependencies { + implementation "com.facebook.react:react-android" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation project(":react-native-nitro-modules") + + implementation project(":onekeyfe_react-native-native-logger") +} diff --git a/native-modules/react-native-perf-memory/android/gradle.properties b/native-modules/react-native-perf-memory/android/gradle.properties new file mode 100644 index 00000000..db7241f6 --- /dev/null +++ b/native-modules/react-native-perf-memory/android/gradle.properties @@ -0,0 +1,4 @@ +ReactNativePerfMemory_kotlinVersion=1.9.25 +ReactNativePerfMemory_compileSdkVersion=35 +ReactNativePerfMemory_targetSdkVersion=35 +ReactNativePerfMemory_minSdkVersion=24 diff --git a/native-modules/react-native-perf-memory/android/src/main/AndroidManifest.xml b/native-modules/react-native-perf-memory/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..94cbbcfc --- /dev/null +++ b/native-modules/react-native-perf-memory/android/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/native-modules/react-native-perf-memory/android/src/main/cpp/cpp-adapter.cpp b/native-modules/react-native-perf-memory/android/src/main/cpp/cpp-adapter.cpp new file mode 100644 index 00000000..ff6b6078 --- /dev/null +++ b/native-modules/react-native-perf-memory/android/src/main/cpp/cpp-adapter.cpp @@ -0,0 +1,6 @@ +#include +#include "reactnativeperfmemoryOnLoad.hpp" + +extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { + return margelo::nitro::reactnativeperfmemory::initialize(vm); +} diff --git a/native-modules/react-native-perf-memory/android/src/main/java/com/margelo/nitro/reactnativeperfmemory/ReactNativePerfMemory.kt b/native-modules/react-native-perf-memory/android/src/main/java/com/margelo/nitro/reactnativeperfmemory/ReactNativePerfMemory.kt new file mode 100644 index 00000000..91a47395 --- /dev/null +++ b/native-modules/react-native-perf-memory/android/src/main/java/com/margelo/nitro/reactnativeperfmemory/ReactNativePerfMemory.kt @@ -0,0 +1,67 @@ +package com.margelo.nitro.reactnativeperfmemory + +import android.app.ActivityManager +import android.content.Context +import android.os.Debug +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.Promise +import com.margelo.nitro.NitroModules +import com.margelo.nitro.nativelogger.OneKeyLog +import java.io.BufferedReader +import java.io.FileReader + +@DoNotStrip +class ReactNativePerfMemory : HybridReactNativePerfMemorySpec() { + + override fun getMemoryUsage(): Promise { + return Promise.async { + val rssBytes = readVmRssBytesFromProcStatus() + + val context = NitroModules.applicationContext + val am = context?.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager + + if (am == null) { + if (rssBytes != null) { + return@async MemoryUsage(rss = rssBytes.toDouble()) + } + OneKeyLog.warn("PerfMemory", "ActivityManager unavailable and /proc/self/status unreadable") + return@async MemoryUsage(rss = 0.0) + } + + val pid = android.os.Process.myPid() + val memInfos = am.getProcessMemoryInfo(intArrayOf(pid)) + + if (memInfos == null || memInfos.isEmpty()) { + if (rssBytes != null) { + return@async MemoryUsage(rss = rssBytes.toDouble()) + } + return@async MemoryUsage(rss = 0.0) + } + + // totalPss is in KB + val pssBytes = memInfos[0].totalPss.toLong() * 1024L + val finalRss = rssBytes ?: pssBytes + + MemoryUsage(rss = finalRss.toDouble()) + } + } + + private fun readVmRssBytesFromProcStatus(): Long? { + try { + BufferedReader(FileReader("/proc/self/status")).use { reader -> + var line: String? + while (reader.readLine().also { line = it } != null) { + if (!line!!.startsWith("VmRSS:")) continue + // Example: "VmRSS:\t 123456 kB" + val parts = line!!.trim().split("\\s+".toRegex()) + if (parts.size < 2) return null + val kb = parts[1].toLong() + return kb * 1024L + } + } + } catch (_: Exception) { + // ignore + } + return null + } +} diff --git a/native-modules/react-native-perf-memory/android/src/main/java/com/margelo/nitro/reactnativeperfmemory/ReactNativePerfMemoryPackage.kt b/native-modules/react-native-perf-memory/android/src/main/java/com/margelo/nitro/reactnativeperfmemory/ReactNativePerfMemoryPackage.kt new file mode 100644 index 00000000..b6f1b036 --- /dev/null +++ b/native-modules/react-native-perf-memory/android/src/main/java/com/margelo/nitro/reactnativeperfmemory/ReactNativePerfMemoryPackage.kt @@ -0,0 +1,24 @@ +package com.margelo.nitro.reactnativeperfmemory + +import com.facebook.react.BaseReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfoProvider + +class ReactNativePerfMemoryPackage : BaseReactPackage() { + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return null + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { HashMap() } + } + + companion object { + init { + System.loadLibrary("reactnativeperfmemory") + } + } +} + + diff --git a/native-modules/react-native-perf-memory/babel.config.js b/native-modules/react-native-perf-memory/babel.config.js new file mode 100644 index 00000000..603d3331 --- /dev/null +++ b/native-modules/react-native-perf-memory/babel.config.js @@ -0,0 +1,13 @@ +module.exports = { + presets: ['@react-native/babel-preset'], + plugins: [ + [ + 'module-resolver', + { + alias: { + 'react-native-perf-memory': './src/index', + }, + }, + ], + ], +}; diff --git a/native-modules/react-native-perf-memory/eslint.config.mjs b/native-modules/react-native-perf-memory/eslint.config.mjs new file mode 100644 index 00000000..3416cf5c --- /dev/null +++ b/native-modules/react-native-perf-memory/eslint.config.mjs @@ -0,0 +1,5 @@ +import { createEslintConfig } from '@react-native/eslint-config'; + +export default createEslintConfig({ + extends: ['@react-native/eslint-config'], +}); diff --git a/native-modules/react-native-perf-memory/ios/ReactNativePerfMemory.swift b/native-modules/react-native-perf-memory/ios/ReactNativePerfMemory.swift new file mode 100644 index 00000000..79ec4370 --- /dev/null +++ b/native-modules/react-native-perf-memory/ios/ReactNativePerfMemory.swift @@ -0,0 +1,41 @@ +import NitroModules +import ReactNativeNativeLogger + +class ReactNativePerfMemory: HybridReactNativePerfMemorySpec { + + func getMemoryUsage() throws -> Promise { + return Promise.async { + // Prefer phys_footprint when available (closest to "real" memory impact) + var vmInfo = task_vm_info_data_t() + var count = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) + + let kr = withUnsafeMutablePointer(to: &vmInfo) { ptr in + ptr.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { intPtr in + task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), intPtr, &count) + } + } + + if kr == KERN_SUCCESS { + let footprint = Double(vmInfo.phys_footprint) + return MemoryUsage(rss: footprint) + } + + // Fallback: resident size via mach_task_basic_info + var basicInfo = mach_task_basic_info_data_t() + var basicCount = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) + + let basicKr = withUnsafeMutablePointer(to: &basicInfo) { ptr in + ptr.withMemoryRebound(to: integer_t.self, capacity: Int(basicCount)) { intPtr in + task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), intPtr, &basicCount) + } + } + + if basicKr == KERN_SUCCESS { + return MemoryUsage(rss: Double(basicInfo.resident_size)) + } + + OneKeyLog.warn("PerfMemory", "Failed to get memory usage, kern_return: \(kr)") + return MemoryUsage(rss: 0) + } + } +} diff --git a/native-modules/react-native-perf-memory/lefthook.yml b/native-modules/react-native-perf-memory/lefthook.yml new file mode 100644 index 00000000..89766b1f --- /dev/null +++ b/native-modules/react-native-perf-memory/lefthook.yml @@ -0,0 +1,10 @@ +pre-commit: + parallel: true + commands: + lint: + glob: "*.{js,ts,tsx}" + run: yarn lint {staged_files} + typecheck: + files: git diff --cached --name-only --diff-filter=ACMRTUXB + glob: "*.{js,ts,tsx}" + run: yarn typecheck diff --git a/native-modules/react-native-perf-memory/nitro.json b/native-modules/react-native-perf-memory/nitro.json new file mode 100644 index 00000000..4e5e7556 --- /dev/null +++ b/native-modules/react-native-perf-memory/nitro.json @@ -0,0 +1,17 @@ +{ + "cxxNamespace": ["reactnativeperfmemory"], + "ios": { + "iosModuleName": "ReactNativePerfMemory" + }, + "android": { + "androidNamespace": ["reactnativeperfmemory"], + "androidCxxLibName": "reactnativeperfmemory" + }, + "autolinking": { + "ReactNativePerfMemory": { + "swift": "ReactNativePerfMemory", + "kotlin": "ReactNativePerfMemory" + } + }, + "ignorePaths": ["node_modules"] +} diff --git a/native-modules/react-native-perf-memory/package.json b/native-modules/react-native-perf-memory/package.json new file mode 100644 index 00000000..ccbea11c --- /dev/null +++ b/native-modules/react-native-perf-memory/package.json @@ -0,0 +1,169 @@ +{ + "name": "@onekeyfe/react-native-perf-memory", + "version": "1.1.24", + "description": "react-native-perf-memory", + "main": "./lib/module/index.js", + "types": "./lib/typescript/src/index.d.ts", + "exports": { + ".": { + "source": "./src/index.tsx", + "types": "./lib/typescript/src/index.d.ts", + "default": "./lib/module/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "src", + "lib", + "android", + "ios", + "cpp", + "nitrogen", + "nitro.json", + "*.podspec", + "react-native.config.js", + "!ios/build", + "!android/build", + "!android/gradle", + "!android/gradlew", + "!android/gradlew.bat", + "!android/local.properties", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__", + "!**/.*" + ], + "scripts": { + "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", + "prepare": "bob build", + "nitrogen": "nitrogen", + "typecheck": "tsc", + "lint": "eslint \"**/*.{js,ts,tsx}\"", + "test": "jest", + "release": "yarn prepare && npm whoami && npm publish --access public" + }, + "keywords": [ + "react-native", + "ios", + "android" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/OneKeyHQ/app-modules/react-native-perf-memory.git" + }, + "author": "onekeyfe (https://github.com/OneKeyHQ/app-modules)", + "license": "MIT", + "bugs": { + "url": "https://github.com/OneKeyHQ/app-modules/react-native-perf-memory/issues" + }, + "homepage": "https://github.com/OneKeyHQ/app-modules/react-native-perf-memory#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "devDependencies": { + "@commitlint/config-conventional": "^19.8.1", + "@eslint/compat": "^1.3.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.35.0", + "@react-native/babel-preset": "0.83.0", + "@react-native/eslint-config": "0.83.0", + "@release-it/conventional-changelog": "^10.0.1", + "@types/jest": "^29.5.14", + "@types/react": "^19.2.0", + "commitlint": "^19.8.1", + "del-cli": "^6.0.0", + "eslint": "^9.35.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "jest": "^29.7.0", + "lefthook": "^2.0.3", + "nitrogen": "0.31.10", + "prettier": "^2.8.8", + "react": "19.2.0", + "react-native": "0.83.0", + "react-native-builder-bob": "^0.40.13", + "react-native-nitro-modules": "0.33.2", + "release-it": "^19.0.4", + "turbo": "^2.5.6", + "typescript": "^5.9.2" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-nitro-modules": "0.33.2" + }, + "react-native-builder-bob": { + "source": "src", + "output": "lib", + "targets": [ + [ + "custom", + { + "script": "nitrogen", + "clean": "nitrogen/" + } + ], + [ + "module", + { + "esm": true + } + ], + [ + "typescript", + { + "project": "tsconfig.build.json" + } + ] + ] + }, + "prettier": { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + }, + "jest": { + "preset": "react-native", + "modulePathIgnorePatterns": [ + "/example/node_modules", + "/lib/" + ] + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "release-it": { + "git": { + "commitMessage": "chore: release ${version}", + "tagName": "v${version}" + }, + "npm": { + "publish": true + }, + "github": { + "release": true + }, + "plugins": { + "@release-it/conventional-changelog": { + "preset": { + "name": "angular" + } + } + } + }, + "create-react-native-library": { + "type": "nitro-module", + "languages": "kotlin-swift", + "tools": [ + "eslint", + "jest", + "lefthook", + "release-it" + ], + "version": "0.56.0" + } +} diff --git a/native-modules/react-native-perf-memory/src/ReactNativePerfMemory.nitro.ts b/native-modules/react-native-perf-memory/src/ReactNativePerfMemory.nitro.ts new file mode 100644 index 00000000..2a4443d0 --- /dev/null +++ b/native-modules/react-native-perf-memory/src/ReactNativePerfMemory.nitro.ts @@ -0,0 +1,10 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +export interface MemoryUsage { + rss: number; +} + +export interface ReactNativePerfMemory + extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { + getMemoryUsage(): Promise; +} diff --git a/native-modules/react-native-perf-memory/src/index.tsx b/native-modules/react-native-perf-memory/src/index.tsx new file mode 100644 index 00000000..a44aac90 --- /dev/null +++ b/native-modules/react-native-perf-memory/src/index.tsx @@ -0,0 +1,8 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import type { ReactNativePerfMemory as ReactNativePerfMemoryType } from './ReactNativePerfMemory.nitro'; + +const ReactNativePerfMemoryHybridObject = + NitroModules.createHybridObject('ReactNativePerfMemory'); + +export const ReactNativePerfMemory = ReactNativePerfMemoryHybridObject; +export type * from './ReactNativePerfMemory.nitro'; diff --git a/native-modules/react-native-perf-memory/tsconfig.build.json b/native-modules/react-native-perf-memory/tsconfig.build.json new file mode 100644 index 00000000..45777014 --- /dev/null +++ b/native-modules/react-native-perf-memory/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "declarationMap": true + }, + "exclude": ["**/__tests__/**/*", "**/__fixtures__/**/*", "**/__mocks__/**/*"] +} diff --git a/native-modules/react-native-perf-memory/tsconfig.json b/native-modules/react-native-perf-memory/tsconfig.json new file mode 100644 index 00000000..e6b6cdec --- /dev/null +++ b/native-modules/react-native-perf-memory/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "rootDir": ".", + "paths": { + "react-native-perf-memory": ["./src/index"] + }, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "customConditions": ["react-native-strict-api"], + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "lib": ["ESNext"], + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext", + "verbatimModuleSyntax": true + } +} diff --git a/native-modules/react-native-perf-memory/turbo.json b/native-modules/react-native-perf-memory/turbo.json new file mode 100644 index 00000000..08b9676e --- /dev/null +++ b/native-modules/react-native-perf-memory/turbo.json @@ -0,0 +1,17 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "outputs": [ + "lib/**", + "nitrogen/**" + ], + "inputs": [ + "src/**", + "android/src/**", + "ios/**", + "cpp/**" + ] + } + } +} diff --git a/native-modules/react-native-splash-screen/.gitignore b/native-modules/react-native-splash-screen/.gitignore new file mode 100644 index 00000000..357a2f9a --- /dev/null +++ b/native-modules/react-native-splash-screen/.gitignore @@ -0,0 +1,89 @@ +# OSX +# +.DS_Store + +# XDE +.expo/ + +# VSCode +.vscode/ +jsconfig.json + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace +**/.xcode.env.local + +# Android/IJ +# +.classpath +.cxx +.gradle +.idea +.project +.settings +local.properties +android.iml + +# Cocoapods +# +example/ios/Pods + +# Ruby +example/vendor/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +android/app/libs +android/keystores/debug.keystore + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Expo +.expo/ + +# Turborepo +.turbo/ + +# generated by bob +lib/ + +# React Native Codegen +ios/generated +android/generated + +# React Native Nitro Modules +nitrogen/ + + + diff --git a/native-modules/react-native-splash-screen/LICENSE b/native-modules/react-native-splash-screen/LICENSE new file mode 100644 index 00000000..24ee4114 --- /dev/null +++ b/native-modules/react-native-splash-screen/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 OneKey + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/native-modules/react-native-splash-screen/ReactNativeSplashScreen.podspec b/native-modules/react-native-splash-screen/ReactNativeSplashScreen.podspec new file mode 100644 index 00000000..f379ef23 --- /dev/null +++ b/native-modules/react-native-splash-screen/ReactNativeSplashScreen.podspec @@ -0,0 +1,30 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "ReactNativeSplashScreen" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => min_ios_version_supported } + s.source = { :git => "https://github.com/OneKeyHQ/app-modules/react-native-splash-screen.git", :tag => "#{s.version}" } + + s.source_files = [ + "ios/**/*.{swift}", + "ios/**/*.{m,mm}", + "cpp/**/*.{hpp,cpp}", + ] + + s.dependency 'React-jsi' + s.dependency 'React-callinvoker' + s.dependency 'ReactNativeNativeLogger' + + load 'nitrogen/generated/ios/ReactNativeSplashScreen+autolinking.rb' + add_nitrogen_files(s) + + install_modules_dependencies(s) +end diff --git a/native-modules/react-native-splash-screen/android/CMakeLists.txt b/native-modules/react-native-splash-screen/android/CMakeLists.txt new file mode 100644 index 00000000..d5bafd6e --- /dev/null +++ b/native-modules/react-native-splash-screen/android/CMakeLists.txt @@ -0,0 +1,24 @@ +project(reactnativesplashscreen) +cmake_minimum_required(VERSION 3.9.0) + +set(PACKAGE_NAME reactnativesplashscreen) +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_CXX_STANDARD 20) + +# Define C++ library and add all sources +add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp) + +# Add Nitrogen specs :) +include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/reactnativesplashscreen+autolinking.cmake) + +# Set up local includes +include_directories("src/main/cpp" "../cpp") + +find_library(LOG_LIB log) + +# Link all libraries together +target_link_libraries( + ${PACKAGE_NAME} + ${LOG_LIB} + android # <-- Android core +) diff --git a/native-modules/react-native-splash-screen/android/build.gradle b/native-modules/react-native-splash-screen/android/build.gradle new file mode 100644 index 00000000..e496c1f7 --- /dev/null +++ b/native-modules/react-native-splash-screen/android/build.gradle @@ -0,0 +1,130 @@ +buildscript { + ext.getExtOrDefault = {name -> + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['ReactNativeSplashScreen_' + name] + } + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:8.7.2" + // noinspection DifferentKotlinGradleVersion + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}" + } +} + +def reactNativeArchitectures() { + def value = rootProject.getProperties().get("reactNativeArchitectures") + return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" +apply from: '../nitrogen/generated/android/reactnativesplashscreen+autolinking.gradle' + +apply plugin: "com.facebook.react" + +def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ReactNativeSplashScreen_" + name]).toInteger() +} + +android { + namespace "com.margelo.nitro.reactnativesplashscreen" + + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + + defaultConfig { + minSdkVersion getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + + externalNativeBuild { + cmake { + cppFlags "-frtti -fexceptions -Wall -fstack-protector-all" + arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" + abiFilters (*reactNativeArchitectures()) + + buildTypes { + debug { + cppFlags "-O1 -g" + } + release { + cppFlags "-O2" + } + } + } + } + } + + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } + } + + packagingOptions { + excludes = [ + "META-INF", + "META-INF/**", + "**/libc++_shared.so", + "**/libfbjni.so", + "**/libjsi.so", + "**/libfolly_json.so", + "**/libfolly_runtime.so", + "**/libglog.so", + "**/libhermes.so", + "**/libhermes-executor-debug.so", + "**/libhermes_executor.so", + "**/libreactnative.so", + "**/libreactnativejni.so", + "**/libturbomodulejsijni.so", + "**/libreact_nativemodule_core.so", + "**/libjscexecutor.so" + ] + } + + buildFeatures { + buildConfig true + prefab true + } + + buildTypes { + release { + minifyEnabled false + } + } + + lintOptions { + disable "GradleCompatible" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs += [ + "generated/java", + "generated/jni" + ] + } + } +} + +repositories { + mavenCentral() + google() +} + +def kotlin_version = getExtOrDefault("kotlinVersion") + +dependencies { + implementation "com.facebook.react:react-android" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation project(":react-native-nitro-modules") + + implementation project(":onekeyfe_react-native-native-logger") +} diff --git a/native-modules/react-native-splash-screen/android/gradle.properties b/native-modules/react-native-splash-screen/android/gradle.properties new file mode 100644 index 00000000..fdba749b --- /dev/null +++ b/native-modules/react-native-splash-screen/android/gradle.properties @@ -0,0 +1,4 @@ +ReactNativeSplashScreen_kotlinVersion=1.9.25 +ReactNativeSplashScreen_compileSdkVersion=35 +ReactNativeSplashScreen_targetSdkVersion=35 +ReactNativeSplashScreen_minSdkVersion=24 diff --git a/native-modules/react-native-splash-screen/android/src/main/AndroidManifest.xml b/native-modules/react-native-splash-screen/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..94cbbcfc --- /dev/null +++ b/native-modules/react-native-splash-screen/android/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/native-modules/react-native-splash-screen/android/src/main/cpp/cpp-adapter.cpp b/native-modules/react-native-splash-screen/android/src/main/cpp/cpp-adapter.cpp new file mode 100644 index 00000000..d0104976 --- /dev/null +++ b/native-modules/react-native-splash-screen/android/src/main/cpp/cpp-adapter.cpp @@ -0,0 +1,6 @@ +#include +#include "reactnativesplashscreenOnLoad.hpp" + +extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { + return margelo::nitro::reactnativesplashscreen::initialize(vm); +} diff --git a/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/ReactNativeSplashScreen.kt b/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/ReactNativeSplashScreen.kt new file mode 100644 index 00000000..4ea746e2 --- /dev/null +++ b/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/ReactNativeSplashScreen.kt @@ -0,0 +1,77 @@ +package com.margelo.nitro.reactnativesplashscreen + +import android.os.Build +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.NitroModules +import com.margelo.nitro.core.Promise +import com.margelo.nitro.nativelogger.OneKeyLog +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +/** + * Splash screen module for Android compatibility. + * + * - API >= 31 (Android 12+): The system AndroidX SplashScreen API handles the splash screen + * natively in MainActivity. Both methods are no-ops and return true immediately. + * + * - API < 31 (Android < 12): The system has no built-in splash screen API. + * MainActivity calls [SplashScreenBridge.show] during onCreate to display a view overlay + * matching the app's splash drawable. This module then delegates: + * - [preventAutoHideAsync] → [SplashScreenBridge.preventAutoHide] to keep the overlay visible + * until JS explicitly hides it. + * - [hideAsync] → [SplashScreenBridge.hide] to remove the overlay. + * + * iOS: expo-splash-screen via the launch storyboard handles everything. The iOS implementation + * in ReactNativeSplashScreen.swift is intentionally empty. + */ +@DoNotStrip +class ReactNativeSplashScreen : HybridReactNativeSplashScreenSpec() { + + override fun preventAutoHideAsync(): Promise { + return Promise.async { + OneKeyLog.info("SplashScreen", "preventAutoHideAsync OS_VERSION=${Build.VERSION.SDK_INT}") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // API 31+: AndroidX SplashScreen handles it in MainActivity + return@async true + } + val activity = NitroModules.applicationContext?.currentActivity + if (activity == null) { + OneKeyLog.warn("SplashScreen", "preventAutoHideAsync: no current activity") + return@async false + } + SplashScreenBridge.preventAutoHide(activity) + true + } + } + + override fun hideAsync(): Promise { + return Promise.async { + OneKeyLog.info("SplashScreen", "hideAsync OS_VERSION=${Build.VERSION.SDK_INT}") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // API 31+: AndroidX SplashScreen handles it + return@async true + } + val activity = NitroModules.applicationContext?.currentActivity + if (activity == null) { + OneKeyLog.warn("SplashScreen", "hideAsync: no current activity") + return@async false + } + val latch = CountDownLatch(1) + var result = false + SplashScreenBridge.hide( + activity, + onSuccess = { hasEffect -> + result = hasEffect + latch.countDown() + }, + onFailure = { reason -> + OneKeyLog.warn("SplashScreen", "hideAsync failed: $reason") + result = false + latch.countDown() + } + ) + latch.await(5, TimeUnit.SECONDS) + result + } + } +} diff --git a/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/ReactNativeSplashScreenPackage.kt b/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/ReactNativeSplashScreenPackage.kt new file mode 100644 index 00000000..17c78f9e --- /dev/null +++ b/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/ReactNativeSplashScreenPackage.kt @@ -0,0 +1,24 @@ +package com.margelo.nitro.reactnativesplashscreen + +import com.facebook.react.BaseReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfoProvider + +class ReactNativeSplashScreenPackage : BaseReactPackage() { + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return null + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { HashMap() } + } + + companion object { + init { + System.loadLibrary("reactnativesplashscreen") + } + } +} + + diff --git a/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/SplashImageResizeMode.kt b/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/SplashImageResizeMode.kt new file mode 100644 index 00000000..750ebb5e --- /dev/null +++ b/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/SplashImageResizeMode.kt @@ -0,0 +1,23 @@ +package com.margelo.nitro.reactnativesplashscreen + +import android.widget.ImageView + +internal enum class SplashImageResizeMode( + val scaleType: ImageView.ScaleType, + val value: String +) { + CONTAIN(ImageView.ScaleType.FIT_CENTER, "contain"), + COVER(ImageView.ScaleType.CENTER_CROP, "cover"), + NATIVE(ImageView.ScaleType.CENTER, "native"); + + companion object { + fun fromString(value: String?): SplashImageResizeMode { + if (value != null) { + for (mode in values()) { + if (mode.value == value) return mode + } + } + return CONTAIN + } + } +} diff --git a/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/SplashScreenBridge.kt b/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/SplashScreenBridge.kt new file mode 100644 index 00000000..65ff1b46 --- /dev/null +++ b/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/SplashScreenBridge.kt @@ -0,0 +1,113 @@ +package com.margelo.nitro.reactnativesplashscreen + +import android.app.Activity +import androidx.core.content.ContextCompat +import com.margelo.nitro.nativelogger.OneKeyLog +import java.util.WeakHashMap + +/** + * Static bridge called by the host app's MainActivity to show/manage the splash screen. + * + * Usage in MainActivity.onCreate() (for API < 31 only): + * SplashScreenBridge.show(this) + */ +object SplashScreenBridge { + private const val TAG = "SplashScreen" + + private val controllers = WeakHashMap() + + @Volatile + var isAlreadyHidden = false + + /** + * Show the splash screen overlay. Call this from MainActivity.onCreate() + * for API < Build.VERSION_CODES.S (API 31). + */ + @JvmStatic + fun show(activity: Activity) { + if (isAlreadyHidden) return + if (controllers.containsKey(activity)) { + OneKeyLog.warn(TAG, "show: already shown for this activity") + return + } + val splashView = createSplashView(activity) ?: run { + OneKeyLog.warn(TAG, "show: failed to create splash view") + return + } + val controller = SplashViewController(activity, splashView) + controllers[activity] = controller + controller.show() + OneKeyLog.info(TAG, "show: started for activity") + } + + @JvmStatic + fun preventAutoHide(activity: Activity) { + val controller = controllers[activity] + if (controller == null) { + OneKeyLog.warn(TAG, "preventAutoHide: no controller for activity") + return + } + controller.preventAutoHide() + } + + @JvmStatic + fun hide( + activity: Activity, + onSuccess: ((Boolean) -> Unit)?, + onFailure: ((String) -> Unit)? + ) { + val controller = controllers[activity] + if (controller == null) { + OneKeyLog.warn(TAG, "hide: no controller for activity") + onFailure?.invoke("No splash screen registered for this activity") + return + } + controller.hide( + onSuccess = { result -> + isAlreadyHidden = true + onSuccess?.invoke(result) + }, + onFailure = onFailure + ) + } + + private fun createSplashView(activity: Activity): android.view.View? { + return try { + val context = activity + + // Read resize mode from app string resource (convention: "expo_splash_screen_resize_mode") + val resizeModeStr = try { + val id = context.resources.getIdentifier( + "expo_splash_screen_resize_mode", "string", context.packageName + ) + if (id != 0) context.getString(id).lowercase() else "contain" + } catch (e: Exception) { "contain" } + + val resizeMode = SplashImageResizeMode.fromString(resizeModeStr) + + // For NATIVE mode use "splashscreen", otherwise use "splashscreen_image" + val drawableName = if (resizeMode == SplashImageResizeMode.NATIVE) "splashscreen" else "splashscreen_image" + val imageResId = context.resources.getIdentifier(drawableName, "drawable", context.packageName) + val bgColorId = context.resources.getIdentifier( + "splashscreen_background", "color", context.packageName + ) + + OneKeyLog.info(TAG, "createSplashView: resizeMode=$resizeModeStr drawable=$drawableName imageResId=$imageResId") + + val view = SplashScreenView(context) + if (bgColorId != 0) { + view.setBackgroundColor(ContextCompat.getColor(context, bgColorId)) + } else { + view.setBackgroundColor(android.graphics.Color.WHITE) + } + if (imageResId != 0) { + view.imageView.setImageResource(imageResId) + } + view.configureResizeMode(resizeMode) + view + } catch (e: Exception) { + OneKeyLog.error(TAG, "createSplashView failed: ${e.message}") + null + } + } +} diff --git a/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/SplashScreenView.kt b/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/SplashScreenView.kt new file mode 100644 index 00000000..ede3c63f --- /dev/null +++ b/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/SplashScreenView.kt @@ -0,0 +1,34 @@ +package com.margelo.nitro.reactnativesplashscreen + +import android.annotation.SuppressLint +import android.content.Context +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.RelativeLayout + +@SuppressLint("ViewConstructor") +internal class SplashScreenView(context: Context) : RelativeLayout(context) { + + val imageView: ImageView = ImageView(context).also { iv -> + iv.layoutParams = LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT + ) + addView(iv) + } + + init { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + + fun configureResizeMode(resizeMode: SplashImageResizeMode) { + imageView.scaleType = resizeMode.scaleType + when (resizeMode) { + SplashImageResizeMode.CONTAIN -> imageView.adjustViewBounds = true + else -> imageView.adjustViewBounds = false + } + } +} diff --git a/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/SplashViewController.kt b/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/SplashViewController.kt new file mode 100644 index 00000000..d61d8429 --- /dev/null +++ b/native-modules/react-native-splash-screen/android/src/main/java/com/margelo/nitro/reactnativesplashscreen/SplashViewController.kt @@ -0,0 +1,127 @@ +package com.margelo.nitro.reactnativesplashscreen + +import android.app.Activity +import android.os.Handler +import android.os.Looper +import android.view.View +import android.view.ViewGroup +import com.margelo.nitro.nativelogger.OneKeyLog +import java.lang.ref.WeakReference + +internal class SplashViewController( + activity: Activity, + private val splashView: View +) { + private companion object { + private const val TAG = "SplashScreen" + private const val SEARCH_INTERVAL_MS = 20L + + // Look up ReactRootView class at runtime to avoid compile-time dependency + private val reactRootViewClass: Class<*>? by lazy { + try { + Class.forName("com.facebook.react.ReactRootView") + } catch (e: ClassNotFoundException) { + null + } + } + } + + private val weakActivity = WeakReference(activity) + private val contentView: ViewGroup = activity.findViewById(android.R.id.content) + private val handler = Handler(Looper.getMainLooper()) + + private var autoHideEnabled = true + private var splashShown = false + private var searchCancelled = false + + fun show() { + val activity = weakActivity.get() ?: return + activity.runOnUiThread { + val parent = splashView.parent as? ViewGroup + parent?.removeView(splashView) + contentView.addView(splashView) + splashShown = true + OneKeyLog.info(TAG, "splash view added to content") + searchForRootView() + } + } + + fun preventAutoHide() { + autoHideEnabled = false + OneKeyLog.info(TAG, "autoHide disabled") + } + + fun hide( + onSuccess: ((Boolean) -> Unit)?, + onFailure: ((String) -> Unit)? + ) { + if (!splashShown) { + OneKeyLog.info(TAG, "hide: splash not shown, skipping") + onSuccess?.invoke(false) + return + } + val activity = weakActivity.get() + if (activity == null || activity.isFinishing || activity.isDestroyed) { + onFailure?.invoke("Activity is already destroyed") + return + } + searchCancelled = true + handler.removeCallbacksAndMessages(null) + Handler(activity.mainLooper).post { + contentView.removeView(splashView) + autoHideEnabled = true + splashShown = false + OneKeyLog.info(TAG, "splash view removed") + onSuccess?.invoke(true) + } + } + + private fun searchForRootView() { + if (searchCancelled) return + val activity = weakActivity.get() + if (activity == null || activity.isFinishing || activity.isDestroyed) { + OneKeyLog.warn(TAG, "searchForRootView: activity destroyed, stopping search") + return + } + if (!splashShown) return + val found = findRootView(contentView) + if (found != null) { + handleRootView(found) + return + } + handler.postDelayed(::searchForRootView, SEARCH_INTERVAL_MS) + } + + private fun findRootView(view: View): ViewGroup? { + val cls = reactRootViewClass + if (cls != null && cls.isInstance(view)) { + return view as? ViewGroup + } + if (view !== splashView && view is ViewGroup) { + for (i in 0 until view.childCount) { + val found = findRootView(view.getChildAt(i)) + if (found != null) return found + } + } + return null + } + + private fun handleRootView(rootView: ViewGroup) { + if (rootView.childCount > 0 && autoHideEnabled) { + hide(null, null) + } + rootView.setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener { + override fun onChildViewAdded(parent: View, child: View) { + if (rootView.childCount == 1 && autoHideEnabled) { + hide(null, null) + } + } + + override fun onChildViewRemoved(parent: View, child: View) { + if (rootView.childCount == 0) { + show() + } + } + }) + } +} diff --git a/native-modules/react-native-splash-screen/babel.config.js b/native-modules/react-native-splash-screen/babel.config.js new file mode 100644 index 00000000..f4cb13d5 --- /dev/null +++ b/native-modules/react-native-splash-screen/babel.config.js @@ -0,0 +1,13 @@ +module.exports = { + presets: ['@react-native/babel-preset'], + plugins: [ + [ + 'module-resolver', + { + alias: { + 'react-native-splash-screen': './src/index', + }, + }, + ], + ], +}; diff --git a/native-modules/react-native-splash-screen/eslint.config.mjs b/native-modules/react-native-splash-screen/eslint.config.mjs new file mode 100644 index 00000000..3416cf5c --- /dev/null +++ b/native-modules/react-native-splash-screen/eslint.config.mjs @@ -0,0 +1,5 @@ +import { createEslintConfig } from '@react-native/eslint-config'; + +export default createEslintConfig({ + extends: ['@react-native/eslint-config'], +}); diff --git a/native-modules/react-native-splash-screen/ios/ReactNativeSplashScreen.swift b/native-modules/react-native-splash-screen/ios/ReactNativeSplashScreen.swift new file mode 100644 index 00000000..8e8b7da5 --- /dev/null +++ b/native-modules/react-native-splash-screen/ios/ReactNativeSplashScreen.swift @@ -0,0 +1,15 @@ +import NitroModules + +// iOS splash screen is managed by expo-splash-screen via the launch storyboard. +// This module exists solely to provide Android < API 31 compatibility (see SplashScreenBridge.kt). +// On iOS, both methods are intentional no-ops. +class ReactNativeSplashScreen: HybridReactNativeSplashScreenSpec { + + func preventAutoHideAsync() throws -> Promise { + return Promise.resolved(withResult: true) + } + + func hideAsync() throws -> Promise { + return Promise.resolved(withResult: true) + } +} diff --git a/native-modules/react-native-splash-screen/lefthook.yml b/native-modules/react-native-splash-screen/lefthook.yml new file mode 100644 index 00000000..89766b1f --- /dev/null +++ b/native-modules/react-native-splash-screen/lefthook.yml @@ -0,0 +1,10 @@ +pre-commit: + parallel: true + commands: + lint: + glob: "*.{js,ts,tsx}" + run: yarn lint {staged_files} + typecheck: + files: git diff --cached --name-only --diff-filter=ACMRTUXB + glob: "*.{js,ts,tsx}" + run: yarn typecheck diff --git a/native-modules/react-native-splash-screen/nitro.json b/native-modules/react-native-splash-screen/nitro.json new file mode 100644 index 00000000..1729f1e6 --- /dev/null +++ b/native-modules/react-native-splash-screen/nitro.json @@ -0,0 +1,17 @@ +{ + "cxxNamespace": ["reactnativesplashscreen"], + "ios": { + "iosModuleName": "ReactNativeSplashScreen" + }, + "android": { + "androidNamespace": ["reactnativesplashscreen"], + "androidCxxLibName": "reactnativesplashscreen" + }, + "autolinking": { + "ReactNativeSplashScreen": { + "swift": "ReactNativeSplashScreen", + "kotlin": "ReactNativeSplashScreen" + } + }, + "ignorePaths": ["node_modules"] +} diff --git a/native-modules/react-native-splash-screen/package.json b/native-modules/react-native-splash-screen/package.json new file mode 100644 index 00000000..fa7a5970 --- /dev/null +++ b/native-modules/react-native-splash-screen/package.json @@ -0,0 +1,169 @@ +{ + "name": "@onekeyfe/react-native-splash-screen", + "version": "1.1.24", + "description": "react-native-splash-screen", + "main": "./lib/module/index.js", + "types": "./lib/typescript/src/index.d.ts", + "exports": { + ".": { + "source": "./src/index.tsx", + "types": "./lib/typescript/src/index.d.ts", + "default": "./lib/module/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "src", + "lib", + "android", + "ios", + "cpp", + "nitrogen", + "nitro.json", + "*.podspec", + "react-native.config.js", + "!ios/build", + "!android/build", + "!android/gradle", + "!android/gradlew", + "!android/gradlew.bat", + "!android/local.properties", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__", + "!**/.*" + ], + "scripts": { + "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", + "prepare": "bob build", + "nitrogen": "nitrogen", + "typecheck": "tsc", + "lint": "eslint \"**/*.{js,ts,tsx}\"", + "test": "jest", + "release": "yarn prepare && npm whoami && npm publish --access public" + }, + "keywords": [ + "react-native", + "ios", + "android" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/OneKeyHQ/app-modules/react-native-splash-screen.git" + }, + "author": "onekeyfe (https://github.com/OneKeyHQ/app-modules)", + "license": "MIT", + "bugs": { + "url": "https://github.com/OneKeyHQ/app-modules/react-native-splash-screen/issues" + }, + "homepage": "https://github.com/OneKeyHQ/app-modules/react-native-splash-screen#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "devDependencies": { + "@commitlint/config-conventional": "^19.8.1", + "@eslint/compat": "^1.3.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.35.0", + "@react-native/babel-preset": "0.83.0", + "@react-native/eslint-config": "0.83.0", + "@release-it/conventional-changelog": "^10.0.1", + "@types/jest": "^29.5.14", + "@types/react": "^19.2.0", + "commitlint": "^19.8.1", + "del-cli": "^6.0.0", + "eslint": "^9.35.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "jest": "^29.7.0", + "lefthook": "^2.0.3", + "nitrogen": "0.31.10", + "prettier": "^2.8.8", + "react": "19.2.0", + "react-native": "0.83.0", + "react-native-builder-bob": "^0.40.13", + "react-native-nitro-modules": "0.33.2", + "release-it": "^19.0.4", + "turbo": "^2.5.6", + "typescript": "^5.9.2" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-nitro-modules": "0.33.2" + }, + "react-native-builder-bob": { + "source": "src", + "output": "lib", + "targets": [ + [ + "custom", + { + "script": "nitrogen", + "clean": "nitrogen/" + } + ], + [ + "module", + { + "esm": true + } + ], + [ + "typescript", + { + "project": "tsconfig.build.json" + } + ] + ] + }, + "prettier": { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + }, + "jest": { + "preset": "react-native", + "modulePathIgnorePatterns": [ + "/example/node_modules", + "/lib/" + ] + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "release-it": { + "git": { + "commitMessage": "chore: release ${version}", + "tagName": "v${version}" + }, + "npm": { + "publish": true + }, + "github": { + "release": true + }, + "plugins": { + "@release-it/conventional-changelog": { + "preset": { + "name": "angular" + } + } + } + }, + "create-react-native-library": { + "type": "nitro-module", + "languages": "kotlin-swift", + "tools": [ + "eslint", + "jest", + "lefthook", + "release-it" + ], + "version": "0.56.0" + } +} diff --git a/native-modules/react-native-splash-screen/src/ReactNativeSplashScreen.nitro.ts b/native-modules/react-native-splash-screen/src/ReactNativeSplashScreen.nitro.ts new file mode 100644 index 00000000..7394f53c --- /dev/null +++ b/native-modules/react-native-splash-screen/src/ReactNativeSplashScreen.nitro.ts @@ -0,0 +1,7 @@ +import type { HybridObject } from 'react-native-nitro-modules'; + +export interface ReactNativeSplashScreen + extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { + preventAutoHideAsync(): Promise; + hideAsync(): Promise; +} diff --git a/native-modules/react-native-splash-screen/src/index.tsx b/native-modules/react-native-splash-screen/src/index.tsx new file mode 100644 index 00000000..198d31a6 --- /dev/null +++ b/native-modules/react-native-splash-screen/src/index.tsx @@ -0,0 +1,8 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import type { ReactNativeSplashScreen as ReactNativeSplashScreenType } from './ReactNativeSplashScreen.nitro'; + +const ReactNativeSplashScreenHybridObject = + NitroModules.createHybridObject('ReactNativeSplashScreen'); + +export const ReactNativeSplashScreen = ReactNativeSplashScreenHybridObject; +export type * from './ReactNativeSplashScreen.nitro'; diff --git a/native-modules/react-native-splash-screen/tsconfig.build.json b/native-modules/react-native-splash-screen/tsconfig.build.json new file mode 100644 index 00000000..45777014 --- /dev/null +++ b/native-modules/react-native-splash-screen/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "declarationMap": true + }, + "exclude": ["**/__tests__/**/*", "**/__fixtures__/**/*", "**/__mocks__/**/*"] +} diff --git a/native-modules/react-native-splash-screen/tsconfig.json b/native-modules/react-native-splash-screen/tsconfig.json new file mode 100644 index 00000000..f533f09c --- /dev/null +++ b/native-modules/react-native-splash-screen/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "rootDir": ".", + "paths": { + "react-native-splash-screen": ["./src/index"] + }, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "customConditions": ["react-native-strict-api"], + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "lib": ["ESNext"], + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext", + "verbatimModuleSyntax": true + } +} diff --git a/native-modules/react-native-splash-screen/turbo.json b/native-modules/react-native-splash-screen/turbo.json new file mode 100644 index 00000000..08b9676e --- /dev/null +++ b/native-modules/react-native-splash-screen/turbo.json @@ -0,0 +1,17 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "outputs": [ + "lib/**", + "nitrogen/**" + ], + "inputs": [ + "src/**", + "android/src/**", + "ios/**", + "cpp/**" + ] + } + } +} diff --git a/native-views/react-native-skeleton/package.json b/native-views/react-native-skeleton/package.json index 0d4a6dea..cd900027 100644 --- a/native-views/react-native-skeleton/package.json +++ b/native-views/react-native-skeleton/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/react-native-skeleton", - "version": "1.1.19", + "version": "1.1.24", "description": "react-native-skeleton", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/scripts/nitro/template/ModuleName.podspec b/scripts/nitro/template/ModuleName.podspec index d1808730..f9243bd5 100644 --- a/scripts/nitro/template/ModuleName.podspec +++ b/scripts/nitro/template/ModuleName.podspec @@ -21,6 +21,7 @@ Pod::Spec.new do |s| s.dependency 'React-jsi' s.dependency 'React-callinvoker' + s.dependency 'ReactNativeNativeLogger' load 'nitrogen/generated/ios/{{modulePascalCase}}+autolinking.rb' add_nitrogen_files(s) diff --git a/scripts/nitro/template/android/build.gradle b/scripts/nitro/template/android/build.gradle index e01a2813..5183b0d8 100644 --- a/scripts/nitro/template/android/build.gradle +++ b/scripts/nitro/template/android/build.gradle @@ -125,4 +125,6 @@ dependencies { implementation "com.facebook.react:react-android" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation project(":react-native-nitro-modules") + + implementation project(":onekeyfe_react-native-native-logger") } diff --git a/yarn.lock b/yarn.lock index 8f83aa6e..15110bfc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2744,14 +2744,19 @@ __metadata: "@babel/core": "npm:^7.25.2" "@babel/preset-env": "npm:^7.25.3" "@babel/runtime": "npm:^7.25.0" + "@onekeyfe/react-native-app-update": "workspace:*" "@onekeyfe/react-native-background-thread": "workspace:*" + "@onekeyfe/react-native-bundle-update": "workspace:*" "@onekeyfe/react-native-check-biometric-auth-changed": "workspace:*" "@onekeyfe/react-native-cloud-kit-module": "workspace:*" "@onekeyfe/react-native-device-utils": "workspace:*" "@onekeyfe/react-native-get-random-values": "workspace:*" "@onekeyfe/react-native-keychain-module": "workspace:*" "@onekeyfe/react-native-lite-card": "workspace:*" + "@onekeyfe/react-native-native-logger": "workspace:*" + "@onekeyfe/react-native-perf-memory": "workspace:*" "@onekeyfe/react-native-skeleton": "workspace:*" + "@onekeyfe/react-native-splash-screen": "workspace:*" "@react-native-community/cli": "npm:20.0.0" "@react-native-community/cli-platform-android": "npm:20.0.0" "@react-native-community/cli-platform-ios": "npm:20.0.0" @@ -2769,6 +2774,7 @@ __metadata: prettier: "npm:2.8.8" react: "npm:19.2.0" react-native: "npm:0.83.0" + react-native-mmkv: "npm:^4.1.2" react-native-nitro-modules: "npm:0.33.2" react-native-safe-area-context: "npm:^5.5.2" react-test-renderer: "npm:19.2.0" @@ -2813,6 +2819,42 @@ __metadata: languageName: unknown linkType: soft +"@onekeyfe/react-native-app-update@workspace:*, @onekeyfe/react-native-app-update@workspace:native-modules/react-native-app-update": + version: 0.0.0-use.local + resolution: "@onekeyfe/react-native-app-update@workspace:native-modules/react-native-app-update" + dependencies: + "@commitlint/config-conventional": "npm:^19.8.1" + "@eslint/compat": "npm:^1.3.2" + "@eslint/eslintrc": "npm:^3.3.1" + "@eslint/js": "npm:^9.35.0" + "@react-native/babel-preset": "npm:0.83.0" + "@react-native/eslint-config": "npm:0.83.0" + "@release-it/conventional-changelog": "npm:^10.0.1" + "@types/jest": "npm:^29.5.14" + "@types/react": "npm:^19.2.0" + commitlint: "npm:^19.8.1" + del-cli: "npm:^6.0.0" + eslint: "npm:^9.35.0" + eslint-config-prettier: "npm:^10.1.8" + eslint-plugin-prettier: "npm:^5.5.4" + jest: "npm:^29.7.0" + lefthook: "npm:^2.0.3" + nitrogen: "npm:0.31.10" + prettier: "npm:^2.8.8" + react: "npm:19.2.0" + react-native: "npm:0.83.0" + react-native-builder-bob: "npm:^0.40.13" + react-native-nitro-modules: "npm:0.33.2" + release-it: "npm:^19.0.4" + turbo: "npm:^2.5.6" + typescript: "npm:^5.9.2" + peerDependencies: + react: "*" + react-native: "*" + react-native-nitro-modules: 0.33.2 + languageName: unknown + linkType: soft + "@onekeyfe/react-native-background-thread@workspace:*, @onekeyfe/react-native-background-thread@workspace:native-modules/react-native-background-thread": version: 0.0.0-use.local resolution: "@onekeyfe/react-native-background-thread@workspace:native-modules/react-native-background-thread" @@ -2846,6 +2888,42 @@ __metadata: languageName: unknown linkType: soft +"@onekeyfe/react-native-bundle-update@workspace:*, @onekeyfe/react-native-bundle-update@workspace:native-modules/react-native-bundle-update": + version: 0.0.0-use.local + resolution: "@onekeyfe/react-native-bundle-update@workspace:native-modules/react-native-bundle-update" + dependencies: + "@commitlint/config-conventional": "npm:^19.8.1" + "@eslint/compat": "npm:^1.3.2" + "@eslint/eslintrc": "npm:^3.3.1" + "@eslint/js": "npm:^9.35.0" + "@react-native/babel-preset": "npm:0.83.0" + "@react-native/eslint-config": "npm:0.83.0" + "@release-it/conventional-changelog": "npm:^10.0.1" + "@types/jest": "npm:^29.5.14" + "@types/react": "npm:^19.2.0" + commitlint: "npm:^19.8.1" + del-cli: "npm:^6.0.0" + eslint: "npm:^9.35.0" + eslint-config-prettier: "npm:^10.1.8" + eslint-plugin-prettier: "npm:^5.5.4" + jest: "npm:^29.7.0" + lefthook: "npm:^2.0.3" + nitrogen: "npm:0.31.10" + prettier: "npm:^2.8.8" + react: "npm:19.2.0" + react-native: "npm:0.83.0" + react-native-builder-bob: "npm:^0.40.13" + react-native-nitro-modules: "npm:0.33.2" + release-it: "npm:^19.0.4" + turbo: "npm:^2.5.6" + typescript: "npm:^5.9.2" + peerDependencies: + react: "*" + react-native: "*" + react-native-nitro-modules: 0.33.2 + languageName: unknown + linkType: soft + "@onekeyfe/react-native-check-biometric-auth-changed@workspace:*, @onekeyfe/react-native-check-biometric-auth-changed@workspace:native-modules/react-native-check-biometric-auth-changed": version: 0.0.0-use.local resolution: "@onekeyfe/react-native-check-biometric-auth-changed@workspace:native-modules/react-native-check-biometric-auth-changed" @@ -3059,6 +3137,78 @@ __metadata: languageName: unknown linkType: soft +"@onekeyfe/react-native-native-logger@workspace:*, @onekeyfe/react-native-native-logger@workspace:native-modules/native-logger": + version: 0.0.0-use.local + resolution: "@onekeyfe/react-native-native-logger@workspace:native-modules/native-logger" + dependencies: + "@commitlint/config-conventional": "npm:^19.8.1" + "@eslint/compat": "npm:^1.3.2" + "@eslint/eslintrc": "npm:^3.3.1" + "@eslint/js": "npm:^9.35.0" + "@react-native/babel-preset": "npm:0.83.0" + "@react-native/eslint-config": "npm:0.83.0" + "@release-it/conventional-changelog": "npm:^10.0.1" + "@types/jest": "npm:^29.5.14" + "@types/react": "npm:^19.2.0" + commitlint: "npm:^19.8.1" + del-cli: "npm:^6.0.0" + eslint: "npm:^9.35.0" + eslint-config-prettier: "npm:^10.1.8" + eslint-plugin-prettier: "npm:^5.5.4" + jest: "npm:^29.7.0" + lefthook: "npm:^2.0.3" + nitrogen: "npm:0.31.10" + prettier: "npm:^2.8.8" + react: "npm:19.2.0" + react-native: "npm:0.83.0" + react-native-builder-bob: "npm:^0.40.13" + react-native-nitro-modules: "npm:0.33.2" + release-it: "npm:^19.0.4" + turbo: "npm:^2.5.6" + typescript: "npm:^5.9.2" + peerDependencies: + react: "*" + react-native: "*" + react-native-nitro-modules: 0.33.2 + languageName: unknown + linkType: soft + +"@onekeyfe/react-native-perf-memory@workspace:*, @onekeyfe/react-native-perf-memory@workspace:native-modules/react-native-perf-memory": + version: 0.0.0-use.local + resolution: "@onekeyfe/react-native-perf-memory@workspace:native-modules/react-native-perf-memory" + dependencies: + "@commitlint/config-conventional": "npm:^19.8.1" + "@eslint/compat": "npm:^1.3.2" + "@eslint/eslintrc": "npm:^3.3.1" + "@eslint/js": "npm:^9.35.0" + "@react-native/babel-preset": "npm:0.83.0" + "@react-native/eslint-config": "npm:0.83.0" + "@release-it/conventional-changelog": "npm:^10.0.1" + "@types/jest": "npm:^29.5.14" + "@types/react": "npm:^19.2.0" + commitlint: "npm:^19.8.1" + del-cli: "npm:^6.0.0" + eslint: "npm:^9.35.0" + eslint-config-prettier: "npm:^10.1.8" + eslint-plugin-prettier: "npm:^5.5.4" + jest: "npm:^29.7.0" + lefthook: "npm:^2.0.3" + nitrogen: "npm:0.31.10" + prettier: "npm:^2.8.8" + react: "npm:19.2.0" + react-native: "npm:0.83.0" + react-native-builder-bob: "npm:^0.40.13" + react-native-nitro-modules: "npm:0.33.2" + release-it: "npm:^19.0.4" + turbo: "npm:^2.5.6" + typescript: "npm:^5.9.2" + peerDependencies: + react: "*" + react-native: "*" + react-native-nitro-modules: 0.33.2 + languageName: unknown + linkType: soft + "@onekeyfe/react-native-skeleton@workspace:*, @onekeyfe/react-native-skeleton@workspace:native-views/react-native-skeleton": version: 0.0.0-use.local resolution: "@onekeyfe/react-native-skeleton@workspace:native-views/react-native-skeleton" @@ -3095,6 +3245,42 @@ __metadata: languageName: unknown linkType: soft +"@onekeyfe/react-native-splash-screen@workspace:*, @onekeyfe/react-native-splash-screen@workspace:native-modules/react-native-splash-screen": + version: 0.0.0-use.local + resolution: "@onekeyfe/react-native-splash-screen@workspace:native-modules/react-native-splash-screen" + dependencies: + "@commitlint/config-conventional": "npm:^19.8.1" + "@eslint/compat": "npm:^1.3.2" + "@eslint/eslintrc": "npm:^3.3.1" + "@eslint/js": "npm:^9.35.0" + "@react-native/babel-preset": "npm:0.83.0" + "@react-native/eslint-config": "npm:0.83.0" + "@release-it/conventional-changelog": "npm:^10.0.1" + "@types/jest": "npm:^29.5.14" + "@types/react": "npm:^19.2.0" + commitlint: "npm:^19.8.1" + del-cli: "npm:^6.0.0" + eslint: "npm:^9.35.0" + eslint-config-prettier: "npm:^10.1.8" + eslint-plugin-prettier: "npm:^5.5.4" + jest: "npm:^29.7.0" + lefthook: "npm:^2.0.3" + nitrogen: "npm:0.31.10" + prettier: "npm:^2.8.8" + react: "npm:19.2.0" + react-native: "npm:0.83.0" + react-native-builder-bob: "npm:^0.40.13" + react-native-nitro-modules: "npm:0.33.2" + release-it: "npm:^19.0.4" + turbo: "npm:^2.5.6" + typescript: "npm:^5.9.2" + peerDependencies: + react: "*" + react-native: "*" + react-native-nitro-modules: 0.33.2 + languageName: unknown + linkType: soft + "@phun-ky/typeof@npm:2.0.3": version: 2.0.3 resolution: "@phun-ky/typeof@npm:2.0.3" @@ -10651,6 +10837,17 @@ __metadata: languageName: node linkType: hard +"react-native-mmkv@npm:^4.1.2": + version: 4.1.2 + resolution: "react-native-mmkv@npm:4.1.2" + peerDependencies: + react: "*" + react-native: "*" + react-native-nitro-modules: "*" + checksum: 10/45f990de6d58acc3a76c82fe0dbe8e4922116c12433d0bbd0813cb0d825c9201dfee5f470c9118b017b24fcdede336cbd6eb2a0186292d7fea1c11d4407d5fe0 + languageName: node + linkType: hard + "react-native-monorepo-config@npm:^0.3.1": version: 0.3.1 resolution: "react-native-monorepo-config@npm:0.3.1"