react-native-security-suite 0.1.2 → 0.2.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.
- package/README.md +40 -3
- package/android/.gradle/7.4/checksums/checksums.lock +0 -0
- package/android/.gradle/7.4/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/7.4/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/7.4/dependencies-accessors/dependencies-accessors.lock +0 -0
- package/android/.gradle/7.4/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/7.4/executionHistory/executionHistory.bin +0 -0
- package/android/.gradle/7.4/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/7.4/fileChanges/last-build.bin +0 -0
- package/android/.gradle/7.4/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/7.4/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/7.4/fileHashes/resourceHashesCache.bin +0 -0
- package/android/.gradle/7.4/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
- package/android/.gradle/file-system.probe +0 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/.idea/compiler.xml +6 -0
- package/android/.idea/gradle.xml +17 -0
- package/android/.idea/jarRepositories.xml +40 -0
- package/android/.idea/misc.xml +10 -0
- package/android/.idea/vcs.xml +6 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/android/gradlew +234 -0
- package/android/gradlew.bat +89 -0
- package/android/local.properties +8 -0
- package/android/src/main/java/com/reactnativesecuritysuite/SecuritySuiteModule.java +182 -18
- package/android/src/main/java/com/reactnativesecuritysuite/SecuritySuitePackage.java +12 -12
- package/android/src/main/java/com/reactnativesecuritysuite/StorageEncryption.java +53 -0
- package/ios/DataHashingMethods.swift +196 -0
- package/ios/SecuritySuite-Bridging-Header.h +1 -0
- package/ios/SecuritySuite.m +14 -0
- package/ios/SecuritySuite.swift +129 -0
- package/ios/SecuritySuite.xcodeproj/project.pbxproj +9 -15
- package/ios/SecuritySuite.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/ios/SecuritySuite.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/ios/SecuritySuite.xcodeproj/project.xcworkspace/xcuserdata/Navabi.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/ios/SecuritySuite.xcodeproj/xcuserdata/Navabi.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
- package/ios/StorageEncryption.swift +81 -0
- package/lib/commonjs/helpers.js +19 -0
- package/lib/commonjs/helpers.js.map +1 -0
- package/lib/commonjs/index.js +173 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/helpers.js +10 -0
- package/lib/module/helpers.js.map +1 -0
- package/lib/module/index.js +143 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/helpers.d.ts +1 -0
- package/lib/typescript/index.d.ts +19 -0
- package/package.json +12 -1
- package/react-native-security-suite.podspec +1 -1
- package/src/helpers.ts +8 -0
- package/src/index.tsx +195 -3
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
package com.reactnativesecuritysuite;
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
import com.facebook.react.bridge.Callback;
|
|
5
4
|
import com.facebook.react.bridge.Promise;
|
|
6
5
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
7
6
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
@@ -9,30 +8,195 @@ import com.facebook.react.bridge.ReactMethod;
|
|
|
9
8
|
import com.facebook.react.module.annotations.ReactModule;
|
|
10
9
|
import com.scottyab.rootbeer.RootBeer;
|
|
11
10
|
|
|
11
|
+
import androidx.annotation.NonNull;
|
|
12
|
+
|
|
13
|
+
import android.provider.Settings;
|
|
14
|
+
import android.util.Base64;
|
|
15
|
+
import android.util.Log;
|
|
16
|
+
|
|
17
|
+
import java.nio.charset.Charset;
|
|
18
|
+
import java.security.InvalidAlgorithmParameterException;
|
|
19
|
+
import java.security.InvalidKeyException;
|
|
20
|
+
import java.security.KeyFactory;
|
|
21
|
+
import java.security.KeyPair;
|
|
22
|
+
import java.security.KeyPairGenerator;
|
|
23
|
+
import java.security.NoSuchAlgorithmException;
|
|
24
|
+
import java.security.PrivateKey;
|
|
25
|
+
import java.security.PublicKey;
|
|
26
|
+
import java.security.spec.ECGenParameterSpec;
|
|
27
|
+
import java.security.spec.X509EncodedKeySpec;
|
|
28
|
+
|
|
29
|
+
import javax.crypto.BadPaddingException;
|
|
30
|
+
import javax.crypto.Cipher;
|
|
31
|
+
import javax.crypto.IllegalBlockSizeException;
|
|
32
|
+
import javax.crypto.KeyAgreement;
|
|
33
|
+
import javax.crypto.NoSuchPaddingException;
|
|
34
|
+
import javax.crypto.SecretKey;
|
|
35
|
+
import javax.crypto.spec.GCMParameterSpec;
|
|
36
|
+
import javax.crypto.spec.SecretKeySpec;
|
|
37
|
+
|
|
12
38
|
@ReactModule(name = SecuritySuiteModule.NAME)
|
|
13
39
|
public class SecuritySuiteModule extends ReactContextBaseJavaModule {
|
|
14
|
-
|
|
15
|
-
|
|
40
|
+
public static final String NAME = "SecuritySuite";
|
|
41
|
+
private ReactApplicationContext context;
|
|
42
|
+
|
|
43
|
+
KeyPairGenerator kpg;
|
|
44
|
+
KeyPair kp;
|
|
45
|
+
PublicKey publicKey;
|
|
46
|
+
PublicKey serverPublicKey;
|
|
47
|
+
PrivateKey privateKey;
|
|
48
|
+
String sharedKey;
|
|
49
|
+
SecretKey secretKey;
|
|
50
|
+
|
|
51
|
+
public SecuritySuiteModule(ReactApplicationContext reactContext) {
|
|
52
|
+
super(reactContext);
|
|
53
|
+
context = reactContext;
|
|
54
|
+
generateKeyPair();
|
|
55
|
+
}
|
|
16
56
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
57
|
+
private void generateKeyPair() {
|
|
58
|
+
try {
|
|
59
|
+
kpg = KeyPairGenerator.getInstance("EC");
|
|
60
|
+
ECGenParameterSpec prime256v1ParamSpec = new ECGenParameterSpec("secp256r1");
|
|
61
|
+
kpg.initialize(prime256v1ParamSpec);
|
|
62
|
+
kp = kpg.genKeyPair();
|
|
63
|
+
publicKey = kp.getPublic();
|
|
64
|
+
privateKey = kp.getPrivate();
|
|
65
|
+
} catch (Exception e) {
|
|
66
|
+
Log.e("generateKeyPair Error: ", String.valueOf(e));
|
|
20
67
|
}
|
|
68
|
+
}
|
|
21
69
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
70
|
+
@ReactMethod
|
|
71
|
+
public void getPublicKey(Promise promise) {
|
|
72
|
+
String base64DEREncoded = Base64.encodeToString(publicKey.getEncoded(), Base64.DEFAULT);
|
|
73
|
+
promise.resolve(base64DEREncoded);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private static SecretKey agreeSecretKey(PrivateKey prk_self, PublicKey pbk_peer, boolean lastPhase) throws Exception {
|
|
77
|
+
SecretKey desSpec;
|
|
78
|
+
try {
|
|
79
|
+
KeyAgreement keyAgree = KeyAgreement.getInstance("ECDH");
|
|
80
|
+
keyAgree.init(prk_self);
|
|
81
|
+
keyAgree.doPhase(pbk_peer, true);
|
|
82
|
+
byte[] sec = keyAgree.generateSecret();
|
|
83
|
+
desSpec = new SecretKeySpec(sec, "AES");
|
|
84
|
+
} catch (NoSuchAlgorithmException e) {
|
|
85
|
+
throw new Exception();
|
|
86
|
+
} catch (InvalidKeyException e) {
|
|
87
|
+
throw new Exception();
|
|
26
88
|
}
|
|
89
|
+
return desSpec;
|
|
90
|
+
}
|
|
27
91
|
|
|
92
|
+
@ReactMethod
|
|
93
|
+
public void getSharedKey(String serverPK, Promise promise) {
|
|
94
|
+
try {
|
|
95
|
+
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.decode(serverPK.getBytes(), Base64.DEFAULT)); // Change ASN1 to publicKey
|
|
96
|
+
KeyFactory keyFactory = KeyFactory.getInstance("EC");
|
|
97
|
+
serverPublicKey = keyFactory.generatePublic(keySpec);
|
|
98
|
+
secretKey = agreeSecretKey(kp.getPrivate(), serverPublicKey, true);
|
|
99
|
+
sharedKey = Base64.encodeToString(secretKey.getEncoded(), Base64.DEFAULT);
|
|
100
|
+
promise.resolve(sharedKey);
|
|
101
|
+
} catch (Exception e) {
|
|
102
|
+
Log.e("getSharedKey Error: ", String.valueOf(e));
|
|
103
|
+
promise.reject(String.valueOf(e));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
28
106
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
107
|
+
@ReactMethod
|
|
108
|
+
public void encrypt(String input, Promise promise) {
|
|
109
|
+
try {
|
|
110
|
+
byte[] inputByte = input.getBytes();
|
|
111
|
+
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
|
112
|
+
byte[] decodedKey = Base64.decode(sharedKey, Base64.DEFAULT);
|
|
113
|
+
SecretKey secretKeySpec = new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES");
|
|
114
|
+
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
|
|
115
|
+
byte[] iv = cipher.getIV();
|
|
116
|
+
assert iv.length == 12;
|
|
117
|
+
byte[] cipherText = cipher.doFinal(inputByte);
|
|
118
|
+
if (cipherText.length != inputByte.length + 16)
|
|
119
|
+
throw new IllegalStateException();
|
|
120
|
+
byte[] output = new byte[12 + inputByte.length + 16];
|
|
121
|
+
System.arraycopy(iv, 0, output, 0, 12);
|
|
122
|
+
System.arraycopy(cipherText, 0, output, 12, cipherText.length);
|
|
123
|
+
promise.resolve(Base64.encodeToString(output, Base64.NO_WRAP));
|
|
124
|
+
} catch (Exception e) {
|
|
125
|
+
Log.e("encrypt Error: ", String.valueOf(e));
|
|
126
|
+
promise.reject(String.valueOf(e));
|
|
35
127
|
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@ReactMethod
|
|
131
|
+
public void decrypt(String input, Promise promise) {
|
|
132
|
+
try {
|
|
133
|
+
byte[] inputBytes = Base64.decode(input.getBytes(), Base64.DEFAULT);
|
|
134
|
+
if (inputBytes.length < 12 + 16) throw new IllegalArgumentException();
|
|
135
|
+
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
|
136
|
+
GCMParameterSpec params = new GCMParameterSpec(128, inputBytes, 0, 12);
|
|
137
|
+
byte[] decodedKey = Base64.decode(sharedKey, Base64.DEFAULT);
|
|
138
|
+
SecretKey secretKeySpec = new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES");
|
|
139
|
+
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, params);
|
|
140
|
+
byte[] plaintext = cipher.doFinal(inputBytes, 12, inputBytes.length - 12);
|
|
141
|
+
String decrypted = new String(plaintext, Charset.forName("UTF-8"));
|
|
142
|
+
promise.resolve(decrypted);
|
|
143
|
+
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
|
|
144
|
+
promise.reject(e);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@ReactMethod
|
|
149
|
+
public void getDeviceId(Callback callback) {
|
|
150
|
+
try {
|
|
151
|
+
String deviceId = getAndroidId();
|
|
152
|
+
callback.invoke(deviceId, null);
|
|
153
|
+
} catch (Exception e) {
|
|
154
|
+
callback.invoke(null, e);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@ReactMethod
|
|
159
|
+
public void storageEncrypt(String input, String secretKey, Boolean hardEncryption, Callback callback) {
|
|
160
|
+
try {
|
|
161
|
+
String key = getAndroidId();
|
|
162
|
+
if (secretKey != null) {
|
|
163
|
+
key = secretKey;
|
|
164
|
+
}
|
|
165
|
+
String encryptedMessage = StorageEncryption.encrypt(input, key, hardEncryption);
|
|
166
|
+
callback.invoke(encryptedMessage, null);
|
|
167
|
+
} catch (Exception e) {
|
|
168
|
+
callback.invoke(null, e);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@ReactMethod
|
|
173
|
+
public void storageDecrypt(String input, String secretKey, Boolean hardEncryption, Callback callback) {
|
|
174
|
+
try {
|
|
175
|
+
String key = getAndroidId();
|
|
176
|
+
if (secretKey != null) {
|
|
177
|
+
key = secretKey;
|
|
178
|
+
}
|
|
179
|
+
String decryptedMessage = StorageEncryption.decrypt(input, key);
|
|
180
|
+
callback.invoke(decryptedMessage, null);
|
|
181
|
+
} catch (Exception e) {
|
|
182
|
+
callback.invoke(null, e.getMessage());
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private String getAndroidId() {
|
|
187
|
+
return Settings.Secure.getString(context.getContentResolver(),
|
|
188
|
+
Settings.Secure.ANDROID_ID);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@ReactMethod
|
|
192
|
+
public void deviceHasSecurityRisk(Promise promise) {
|
|
193
|
+
RootBeer rootBeer = new RootBeer(context);
|
|
194
|
+
promise.resolve(rootBeer.isRooted());
|
|
195
|
+
}
|
|
36
196
|
|
|
37
|
-
|
|
197
|
+
@Override
|
|
198
|
+
@NonNull
|
|
199
|
+
public String getName() {
|
|
200
|
+
return NAME;
|
|
201
|
+
}
|
|
38
202
|
}
|
|
@@ -12,17 +12,17 @@ import java.util.Collections;
|
|
|
12
12
|
import java.util.List;
|
|
13
13
|
|
|
14
14
|
public class SecuritySuitePackage implements ReactPackage {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
@NonNull
|
|
16
|
+
@Override
|
|
17
|
+
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
|
18
|
+
List<NativeModule> modules = new ArrayList<>();
|
|
19
|
+
modules.add(new SecuritySuiteModule(reactContext));
|
|
20
|
+
return modules;
|
|
21
|
+
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
@NonNull
|
|
24
|
+
@Override
|
|
25
|
+
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
|
|
26
|
+
return Collections.emptyList();
|
|
27
|
+
}
|
|
28
28
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package com.reactnativesecuritysuite;
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import android.util.Base64;
|
|
5
|
+
|
|
6
|
+
import java.security.SecureRandom;
|
|
7
|
+
import java.util.Arrays;
|
|
8
|
+
|
|
9
|
+
import javax.crypto.Cipher;
|
|
10
|
+
import javax.crypto.spec.IvParameterSpec;
|
|
11
|
+
import javax.crypto.spec.SecretKeySpec;
|
|
12
|
+
|
|
13
|
+
public class StorageEncryption {
|
|
14
|
+
|
|
15
|
+
public static String encrypt(String input, String encryptionKey, Boolean hardEncryption) {
|
|
16
|
+
try {
|
|
17
|
+
byte[] iv = new byte[16];
|
|
18
|
+
if (hardEncryption) {
|
|
19
|
+
new SecureRandom().nextBytes(iv);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
23
|
+
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptionKey.getBytes("utf-8"), "AES"), new IvParameterSpec(iv));
|
|
24
|
+
byte[] cipherText = cipher.doFinal(input.getBytes("utf-8"));
|
|
25
|
+
byte[] ivAndCipherText = getCombinedArray(iv, cipherText);
|
|
26
|
+
return Base64.encodeToString(ivAndCipherText, Base64.NO_WRAP);
|
|
27
|
+
} catch (Exception e) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public static String decrypt(String encoded, String encryptionKey) {
|
|
33
|
+
try {
|
|
34
|
+
byte[] ivAndCipherText = Base64.decode(encoded, Base64.NO_WRAP);
|
|
35
|
+
byte[] iv = Arrays.copyOfRange(ivAndCipherText, 0, 16);
|
|
36
|
+
byte[] cipherText = Arrays.copyOfRange(ivAndCipherText, 16, ivAndCipherText.length);
|
|
37
|
+
|
|
38
|
+
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
39
|
+
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(encryptionKey.getBytes("utf-8"), "AES"), new IvParameterSpec(iv));
|
|
40
|
+
return new String(cipher.doFinal(cipherText), "utf-8");
|
|
41
|
+
} catch (Exception e) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private static byte[] getCombinedArray(byte[] one, byte[] two) {
|
|
47
|
+
byte[] combined = new byte[one.length + two.length];
|
|
48
|
+
for (int i = 0; i < combined.length; ++i) {
|
|
49
|
+
combined[i] = i < one.length ? one[i] : two[i - one.length];
|
|
50
|
+
}
|
|
51
|
+
return combined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Data+HashingMethods.swift
|
|
3
|
+
//
|
|
4
|
+
// Created by Amir Sepehrom on 5/31/21.
|
|
5
|
+
//
|
|
6
|
+
|
|
7
|
+
import Foundation
|
|
8
|
+
import Security
|
|
9
|
+
import CommonCrypto
|
|
10
|
+
|
|
11
|
+
extension SecKey {
|
|
12
|
+
|
|
13
|
+
fileprivate subscript(attribute attr: CFString) -> Any? {
|
|
14
|
+
let attributes = SecKeyCopyAttributes(self) as? [CFString: Any]
|
|
15
|
+
return attributes?[attr]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Returns key length.
|
|
19
|
+
public var keySize: Int {
|
|
20
|
+
// swiftlint: disable force_cast
|
|
21
|
+
return self[attribute: kSecAttrKeySizeInBits] as! Int
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Returns encryption method of key.
|
|
25
|
+
public var type: KeyType {
|
|
26
|
+
// swiftlint: disable force_cast
|
|
27
|
+
let type = self[attribute: kSecAttrType] as! CFString
|
|
28
|
+
return KeyType(rawValue: type)!
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Public ASN1 header appropriate for current key.
|
|
32
|
+
fileprivate func asn1Header() throws -> Data {
|
|
33
|
+
return try type.asn1Header(keySize: keySize)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Exporable key data with ASN1 header.
|
|
37
|
+
public func bytes() throws -> Data {
|
|
38
|
+
return try asn1Header() + rawBytes()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Raw bytes for key that generated by Security framework.
|
|
42
|
+
public func rawBytes() throws -> Data {
|
|
43
|
+
var error: Unmanaged<CFError>?
|
|
44
|
+
|
|
45
|
+
guard let keyData = SecKeyCopyExternalRepresentation(self, &error) as Data? else {
|
|
46
|
+
throw error?.takeRetainedValue() ?? CryptographyError.invalidKey
|
|
47
|
+
}
|
|
48
|
+
return keyData
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Encryption method
|
|
52
|
+
public enum KeyType: RawRepresentable {
|
|
53
|
+
/// Eliptic curve
|
|
54
|
+
case ec
|
|
55
|
+
/// RSA
|
|
56
|
+
case rsa
|
|
57
|
+
|
|
58
|
+
/// Initilize from kSecAttrKeyType string constant.
|
|
59
|
+
public init?(rawValue: CFString) {
|
|
60
|
+
switch rawValue {
|
|
61
|
+
case kSecAttrKeyTypeEC, kSecAttrKeyTypeECSECPrimeRandom:
|
|
62
|
+
self = .ec
|
|
63
|
+
case kSecAttrKeyTypeRSA:
|
|
64
|
+
self = .rsa
|
|
65
|
+
default:
|
|
66
|
+
return nil
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// kSecAttrKeyType counterpart of option.
|
|
71
|
+
public var rawValue: CFString {
|
|
72
|
+
switch self {
|
|
73
|
+
case .ec:
|
|
74
|
+
return kSecAttrKeyTypeECSECPrimeRandom
|
|
75
|
+
case .rsa:
|
|
76
|
+
return kSecAttrKeyTypeRSA
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// ASN1 headers for encrypting public keys.
|
|
81
|
+
public struct ASN1 {
|
|
82
|
+
public static let rsa2048 = Data(base64Encoded: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A")!
|
|
83
|
+
public static let rsa4096 = Data(base64Encoded: "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A")!
|
|
84
|
+
public static let ec256 = Data(base64Encoded: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgA=")!
|
|
85
|
+
public static let ec384 = Data(base64Encoded: "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgA=")!
|
|
86
|
+
public static let ec521 = Data(base64Encoded: "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAA==")!
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func asn1Header(keySize: Int) throws -> Data {
|
|
90
|
+
switch (self, keySize) {
|
|
91
|
+
case (.ec, 256):
|
|
92
|
+
return ASN1.ec256
|
|
93
|
+
case (.ec, 384):
|
|
94
|
+
return ASN1.ec384
|
|
95
|
+
case (.ec, 521):
|
|
96
|
+
return ASN1.ec521
|
|
97
|
+
case (.rsa, 2048):
|
|
98
|
+
return ASN1.rsa2048
|
|
99
|
+
case (.rsa, 4096):
|
|
100
|
+
return ASN1.rsa4096
|
|
101
|
+
default:
|
|
102
|
+
throw CryptographyError.unsupported
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public enum CryptographyError: Error {
|
|
109
|
+
/// Key data is not valid or not conform to expected type and length.
|
|
110
|
+
case invalidKey
|
|
111
|
+
/// Encryption failed.
|
|
112
|
+
case failedEncryption
|
|
113
|
+
/// Decryption failed.
|
|
114
|
+
case failedDecryption
|
|
115
|
+
/// Data that passed to encrypt/decrypt is empty.
|
|
116
|
+
case emptyData
|
|
117
|
+
/// Encrypted data is not valid or encrypted with another key type.
|
|
118
|
+
case invalidData
|
|
119
|
+
/// Encryption/Decryption method is not supported.
|
|
120
|
+
case unsupported
|
|
121
|
+
|
|
122
|
+
// Enclave
|
|
123
|
+
/// Private key is not exist on the device enclave.
|
|
124
|
+
case keyNotFound
|
|
125
|
+
/// Operation with private key which saved in enclave is not posssible.
|
|
126
|
+
case notAllowed
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public enum Digest: String {
|
|
130
|
+
case md5 = "MD5"
|
|
131
|
+
case sha1 = "SHA1"
|
|
132
|
+
case sha256 = "SHA256"
|
|
133
|
+
case sha384 = "SHA384"
|
|
134
|
+
case sha512 = "SHA512"
|
|
135
|
+
|
|
136
|
+
public var length: Int {
|
|
137
|
+
switch self {
|
|
138
|
+
case .md5:
|
|
139
|
+
return Int(CC_MD5_DIGEST_LENGTH)
|
|
140
|
+
case .sha1:
|
|
141
|
+
return Int(CC_SHA1_DIGEST_LENGTH)
|
|
142
|
+
case .sha256:
|
|
143
|
+
return Int(CC_SHA256_DIGEST_LENGTH)
|
|
144
|
+
case .sha384:
|
|
145
|
+
return Int(CC_SHA384_DIGEST_LENGTH)
|
|
146
|
+
case .sha512:
|
|
147
|
+
return Int(CC_SHA512_DIGEST_LENGTH)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public var hmacAlgorithm: CCHmacAlgorithm {
|
|
152
|
+
switch self {
|
|
153
|
+
case .md5:
|
|
154
|
+
return CCHmacAlgorithm(kCCHmacAlgMD5)
|
|
155
|
+
case .sha1:
|
|
156
|
+
return CCHmacAlgorithm(kCCHmacAlgSHA1)
|
|
157
|
+
case .sha256:
|
|
158
|
+
return CCHmacAlgorithm(kCCHmacAlgSHA256)
|
|
159
|
+
case .sha384:
|
|
160
|
+
return CCHmacAlgorithm(kCCHmacAlgSHA384)
|
|
161
|
+
case .sha512:
|
|
162
|
+
return CCHmacAlgorithm(kCCHmacAlgSHA512)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
extension Data {
|
|
168
|
+
/**
|
|
169
|
+
Calculates hash digest of data.
|
|
170
|
+
|
|
171
|
+
- Parameter digest: digest type. Currently only SHA is supported.
|
|
172
|
+
- Returns: A data object with length equal to digest length.
|
|
173
|
+
*/
|
|
174
|
+
public func hash(digest: Digest) -> Data {
|
|
175
|
+
guard !isEmpty else { return Data() }
|
|
176
|
+
var result = [UInt8](repeating: 0, count: digest.length)
|
|
177
|
+
self.withUnsafeBytes { (buf: UnsafeRawBufferPointer) -> Void in
|
|
178
|
+
let ptr = buf.baseAddress!
|
|
179
|
+
let dataLen = CC_LONG(buf.count)
|
|
180
|
+
switch digest {
|
|
181
|
+
case .md5:
|
|
182
|
+
CC_MD5(ptr, dataLen, &result)
|
|
183
|
+
case .sha1:
|
|
184
|
+
CC_SHA1(ptr, dataLen, &result)
|
|
185
|
+
case .sha256:
|
|
186
|
+
CC_SHA256(ptr, dataLen, &result)
|
|
187
|
+
case .sha384:
|
|
188
|
+
CC_SHA384(ptr, dataLen, &result)
|
|
189
|
+
case .sha512:
|
|
190
|
+
CC_SHA512(ptr, dataLen, &result)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return Data(result)
|
|
195
|
+
}
|
|
196
|
+
}
|
package/ios/SecuritySuite.m
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
@interface RCT_EXTERN_MODULE(SecuritySuite, NSObject)
|
|
4
4
|
|
|
5
|
+
RCT_EXTERN_METHOD(getPublicKey:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
|
|
6
|
+
|
|
7
|
+
RCT_EXTERN_METHOD(getSharedKey:(NSString)serverPK withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
|
|
8
|
+
|
|
9
|
+
RCT_EXTERN_METHOD(encrypt:(NSString)input withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
|
|
10
|
+
|
|
11
|
+
RCT_EXTERN_METHOD(decrypt:(NSString)input withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
|
|
12
|
+
|
|
13
|
+
RCT_EXTERN_METHOD(getDeviceId:(RCTResponseSenderBlock)callback)
|
|
14
|
+
|
|
15
|
+
RCT_EXTERN_METHOD(storageEncrypt:(NSString)input withSecretKey:(NSString*)secretKey withHardEncryption:(BOOL)hardEncryption withCallback:(RCTResponseSenderBlock)callback)
|
|
16
|
+
|
|
17
|
+
RCT_EXTERN_METHOD(storageDecrypt:(NSString)input withSecretKey:(NSString*)secretKey withHardEncryption:(BOOL)hardEncryption withCallback:(RCTResponseSenderBlock)callback)
|
|
18
|
+
|
|
5
19
|
RCT_EXTERN_METHOD(deviceHasSecurityRisk:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
|
|
6
20
|
|
|
7
21
|
+ (BOOL)requiresMainQueueSetup
|
package/ios/SecuritySuite.swift
CHANGED
|
@@ -1,7 +1,128 @@
|
|
|
1
1
|
import IOSSecuritySuite
|
|
2
|
+
import Foundation
|
|
3
|
+
import CryptoKit
|
|
4
|
+
import SwiftUI
|
|
2
5
|
|
|
6
|
+
@available(iOS 13.0, *)
|
|
3
7
|
@objc(SecuritySuite)
|
|
4
8
|
class SecuritySuite: NSObject {
|
|
9
|
+
var privateKey: String!,
|
|
10
|
+
publicKey: String!,
|
|
11
|
+
sharedKey: String!,
|
|
12
|
+
keyData: Data!
|
|
13
|
+
|
|
14
|
+
@objc(getPublicKey:withRejecter:)
|
|
15
|
+
func getPublicKey(resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
|
|
16
|
+
do {
|
|
17
|
+
privateKey = P256.KeyAgreement.PrivateKey().rawRepresentation.base64EncodedString()
|
|
18
|
+
keyData = Data(base64Encoded: privateKey as! String)!
|
|
19
|
+
publicKey = try? (ASN1.ec256 + [0x04] + P256.KeyAgreement.PrivateKey(rawRepresentation: keyData).publicKey.rawRepresentation).base64EncodedString()
|
|
20
|
+
|
|
21
|
+
resolve(publicKey)
|
|
22
|
+
} catch {
|
|
23
|
+
reject("error", "GET_PUBLIC_KEY_ERROR", nil)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@objc(getSharedKey:withResolver:withRejecter:)
|
|
28
|
+
func getSharedKey(serverPK: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
|
|
29
|
+
do {
|
|
30
|
+
print("privateKey", privateKey)
|
|
31
|
+
guard let serverPublicKeyData = Data(base64Encoded: serverPK as String),
|
|
32
|
+
let privateKeyData = Data(base64Encoded: privateKey as String) else { return }
|
|
33
|
+
|
|
34
|
+
sharedKey = try? P256.KeyAgreement.PrivateKey(rawRepresentation: privateKeyData).sharedSecretFromKeyAgreement(with: .init(rawRepresentation: serverPublicKeyData.dropFirst(ASN1.ec256.count + 1)))
|
|
35
|
+
.withUnsafeBytes({ Data(buffer: $0.bindMemory(to: UInt8.self)) })
|
|
36
|
+
.base64EncodedString()
|
|
37
|
+
|
|
38
|
+
resolve(sharedKey)
|
|
39
|
+
} catch {
|
|
40
|
+
reject("error", "GET_SHARED_KEY_ERROR", nil)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@objc(encrypt:withResolver:withRejecter:)
|
|
45
|
+
func encrypt(input: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
|
|
46
|
+
do {
|
|
47
|
+
guard let keyData = Data(base64Encoded: sharedKey) else {
|
|
48
|
+
reject("error", "DECRYPT_SHARED_KEY_ERROR", nil)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
let data = Data((input as String).utf8)
|
|
52
|
+
if keyData == nil {
|
|
53
|
+
reject("error", "keyData is null", nil)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
let key = SymmetricKey(data: keyData)
|
|
57
|
+
let output = try? AES.GCM.seal(data, using: key).combined?.base64EncodedString()
|
|
58
|
+
resolve(output)
|
|
59
|
+
} catch {
|
|
60
|
+
reject("error", "ENCRYPT_ERROR", nil)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@objc(decrypt:withResolver:withRejecter:)
|
|
65
|
+
func decrypt(input: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
|
|
66
|
+
do {
|
|
67
|
+
guard let keyData = Data(base64Encoded: sharedKey) else {
|
|
68
|
+
reject("error", "DECRYPT_SHARED_KEY_ERROR", nil)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
guard let data = Data(base64Encoded: "\(input)") else {
|
|
72
|
+
reject("error", "INPUT_ERROR", nil)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
let key = SymmetricKey(data: keyData)
|
|
76
|
+
let output = ( try? AES.GCM.open(.init(combined: data), using: key)).map({String(decoding: $0, as: UTF8.self)})
|
|
77
|
+
|
|
78
|
+
resolve(output)
|
|
79
|
+
} catch {
|
|
80
|
+
reject("error", "DECRYPT_ERROR", nil)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@objc(storageEncrypt:withSecretKey:withHardEncryption:withCallback:)
|
|
85
|
+
func storageEncrypt(input: NSString, secretKey: NSString, hardEncryption: Bool, callback: RCTResponseSenderBlock) -> Void {
|
|
86
|
+
do {
|
|
87
|
+
var encryptionKey = getDeviceId();
|
|
88
|
+
if secretKey != nil {
|
|
89
|
+
encryptionKey = secretKey as String;
|
|
90
|
+
}
|
|
91
|
+
let storageEncryption: StorageEncryption = StorageEncryption()
|
|
92
|
+
let encrypted = try? storageEncryption.encrypt(plain: "\(input)", encryptionKey: encryptionKey, hardEncryption: hardEncryption)
|
|
93
|
+
callback([encrypted, NSNull()])
|
|
94
|
+
} catch {
|
|
95
|
+
callback([NSNull(), "SOFT_DECRYPT_ERROR"])
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@objc(storageDecrypt:withSecretKey:withHardEncryption:withCallback:)
|
|
100
|
+
func storageDecrypt(input: NSString, secretKey: NSString, hardEncryption: Bool, callback: RCTResponseSenderBlock) -> Void {
|
|
101
|
+
do {
|
|
102
|
+
var encryptionKey = getDeviceId()
|
|
103
|
+
if secretKey != nil {
|
|
104
|
+
encryptionKey = secretKey as String;
|
|
105
|
+
}
|
|
106
|
+
let storageEncryption: StorageEncryption = StorageEncryption()
|
|
107
|
+
let decrypted = try? storageEncryption.decrypt(decoded: "\(input)", encryptionKey: encryptionKey, hardEncryption: hardEncryption)
|
|
108
|
+
callback([decrypted, NSNull()])
|
|
109
|
+
} catch {
|
|
110
|
+
callback([NSNull(), "SOFT_DECRYPT_ERROR"])
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@objc(getDeviceId:)
|
|
115
|
+
func getDeviceId(callback: RCTResponseSenderBlock) -> Void {
|
|
116
|
+
do {
|
|
117
|
+
callback([getDeviceId(), NSNull()]);
|
|
118
|
+
} catch {
|
|
119
|
+
callback([NSNull(), "GET_DEVICE_ID_ERROR"]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func getDeviceId() -> String {
|
|
124
|
+
return UIDevice.current.identifierForVendor!.uuidString.replacingOccurrences(of: "-", with: "", options: [], range: nil)
|
|
125
|
+
}
|
|
5
126
|
|
|
6
127
|
@objc(deviceHasSecurityRisk:withRejecter:)
|
|
7
128
|
func deviceHasSecurityRisk(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void {
|
|
@@ -13,3 +134,11 @@ class SecuritySuite: NSObject {
|
|
|
13
134
|
}
|
|
14
135
|
}
|
|
15
136
|
}
|
|
137
|
+
|
|
138
|
+
struct ASN1 {
|
|
139
|
+
static let rsa2048 = Data(base64Encoded: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A")!
|
|
140
|
+
static let rsa4096 = Data(base64Encoded: "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A")!
|
|
141
|
+
static let ec256 = Data(base64Encoded: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgA=")!
|
|
142
|
+
static let ec384 = Data(base64Encoded: "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgA=")!
|
|
143
|
+
static let ec521 = Data(base64Encoded: "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQ=")!
|
|
144
|
+
}
|