expo-device-crypto 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,404 @@
1
+ import ExpoModulesCore
2
+ import Security
3
+ import Foundation
4
+ import LocalAuthentication
5
+
6
+ enum SecureSigningModuleResult: String {
7
+ case KEY_PAIR_GENERATED = "KEY_PAIR_GENERATED"
8
+ case KEY_PAIR_ALREADY_EXISTS = "KEY_PAIR_ALREADY_EXISTS"
9
+ case NOT_AVAILABLE = "NOT_AVAILABLE"
10
+ }
11
+
12
+ enum AuthCheckResult: String {
13
+ case AVAILABLE = "AVAILABLE"
14
+ case NO_HARDWARE = "NO_HARDWARE"
15
+ case UNAVAILABLE = "UNAVAILABLE"
16
+ }
17
+
18
+ enum AuthMethod: String {
19
+ case PASSCODE = "PASSCODE"
20
+ case PASSCODE_OR_BIOMETRIC = "PASSCODE_OR_BIOMETRIC"
21
+ }
22
+
23
+ enum AlgorithmType: String {
24
+ case ECDSA_SECP256R1_SHA256 = "ECDSA_SECP256R1_SHA256"
25
+ case RSA_2048_PKCS1 = "RSA_2048_PKCS1"
26
+ }
27
+
28
+ public class DeviceCryptoModule: Module {
29
+ private func toiOSAlgo(algorithm: AlgorithmType) -> SecKeyAlgorithm {
30
+ switch algorithm {
31
+ case .ECDSA_SECP256R1_SHA256:
32
+ return .ecdsaSignatureMessageX962SHA256
33
+ case .RSA_2048_PKCS1:
34
+ return .rsaEncryptionPKCS1
35
+ }
36
+ }
37
+
38
+ // Converts ANSI x962 EC point to P‑256 SPKI DER format
39
+ private func x962ECPointToP256SPKI(_ publicKey: SecKey) -> Data? {
40
+ var error: Unmanaged<CFError>?
41
+ guard let raw = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
42
+ return nil
43
+ }
44
+
45
+ guard raw.count == 65 else { return nil }
46
+ let prefix = Data([
47
+ 0x30, 0x59,
48
+ 0x30, 0x13,
49
+ 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01,
50
+ 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07,
51
+ 0x03, 0x42, 0x00
52
+ ])
53
+ return prefix + raw
54
+ }
55
+
56
+ // Converts PKCS#1 RSA public key bytes to SPKI DER format.
57
+ private func rsaPKCS1ToSPKI(_ publicKey: SecKey) -> Data? {
58
+ guard let pkcs1 = SecKeyCopyExternalRepresentation(publicKey, nil) as Data? else {
59
+ return nil
60
+ }
61
+
62
+ // rsaEncryption OID: 1.2.840.113549.1.1.1 with NULL params
63
+ let algorithmIdentifier = Data([
64
+ 0x30, 0x0D,
65
+ 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01,
66
+ 0x05, 0x00
67
+ ])
68
+
69
+ let bitStringPayload = Data([0x00]) + pkcs1
70
+ guard let bitString = asn1Wrap(tag: 0x03, content: bitStringPayload) else {
71
+ return nil
72
+ }
73
+
74
+ return asn1Wrap(tag: 0x30, content: algorithmIdentifier + bitString)
75
+ }
76
+
77
+ private func asn1Wrap(tag: UInt8, content: Data) -> Data? {
78
+ guard let length = asn1Length(content.count) else {
79
+ return nil
80
+ }
81
+ return Data([tag]) + length + content
82
+ }
83
+
84
+ private func asn1Length(_ length: Int) -> Data? {
85
+ if length < 0x80 {
86
+ return Data([UInt8(length)])
87
+ }
88
+
89
+ var value = length
90
+ var bytes: [UInt8] = []
91
+ while value > 0 {
92
+ bytes.insert(UInt8(value & 0xff), at: 0)
93
+ value >>= 8
94
+ }
95
+
96
+ guard bytes.count <= 4 else {
97
+ return nil
98
+ }
99
+
100
+ return Data([0x80 | UInt8(bytes.count)]) + Data(bytes)
101
+ }
102
+
103
+ private func isAuthCheckAvailable() -> String {
104
+ let context = LAContext()
105
+ let available = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil)
106
+ if available {
107
+ return AuthCheckResult.AVAILABLE.rawValue
108
+ } else {
109
+ return AuthCheckResult.UNAVAILABLE.rawValue
110
+ }
111
+ }
112
+
113
+ private func getAliases() -> [String] {
114
+ let query: [String: Any] = [
115
+ kSecClass as String: kSecClassKey,
116
+ kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
117
+ kSecMatchLimit as String: kSecMatchLimitAll,
118
+ kSecReturnAttributes as String: true,
119
+ ]
120
+
121
+ var result: CFTypeRef?
122
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
123
+ guard status == errSecSuccess else { return [] }
124
+
125
+ let items = (result as? [[String: Any]]) ?? []
126
+ return items.compactMap { attrs in
127
+ let tagKey = kSecAttrApplicationTag as String
128
+ if let tagString = attrs[tagKey] as? String {
129
+ return tagString
130
+ }
131
+ return nil
132
+ }
133
+ }
134
+
135
+ private func getSecKeyQuery(_ alias: String, keyClass: CFString, returnRef: Bool = true) -> [String: Any] {
136
+ var query: [String: Any] = [
137
+ kSecClass as String: kSecClassKey,
138
+ kSecAttrApplicationTag as String: alias,
139
+ kSecAttrKeyClass as String: keyClass,
140
+ ]
141
+ if returnRef {
142
+ query[kSecReturnRef as String] = true
143
+ }
144
+ return query
145
+ }
146
+
147
+ private func getSecKeyByAlias(_ alias: String, keyClass: CFString) -> SecKey? {
148
+ let query: [String: Any] = self.getSecKeyQuery(alias, keyClass: keyClass)
149
+ var item: CFTypeRef?
150
+ let status = SecItemCopyMatching(query as CFDictionary, &item)
151
+ guard status == errSecSuccess else { return nil }
152
+ return (item as! SecKey)
153
+ }
154
+
155
+ private func retrievePublicKey(_ secKey: SecKey) -> String? {
156
+ let publicKey = SecKeyCopyPublicKey(secKey)!
157
+
158
+ guard
159
+ let attrs = SecKeyCopyAttributes(publicKey) as? [String: Any],
160
+ let keyType = attrs[kSecAttrKeyType as String] as? String
161
+ else {
162
+ return nil
163
+ }
164
+
165
+ if keyType == (kSecAttrKeyTypeECSECPrimeRandom as String) {
166
+ return x962ECPointToP256SPKI(publicKey)?.base64EncodedString()
167
+ }
168
+
169
+ if keyType == (kSecAttrKeyTypeRSA as String) {
170
+ return rsaPKCS1ToSPKI(publicKey)?.base64EncodedString()
171
+ }
172
+
173
+ guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) as Data? else {
174
+ return nil
175
+ }
176
+ return publicKeyData.base64EncodedString()
177
+ }
178
+
179
+ private func removeKeyStoreEntry(_ alias: String) -> Bool {
180
+ let privateQuery = self.getSecKeyQuery(alias, keyClass: kSecAttrKeyClassPrivate, returnRef: false)
181
+ let privateStatus = SecItemDelete(privateQuery as CFDictionary)
182
+
183
+ let publicQuery = self.getSecKeyQuery(alias, keyClass: kSecAttrKeyClassPublic, returnRef: false)
184
+ let publicStatus = SecItemDelete(publicQuery as CFDictionary)
185
+
186
+ return privateStatus == errSecSuccess || publicStatus == errSecSuccess
187
+ }
188
+
189
+ private func buildECDSA_SECP256R1_SHA256(alias: String, reqAuth: Bool, authMethod: AuthMethod) -> SecKey? {
190
+ let accessFlags: SecAccessControlCreateFlags
191
+ if reqAuth {
192
+ switch authMethod {
193
+ case .PASSCODE:
194
+ accessFlags = [.privateKeyUsage, .devicePasscode]
195
+ case .PASSCODE_OR_BIOMETRIC:
196
+ accessFlags = [.privateKeyUsage, .userPresence]
197
+ }
198
+ } else {
199
+ accessFlags = .privateKeyUsage
200
+ }
201
+
202
+ let access = SecAccessControlCreateWithFlags(
203
+ kCFAllocatorDefault,
204
+ kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
205
+ accessFlags,
206
+ nil
207
+ )
208
+
209
+ let attributes: NSDictionary = [
210
+ kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
211
+ kSecAttrKeySizeInBits: 256,
212
+ kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
213
+ kSecPrivateKeyAttrs: [
214
+ kSecAttrIsPermanent: true,
215
+ kSecAttrApplicationTag: alias,
216
+ kSecAttrAccessControl: access
217
+ ],
218
+ kSecPublicKeyAttrs: [
219
+ kSecAttrIsPermanent: true,
220
+ kSecAttrApplicationTag: alias
221
+ ]
222
+ ]
223
+
224
+ return SecKeyCreateRandomKey(attributes, nil)
225
+ }
226
+
227
+ private func buildRSA_2048_PKCS1(alias: String, reqAuth: Bool, authMethod: AuthMethod) -> SecKey? {
228
+ let accessFlags: SecAccessControlCreateFlags
229
+ if reqAuth {
230
+ switch authMethod {
231
+ case .PASSCODE:
232
+ accessFlags = [.devicePasscode]
233
+ case .PASSCODE_OR_BIOMETRIC:
234
+ accessFlags = [.userPresence]
235
+ }
236
+ } else {
237
+ accessFlags = []
238
+ }
239
+
240
+ let access = SecAccessControlCreateWithFlags(
241
+ kCFAllocatorDefault,
242
+ kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
243
+ accessFlags,
244
+ nil
245
+ )
246
+
247
+ let attributes: NSDictionary = [
248
+ kSecAttrKeyType: kSecAttrKeyTypeRSA,
249
+ kSecAttrKeySizeInBits: 2048,
250
+ kSecPrivateKeyAttrs: [
251
+ kSecAttrIsPermanent: true,
252
+ kSecAttrApplicationTag: alias,
253
+ kSecAttrAccessControl: access
254
+ ],
255
+ kSecPublicKeyAttrs: [
256
+ kSecAttrIsPermanent: true,
257
+ kSecAttrApplicationTag: alias
258
+ ]
259
+ ]
260
+
261
+ return SecKeyCreateRandomKey(attributes, nil)
262
+ }
263
+
264
+ public func definition() -> ModuleDefinition {
265
+
266
+ Name("DeviceCrypto")
267
+
268
+ Function("isAuthCheckAvailable") { () -> String in
269
+ return self.isAuthCheckAvailable()
270
+ }
271
+
272
+ Function("generateKeyPair") { (alias: String, o: [String: Any]) -> String in
273
+ let reqAuth = o["reqAuth"] as! Bool
274
+ let authMethod = AuthMethod(rawValue: o["authMethod"] as! String)
275
+ let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
276
+
277
+ if reqAuth && self.isAuthCheckAvailable() != AuthCheckResult.AVAILABLE.rawValue {
278
+ throw NSError(
279
+ domain: "DeviceCrypto",
280
+ code: 1,
281
+ userInfo: [NSLocalizedDescriptionKey: "NO_AUTH_AVAILABLE"]
282
+ )
283
+ }
284
+
285
+ let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
286
+ if secKey != nil {
287
+ return SecureSigningModuleResult.KEY_PAIR_ALREADY_EXISTS.rawValue
288
+ }
289
+
290
+ switch algoType {
291
+ case .ECDSA_SECP256R1_SHA256:
292
+ self.buildECDSA_SECP256R1_SHA256(alias: alias, reqAuth: reqAuth, authMethod: authMethod!)
293
+ case .RSA_2048_PKCS1:
294
+ self.buildRSA_2048_PKCS1(alias: alias, reqAuth: reqAuth, authMethod: authMethod!)
295
+ default:
296
+ throw NSError(
297
+ domain: "DeviceCrypto",
298
+ code: 1,
299
+ userInfo: [NSLocalizedDescriptionKey: "INVALID_ALGORITHM_TYPE"]
300
+ )
301
+ }
302
+
303
+ return SecureSigningModuleResult.KEY_PAIR_GENERATED.rawValue
304
+ }
305
+
306
+ Function("removeKeyPair") { (alias: String) -> Bool in
307
+ return self.removeKeyStoreEntry(alias)
308
+ }
309
+
310
+ Function("aliases") { () -> [String] in
311
+ return self.getAliases()
312
+ }
313
+
314
+ Function("getPublicKey") { (alias: String) -> String? in
315
+ let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPublic)
316
+ guard let secKey else { return nil }
317
+ return self.retrievePublicKey(secKey)
318
+ }
319
+
320
+ AsyncFunction("sign") { (alias: String, data: String, o: [String: Any]) -> String? in
321
+ let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
322
+ guard let secKey else { return nil }
323
+
324
+ let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
325
+ let algo = self.toiOSAlgo(algorithm: algoType!)
326
+
327
+ var signingError: Unmanaged<CFError>?
328
+ let signatureCF = SecKeyCreateSignature(
329
+ secKey,
330
+ algo,
331
+ Data(data.utf8) as CFData,
332
+ &signingError
333
+ )
334
+
335
+ if let error = signingError?.takeRetainedValue() {
336
+ throw error as Error
337
+ }
338
+
339
+ guard let signatureCF else { return nil }
340
+ let signature = signatureCF as Data
341
+ return signature.base64EncodedString()
342
+ }
343
+
344
+ Function("verify") { (alias: String, data: String, signature: String, o: [String: Any]) -> Bool? in
345
+ let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
346
+ guard let secKey else { return nil }
347
+
348
+ guard let publicKey = SecKeyCopyPublicKey(secKey) else { return nil }
349
+ guard let signatureData = Data(base64Encoded: signature) else { return nil }
350
+
351
+ let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
352
+ let algo = self.toiOSAlgo(algorithm: algoType!)
353
+
354
+ let valid = SecKeyVerifySignature(
355
+ publicKey,
356
+ algo,
357
+ Data(data.utf8) as CFData,
358
+ signatureData as CFData,
359
+ nil
360
+ )
361
+ return valid
362
+ }
363
+
364
+ AsyncFunction("encrypt") { (alias: String, data: String, o: [String: Any]) -> String? in
365
+ let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPublic)
366
+ guard let secKey else { return nil }
367
+ let publicKey = SecKeyCopyPublicKey(secKey)!
368
+
369
+ let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
370
+ let algo = self.toiOSAlgo(algorithm: algoType!)
371
+
372
+ guard let encrypted = SecKeyCreateEncryptedData(
373
+ publicKey,
374
+ algo,
375
+ Data(data.utf8) as CFData,
376
+ nil
377
+ ) as Data? else {
378
+ return nil
379
+ }
380
+
381
+ return encrypted.base64EncodedString()
382
+ }
383
+
384
+ AsyncFunction("decrypt") { (alias: String, data: String, o: [String: Any]) -> String? in
385
+ let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
386
+ guard let secKey else { return nil }
387
+ guard let encryptedData = Data(base64Encoded: data) else { return nil }
388
+
389
+ let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
390
+ let algo = self.toiOSAlgo(algorithm: algoType!)
391
+
392
+ guard let decrypted = SecKeyCreateDecryptedData(
393
+ secKey,
394
+ algo,
395
+ encryptedData as CFData,
396
+ nil
397
+ ) as Data? else {
398
+ return nil
399
+ }
400
+
401
+ return String(data: decrypted, encoding: .utf8)
402
+ }
403
+ }
404
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "expo-device-crypto",
3
+ "version": "0.1.0",
4
+ "description": "Crypto for Expo using device hardware capabilities",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "scripts": {
8
+ "build": "expo-module build",
9
+ "clean": "expo-module clean",
10
+ "lint": "expo-module lint",
11
+ "test": "expo-module test",
12
+ "prepare": "expo-module prepare",
13
+ "prepublishOnly": "expo-module prepublishOnly",
14
+ "expo-module": "expo-module",
15
+ "open:ios": "xed example/ios",
16
+ "open:android": "open -a \"Android Studio\" example/android"
17
+ },
18
+ "keywords": [
19
+ "expo",
20
+ "expo-module",
21
+ "react-native",
22
+ "native-module",
23
+ "crypto",
24
+ "cryptography",
25
+ "device-crypto",
26
+ "hardware-backed",
27
+ "secure-enclave",
28
+ "android-keystore",
29
+ "ios-keychain",
30
+ "key-management",
31
+ "secure-key-storage",
32
+ "digital-signature",
33
+ "ecdsa",
34
+ "secp256r1",
35
+ "rsa",
36
+ "rsa-pkcs1"
37
+ ],
38
+ "repository": "https://github.com/piotr-pietras/expo-device-crypto",
39
+ "bugs": {
40
+ "url": "https://github.com/piotr-pietras/expo-device-crypto/issues"
41
+ },
42
+ "author": "Piotr <pietraspiotr94@gmail.com> (https://github.com/piotr-pietras)",
43
+ "license": "MIT",
44
+ "homepage": "https://github.com/piotr-pietras/expo-device-crypto#readme",
45
+ "dependencies": {},
46
+ "devDependencies": {
47
+ "expo-module-scripts": "55.0.2"
48
+ },
49
+ "overrides": {
50
+ "@react-native/dev-middleware": "0.84.1"
51
+ },
52
+ "peerDependencies": {
53
+ "expo": "*"
54
+ }
55
+ }
@@ -0,0 +1,106 @@
1
+ export enum GenerateKeyPairResult {
2
+ KEY_PAIR_GENERATED = "KEY_PAIR_GENERATED",
3
+ KEY_PAIR_ALREADY_EXISTS = "KEY_PAIR_ALREADY_EXISTS",
4
+ NOT_AVAILABLE = "NOT_AVAILABLE",
5
+ }
6
+
7
+ export enum AuthCheckResult {
8
+ AVAILABLE = "AVAILABLE",
9
+ NO_HARDWARE = "NO_HARDWARE",
10
+ UNAVAILABLE = "UNAVAILABLE",
11
+ }
12
+
13
+ export enum AuthMethod {
14
+ PASSCODE = "PASSCODE",
15
+ PASSCODE_OR_BIOMETRIC = "PASSCODE_OR_BIOMETRIC",
16
+ }
17
+
18
+ export enum SigningAlgorithm {
19
+ ECDSA_SECP256R1_SHA256 = "ECDSA_SECP256R1_SHA256",
20
+ }
21
+
22
+ export enum EncryptionAlgorithm {
23
+ RSA_2048_PKCS1 = "RSA_2048_PKCS1",
24
+ }
25
+
26
+ export interface GenerateKeyPairOptions {
27
+ /**
28
+ * The algorithm type of the key to generate.
29
+ */
30
+ algorithmType: SigningAlgorithm | EncryptionAlgorithm;
31
+ /**
32
+ * Whether to require authentication to sign.
33
+ * Setting this to true will prompt biometric or passcode authentication before signing.
34
+ *
35
+ * Please check the *`isAuthCheckAvailable`* function to see if authentication is available.
36
+ * If authentication is not available, generating a key pair will throw an error.
37
+ * @default false
38
+ */
39
+ requireAuthentication?: boolean;
40
+ /**
41
+ * The method of authentication to use.
42
+ * Note that on Android you have to define it when signing.
43
+ *
44
+ * If you want to allow to use Face ID, you need to add the following to your app.json config file:
45
+ * ```json
46
+ * "ios": {
47
+ * "infoPlist": {
48
+ * "NSFaceIDUsageDescription": "We use Face ID to secure your data.",
49
+ * }
50
+ * }
51
+ * ```
52
+ * @default AuthMethod.PASSCODE_OR_BIOMETRIC
53
+ * @platform ios
54
+ */
55
+ authMethod?: AuthMethod;
56
+ }
57
+
58
+ interface BaseSignDecryptOptions {
59
+ /**
60
+ * The title of the prompt to show when authentication is required.
61
+ * @default "Unlock"
62
+ * @platform android
63
+ */
64
+ promptTitle?: string;
65
+ /**
66
+ * The subtitle of the prompt to show when authentication is required.
67
+ * @default "Enter your PIN to continue"
68
+ * @platform android
69
+ */
70
+ promptSubtitle?: string;
71
+ /**
72
+ * The method of authentication to use.
73
+ * Note that on iOS you have to define it when generating the key pair.
74
+ * @default AuthMethod.PASSCODE_OR_BIOMETRIC
75
+ * @platform android
76
+ */
77
+ authMethod?: AuthMethod;
78
+ }
79
+
80
+ interface BaseSigningOptions {
81
+ /**
82
+ * The algorithm type of the key to use for signing.
83
+ */
84
+ algorithmType?: SigningAlgorithm;
85
+ }
86
+
87
+ interface BaseEncryptionOptions {
88
+ /**
89
+ * The algorithm type of the key to use for encrypting.
90
+ */
91
+ algorithmType?: EncryptionAlgorithm;
92
+ }
93
+
94
+ export interface SignOptions extends BaseSignDecryptOptions, BaseSigningOptions {}
95
+ export interface VerifyOptions extends BaseSigningOptions {}
96
+
97
+ export interface DecryptOptions extends BaseSignDecryptOptions, BaseEncryptionOptions {}
98
+ export interface EncryptOptions extends BaseEncryptionOptions {}
99
+
100
+ export interface GetPublicKeyOptions {
101
+ /**
102
+ * The format of the public key to return.
103
+ * @default "PEM" (Base64 of PEM-encoded SubjectPublicKeyInfo (SPKI) for P‑256)
104
+ */
105
+ format?: "PEM";
106
+ }