@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.
@@ -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,9 @@
1
+ {
2
+ "platforms": ["ios", "android"],
3
+ "ios": {
4
+ "modules": ["CdpAppAttestModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["expo.modules.cdpappattest.CdpAppAttestModule"]
8
+ }
9
+ }
@@ -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.0",
4
- "description": "Placeholder package",
5
- "main": "index.js",
6
- "author": "Coinbase Inc.",
7
- "license": "Apache-2.0"
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");