@coinbase/cdp-app-attest 0.0.0 → 0.0.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +40 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/expo/modules/cdpappattest/CdpAppAttestModule.kt +105 -0
- package/dist/esm/index.js +34 -0
- package/dist/types/index.d.ts +43 -0
- package/expo-module.config.json +9 -0
- package/ios/CdpAppAttest.podspec +27 -0
- package/ios/CdpAppAttestModule.swift +282 -0
- package/package.json +62 -6
- package/index.js +0 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
apply plugin: 'kotlin-android'
|
|
3
|
+
apply plugin: 'expo-module-gradle-plugin'
|
|
4
|
+
|
|
5
|
+
android {
|
|
6
|
+
namespace "expo.modules.cdpappattest"
|
|
7
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
|
8
|
+
|
|
9
|
+
defaultConfig {
|
|
10
|
+
minSdkVersion safeExtGet("minSdkVersion", 23)
|
|
11
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
|
12
|
+
versionName "0.0.1"
|
|
13
|
+
versionCode 1
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
compileOptions {
|
|
17
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
18
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
kotlinOptions {
|
|
22
|
+
jvmTarget = "17"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
dependencies {
|
|
27
|
+
implementation project(':expo-modules-core')
|
|
28
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
|
|
29
|
+
implementation "com.google.android.play:integrity:1.4.0"
|
|
30
|
+
implementation "com.google.android.gms:play-services-base:18.5.0"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def safeExtGet(prop, fallback) {
|
|
34
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def getKotlinVersion() {
|
|
38
|
+
def kotlinVersion = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : "1.9.23"
|
|
39
|
+
return kotlinVersion
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<manifest />
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
package expo.modules.cdpappattest
|
|
2
|
+
|
|
3
|
+
import com.google.android.play.core.integrity.IntegrityManagerFactory
|
|
4
|
+
import com.google.android.play.core.integrity.StandardIntegrityManager
|
|
5
|
+
import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenProvider
|
|
6
|
+
import expo.modules.kotlin.Promise
|
|
7
|
+
import expo.modules.kotlin.exception.CodedException
|
|
8
|
+
import expo.modules.kotlin.modules.Module
|
|
9
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
10
|
+
import expo.modules.kotlin.records.Field
|
|
11
|
+
import expo.modules.kotlin.records.Record
|
|
12
|
+
|
|
13
|
+
class InitializeConfig : Record {
|
|
14
|
+
@Field
|
|
15
|
+
val cloudProjectNumber: String = ""
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class CdpAppAttestModule : Module() {
|
|
19
|
+
private var tokenProvider: StandardIntegrityTokenProvider? = null
|
|
20
|
+
|
|
21
|
+
override fun definition() = ModuleDefinition {
|
|
22
|
+
Name("CdpAppAttest")
|
|
23
|
+
|
|
24
|
+
AsyncFunction("isSupported") {
|
|
25
|
+
val context = appContext.reactContext ?: return@AsyncFunction false
|
|
26
|
+
try {
|
|
27
|
+
com.google.android.gms.common.GoogleApiAvailability.getInstance()
|
|
28
|
+
.isGooglePlayServicesAvailable(context) == com.google.android.gms.common.ConnectionResult.SUCCESS
|
|
29
|
+
} catch (_: Exception) {
|
|
30
|
+
false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
AsyncFunction("initialize") { config: InitializeConfig, promise: Promise ->
|
|
35
|
+
val context = appContext.reactContext
|
|
36
|
+
if (context == null) {
|
|
37
|
+
promise.reject(CodedException("ERR_MISSING_CONTEXT", "Application context is not available", null))
|
|
38
|
+
return@AsyncFunction
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
val projectNumber = config.cloudProjectNumber.toLongOrNull()
|
|
42
|
+
if (projectNumber == null) {
|
|
43
|
+
promise.reject(CodedException("ERR_INVALID_CONFIG", "cloudProjectNumber must be a valid number", null))
|
|
44
|
+
return@AsyncFunction
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
val manager = IntegrityManagerFactory.createStandard(context)
|
|
49
|
+
val prepareRequest = StandardIntegrityManager.PrepareIntegrityTokenRequest.builder()
|
|
50
|
+
.setCloudProjectNumber(projectNumber)
|
|
51
|
+
.build()
|
|
52
|
+
|
|
53
|
+
manager.prepareIntegrityToken(prepareRequest)
|
|
54
|
+
.addOnSuccessListener { provider ->
|
|
55
|
+
tokenProvider = provider
|
|
56
|
+
promise.resolve(null)
|
|
57
|
+
}
|
|
58
|
+
.addOnFailureListener { e ->
|
|
59
|
+
promise.reject(CodedException("ERR_PREPARE_FAILED", "Failed to prepare integrity token: ${e.message}", e))
|
|
60
|
+
}
|
|
61
|
+
} catch (e: Exception) {
|
|
62
|
+
promise.reject(CodedException("ERR_INITIALIZE_FAILED", "Failed to initialize Play Integrity: ${e.message}", e))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
AsyncFunction("attest") { _: String, promise: Promise ->
|
|
67
|
+
promise.reject(CodedException("ERR_NOT_SUPPORTED", "Attestation exchange is not supported on Android. Use createAssertion() instead.", null))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
AsyncFunction("createAssertion") { clientData: String, promise: Promise ->
|
|
71
|
+
val provider = tokenProvider
|
|
72
|
+
if (provider == null) {
|
|
73
|
+
promise.reject(CodedException("ERR_NOT_INITIALIZED", "Token provider not initialized. Call initialize() first.", null))
|
|
74
|
+
return@AsyncFunction
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
val request = StandardIntegrityManager.StandardIntegrityTokenRequest.builder()
|
|
79
|
+
.setRequestHash(clientData)
|
|
80
|
+
.build()
|
|
81
|
+
|
|
82
|
+
provider.request(request)
|
|
83
|
+
.addOnSuccessListener { response ->
|
|
84
|
+
val result = mapOf(
|
|
85
|
+
"android" to mapOf(
|
|
86
|
+
"integrityToken" to response.token()
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
promise.resolve(result)
|
|
90
|
+
}
|
|
91
|
+
.addOnFailureListener { e ->
|
|
92
|
+
promise.reject(CodedException("ERR_INTEGRITY_TOKEN", "Failed to get integrity token: ${e.message}", e))
|
|
93
|
+
}
|
|
94
|
+
} catch (e: Exception) {
|
|
95
|
+
promise.reject(CodedException("ERR_INTEGRITY_TOKEN", "Failed to request integrity token: ${e.message}", e))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
AsyncFunction("clearKey") { promise: Promise ->
|
|
100
|
+
tokenProvider = null
|
|
101
|
+
promise.resolve(true)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { requireNativeModule as a } from "expo-modules-core";
|
|
2
|
+
const t = a("CdpAppAttest");
|
|
3
|
+
async function r() {
|
|
4
|
+
return await t.isSupported();
|
|
5
|
+
}
|
|
6
|
+
async function c(e) {
|
|
7
|
+
if (typeof t.initialize == "function")
|
|
8
|
+
return await t.initialize(e);
|
|
9
|
+
}
|
|
10
|
+
async function o(e) {
|
|
11
|
+
return await t.attest(e);
|
|
12
|
+
}
|
|
13
|
+
async function s(e) {
|
|
14
|
+
return await t.createAssertion(e);
|
|
15
|
+
}
|
|
16
|
+
const i = "coinbase.cdp.attestation-registration-confirmed";
|
|
17
|
+
async function u(e) {
|
|
18
|
+
await t.setKeychainValue(i, e);
|
|
19
|
+
}
|
|
20
|
+
async function p() {
|
|
21
|
+
return await t.getKeychainValue(i);
|
|
22
|
+
}
|
|
23
|
+
async function f() {
|
|
24
|
+
await t.clearKey(), await t.deleteKeychainValue(i);
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
o as attest,
|
|
28
|
+
f as clearAttestation,
|
|
29
|
+
u as confirmRegistration,
|
|
30
|
+
s as createAssertion,
|
|
31
|
+
p as getRegisteredKeyId,
|
|
32
|
+
c as initialize,
|
|
33
|
+
r as isSupported
|
|
34
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
declare interface AndroidIntegrityData {
|
|
2
|
+
integrityToken: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export declare interface AssertionResult {
|
|
6
|
+
ios?: IOSAssertionData;
|
|
7
|
+
android?: AndroidIntegrityData;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export declare function attest(challenge: string): Promise<AttestationResult>;
|
|
11
|
+
|
|
12
|
+
export declare interface AttestationResult {
|
|
13
|
+
ios?: IOSAttestationData;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export declare function clearAttestation(): Promise<void>;
|
|
17
|
+
|
|
18
|
+
export declare function confirmRegistration(keyId: string): Promise<void>;
|
|
19
|
+
|
|
20
|
+
export declare function createAssertion(clientData: string): Promise<AssertionResult>;
|
|
21
|
+
|
|
22
|
+
export declare function getRegisteredKeyId(): Promise<string | null>;
|
|
23
|
+
|
|
24
|
+
export declare function initialize(config?: InitializeConfig): Promise<void>;
|
|
25
|
+
|
|
26
|
+
export declare interface InitializeConfig {
|
|
27
|
+
cloudProjectNumber?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
declare interface IOSAssertionData {
|
|
31
|
+
assertion: string;
|
|
32
|
+
keyId: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
declare interface IOSAttestationData {
|
|
36
|
+
keyId: string;
|
|
37
|
+
attestation: string;
|
|
38
|
+
bundleId: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export declare function isSupported(): Promise<boolean>;
|
|
42
|
+
|
|
43
|
+
export { }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'CdpAppAttest'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.homepage = package['homepage']
|
|
13
|
+
s.platforms = { :ios => '14.0' }
|
|
14
|
+
s.swift_version = '5.4'
|
|
15
|
+
s.source = { git: 'https://github.com/coinbase/cdp-web' }
|
|
16
|
+
s.static_framework = true
|
|
17
|
+
|
|
18
|
+
s.dependency 'ExpoModulesCore'
|
|
19
|
+
|
|
20
|
+
# Swift/Objective-C compatibility
|
|
21
|
+
s.pod_target_xcconfig = {
|
|
22
|
+
'DEFINES_MODULE' => 'YES',
|
|
23
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
27
|
+
end
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import DeviceCheck
|
|
3
|
+
import CryptoKit
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CDP module for iOS App Attest.
|
|
7
|
+
*
|
|
8
|
+
* Wraps DCAppAttestService to provide JavaScript access to Apple's App Attest
|
|
9
|
+
* device attestation APIs. Allows apps to prove they are legitimate instances
|
|
10
|
+
* running on genuine Apple devices.
|
|
11
|
+
*/
|
|
12
|
+
public class CdpAppAttestModule: Module {
|
|
13
|
+
// Storage key for the attestation key ID in Keychain
|
|
14
|
+
private let keyIdStorageKey = "cdp_attestation_key_id"
|
|
15
|
+
// Keychain service name
|
|
16
|
+
private let keychainService = "com.coinbase.cdp.attestation"
|
|
17
|
+
|
|
18
|
+
// MARK: - Keychain Helpers
|
|
19
|
+
|
|
20
|
+
/// Saves a string to the Keychain
|
|
21
|
+
private func saveToKeychain(key: String, value: String) -> Bool {
|
|
22
|
+
guard let data = value.data(using: .utf8) else {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// First try to update existing item
|
|
27
|
+
let updateQuery: [String: Any] = [
|
|
28
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
29
|
+
kSecAttrService as String: keychainService,
|
|
30
|
+
kSecAttrAccount as String: key
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
let updateAttributes: [String: Any] = [
|
|
34
|
+
kSecValueData as String: data
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
let updateStatus = SecItemUpdate(updateQuery as CFDictionary, updateAttributes as CFDictionary)
|
|
38
|
+
|
|
39
|
+
if updateStatus == errSecSuccess {
|
|
40
|
+
return true
|
|
41
|
+
} else if updateStatus == errSecItemNotFound {
|
|
42
|
+
// Item doesn't exist, add it
|
|
43
|
+
let addQuery: [String: Any] = [
|
|
44
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
45
|
+
kSecAttrService as String: keychainService,
|
|
46
|
+
kSecAttrAccount as String: key,
|
|
47
|
+
kSecValueData as String: data,
|
|
48
|
+
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
|
|
52
|
+
return addStatus == errSecSuccess
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Retrieves a string from the Keychain
|
|
59
|
+
private func getFromKeychain(key: String) -> String? {
|
|
60
|
+
let query: [String: Any] = [
|
|
61
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
62
|
+
kSecAttrService as String: keychainService,
|
|
63
|
+
kSecAttrAccount as String: key,
|
|
64
|
+
kSecReturnData as String: true,
|
|
65
|
+
kSecMatchLimit as String: kSecMatchLimitOne
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
var result: AnyObject?
|
|
69
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
70
|
+
|
|
71
|
+
guard status == errSecSuccess,
|
|
72
|
+
let data = result as? Data,
|
|
73
|
+
let string = String(data: data, encoding: .utf8) else {
|
|
74
|
+
return nil
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Deletes a value from the Keychain
|
|
81
|
+
private func deleteFromKeychain(key: String) -> Bool {
|
|
82
|
+
let query: [String: Any] = [
|
|
83
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
84
|
+
kSecAttrService as String: keychainService,
|
|
85
|
+
kSecAttrAccount as String: key
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
let status = SecItemDelete(query as CFDictionary)
|
|
89
|
+
return status == errSecSuccess || status == errSecItemNotFound
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// MARK: - Key Management
|
|
93
|
+
|
|
94
|
+
/// Gets cached key ID from Keychain or generates a new one
|
|
95
|
+
private func getOrGenerateKeyId(completion: @escaping (String?, Error?) -> Void) {
|
|
96
|
+
if #available(iOS 14.0, *) {
|
|
97
|
+
let service = DCAppAttestService.shared
|
|
98
|
+
|
|
99
|
+
// Check if we have a cached key ID in Keychain
|
|
100
|
+
if let cachedKeyId = getFromKeychain(key: keyIdStorageKey) {
|
|
101
|
+
completion(cachedKeyId, nil)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Generate new key
|
|
106
|
+
service.generateKey { keyId, error in
|
|
107
|
+
if let error = error {
|
|
108
|
+
completion(nil, error)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
guard let keyId = keyId else {
|
|
113
|
+
completion(nil, NSError(domain: "CdpAppAttest", code: -1, userInfo: [NSLocalizedDescriptionKey: "Key generation returned nil"]))
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Save to Keychain
|
|
118
|
+
if !self.saveToKeychain(key: self.keyIdStorageKey, value: keyId) {
|
|
119
|
+
completion(nil, NSError(domain: "CdpAppAttest", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to save key ID to Keychain"]))
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
completion(keyId, nil)
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
completion(nil, NSError(domain: "CdpAppAttest", code: -1, userInfo: [NSLocalizedDescriptionKey: "iOS 14.0+ required"]))
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Define module metadata
|
|
131
|
+
public func definition() -> ModuleDefinition {
|
|
132
|
+
Name("CdpAppAttest")
|
|
133
|
+
|
|
134
|
+
// Check if App Attest is supported on this device
|
|
135
|
+
AsyncFunction("isSupported") { () -> Bool in
|
|
136
|
+
if #available(iOS 14.0, *) {
|
|
137
|
+
return DCAppAttestService.shared.isSupported
|
|
138
|
+
}
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Attest the app instance with automatic key management
|
|
143
|
+
AsyncFunction("attest") { (challenge: String, promise: Promise) in
|
|
144
|
+
if #available(iOS 14.0, *) {
|
|
145
|
+
let service = DCAppAttestService.shared
|
|
146
|
+
|
|
147
|
+
guard service.isSupported else {
|
|
148
|
+
promise.reject("ERR_NOT_SUPPORTED", "App Attest is not supported on this device")
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Get or generate key ID
|
|
153
|
+
self.getOrGenerateKeyId { keyId, error in
|
|
154
|
+
if let error = error {
|
|
155
|
+
promise.reject("ERR_GET_KEY", "Failed to get or generate key: \(error.localizedDescription)")
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
guard let keyId = keyId else {
|
|
160
|
+
promise.reject("ERR_GET_KEY", "Failed to get key ID")
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Decode base64 challenge
|
|
165
|
+
guard let challengeData = Data(base64Encoded: challenge) else {
|
|
166
|
+
promise.reject("ERR_INVALID_CHALLENGE", "Challenge must be base64-encoded")
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Hash the challenge
|
|
171
|
+
let challengeHash = Data(SHA256.hash(data: challengeData))
|
|
172
|
+
|
|
173
|
+
// Attest the key
|
|
174
|
+
service.attestKey(keyId, clientDataHash: challengeHash) { attestation, error in
|
|
175
|
+
if let error = error {
|
|
176
|
+
promise.reject("ERR_ATTEST_KEY", "Failed to attest key: \(error.localizedDescription)")
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
guard let attestation = attestation else {
|
|
181
|
+
promise.reject("ERR_ATTEST_KEY", "Attestation returned nil")
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let bundleId = Bundle.main.bundleIdentifier ?? "unknown"
|
|
186
|
+
|
|
187
|
+
// Wrap response in "ios" key for CDP API spec
|
|
188
|
+
let result: [String: Any] = [
|
|
189
|
+
"ios": [
|
|
190
|
+
"keyId": keyId,
|
|
191
|
+
"attestation": attestation.base64EncodedString(),
|
|
192
|
+
"bundleId": bundleId
|
|
193
|
+
]
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
promise.resolve(result)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
promise.reject("ERR_NOT_SUPPORTED", "App Attest requires iOS 14.0 or later")
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Clear cached attestation key (allows re-attestation with a new key)
|
|
205
|
+
AsyncFunction("clearKey") { (promise: Promise) in
|
|
206
|
+
let success = self.deleteFromKeychain(key: self.keyIdStorageKey)
|
|
207
|
+
promise.resolve(success)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Generic Keychain storage (allows SDK to store custom flags)
|
|
211
|
+
AsyncFunction("setKeychainValue") { (key: String, value: String, promise: Promise) in
|
|
212
|
+
let success = self.saveToKeychain(key: key, value: value)
|
|
213
|
+
promise.resolve(success)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Generic Keychain retrieval
|
|
217
|
+
AsyncFunction("getKeychainValue") { (key: String, promise: Promise) in
|
|
218
|
+
if let value = self.getFromKeychain(key: key) {
|
|
219
|
+
promise.resolve(value)
|
|
220
|
+
} else {
|
|
221
|
+
promise.resolve(nil)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Generic Keychain deletion
|
|
226
|
+
AsyncFunction("deleteKeychainValue") { (key: String, promise: Promise) in
|
|
227
|
+
let success = self.deleteFromKeychain(key: key)
|
|
228
|
+
promise.resolve(success)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Simplified generateAssertion that uses cached key
|
|
232
|
+
AsyncFunction("createAssertion") { (clientData: String, promise: Promise) in
|
|
233
|
+
if #available(iOS 14.0, *) {
|
|
234
|
+
let service = DCAppAttestService.shared
|
|
235
|
+
|
|
236
|
+
guard service.isSupported else {
|
|
237
|
+
promise.reject("ERR_NOT_SUPPORTED", "App Attest is not supported on this device")
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Get cached key ID from Keychain
|
|
242
|
+
guard let keyId = self.getFromKeychain(key: self.keyIdStorageKey) else {
|
|
243
|
+
promise.reject("ERR_NO_KEY", "No attestation key found. Call attest() first to generate a key.")
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Decode base64 client data
|
|
248
|
+
guard let clientDataBytes = Data(base64Encoded: clientData) else {
|
|
249
|
+
promise.reject("ERR_INVALID_DATA", "Client data must be base64-encoded")
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Hash the client data
|
|
254
|
+
let clientDataHash = Data(SHA256.hash(data: clientDataBytes))
|
|
255
|
+
|
|
256
|
+
service.generateAssertion(keyId, clientDataHash: clientDataHash) { assertion, error in
|
|
257
|
+
if let error = error {
|
|
258
|
+
promise.reject("ERR_GENERATE_ASSERTION", "Failed to generate assertion: \(error.localizedDescription)")
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
guard let assertion = assertion else {
|
|
263
|
+
promise.reject("ERR_GENERATE_ASSERTION", "Assertion generation returned nil")
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Wrap response in "ios" key for CDP API spec
|
|
268
|
+
let result: [String: Any] = [
|
|
269
|
+
"ios": [
|
|
270
|
+
"assertion": assertion.base64EncodedString(),
|
|
271
|
+
"keyId": keyId
|
|
272
|
+
]
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
promise.resolve(result)
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
promise.reject("ERR_NOT_SUPPORTED", "App Attest requires iOS 14.0 or later")
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coinbase/cdp-app-attest",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
"version": "0.0.95",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CDP native module for iOS App Attest and Android Play Integrity",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist/**",
|
|
8
|
+
"ios/**",
|
|
9
|
+
"android/**",
|
|
10
|
+
"expo-module.config.json",
|
|
11
|
+
"!dist/**/*.tsbuildinfo"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/types/index.d.ts",
|
|
16
|
+
"default": "./dist/esm/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"react-native",
|
|
22
|
+
"expo",
|
|
23
|
+
"app-attest",
|
|
24
|
+
"play-integrity",
|
|
25
|
+
"attestation"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/coinbase/cdp-web.git",
|
|
30
|
+
"directory": "packages/cdp-app-attest"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/coinbase/cdp-web/issues"
|
|
34
|
+
},
|
|
35
|
+
"author": "Coinbase",
|
|
36
|
+
"license": "Apache-2.0",
|
|
37
|
+
"homepage": "https://github.com/coinbase/cdp-web/tree/master/packages/cdp-app-attest",
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"expo": "*",
|
|
40
|
+
"expo-modules-core": "*",
|
|
41
|
+
"react": "*",
|
|
42
|
+
"react-native": "*"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@size-limit/preset-small-lib": "^11.2.0",
|
|
46
|
+
"size-limit": "^11.2.0"
|
|
47
|
+
},
|
|
48
|
+
"size-limit": [
|
|
49
|
+
{
|
|
50
|
+
"name": "full-package",
|
|
51
|
+
"path": "./dist/esm/index.js",
|
|
52
|
+
"limit": "2 KB"
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "pnpm run clean && pnpm run check:types && vite build",
|
|
57
|
+
"check:types": "tsc --noEmit",
|
|
58
|
+
"clean": "rm -rf dist build",
|
|
59
|
+
"clean:all": "pnpm clean && rm -rf node_modules",
|
|
60
|
+
"size-limit": "size-limit",
|
|
61
|
+
"test": "vitest",
|
|
62
|
+
"test:run": "vitest --run"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
console.log("Temporary package");
|