Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ECCrypto native module with hardware-back key storage / secure enclave . #353

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.0.0'
implementation 'com.facebook.react:react-native:0.60.3'

compile 'com.google.crypto.tink:tink-android:1.3.0-rc1'

if (useIntlJsc) {
implementation 'org.webkit:android-jsc-intl:+'
} else {
Expand Down
120 changes: 120 additions & 0 deletions android/app/src/main/java/io/parity/signer/ECCryptoModule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright 2015-2019 Parity Technologies (UK) Ltd.
// This file is part of Parity.

// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.

package io.parity.signer;

import android.os.Build;
import android.util.Base64;
import android.util.Log;
import com.facebook.react.bridge.*;
import com.google.crypto.tink.HybridDecrypt;
import com.google.crypto.tink.HybridEncrypt;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.hybrid.HybridConfig;
import com.google.crypto.tink.hybrid.HybridDecryptFactory;
import com.google.crypto.tink.hybrid.HybridEncryptFactory;
import com.google.crypto.tink.hybrid.HybridKeyTemplates;
import com.google.crypto.tink.integration.android.AndroidKeysetManager;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;

public class ECCryptoModule extends ReactContextBaseJavaModule {
private static final String KEYSTORE_PROVIDER_NAME = "parity.singer.keystore";
private static final String SHARED_PREF_FILE_NAME = "parity.signer.shared.pref";
private static final String ANDORID_KEYSTORE_PREFIX = "android-keystore://";
private static final Map<Integer, String> sizeToName = new HashMap<Integer, String>();
private static final Map<Integer, byte[]> sizeToHead = new HashMap<Integer, byte[]>();

private final boolean isModern = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M;
private KeysetHandle keysetHandle;

public ECCryptoModule(ReactApplicationContext reactContext) {
super(reactContext);
try {
HybridConfig.init();
} catch (GeneralSecurityException ex) {
Log.e("init ECCrypto Module", "ERR", ex);
}
}

private static String getKeyUri(String keyLabel) {
return ANDORID_KEYSTORE_PREFIX + keyLabel;
}

private static String toBase64(byte[] bytes) {
return Base64.encodeToString(bytes, Base64.NO_WRAP);
}

private static byte[] fromBase64(String str) {
return Base64.decode(str, Base64.NO_WRAP);
}

@Override
public String getName() {
return "ECCrypto";
}

@ReactMethod
public void encrypt(ReadableMap map, Promise promise) {
try {
KeysetHandle keysetHandle = getOrGenerateNewKeysetHandle(map.getString("label"));
KeysetHandle publicKeysetHandle = keysetHandle.getPublicKeysetHandle();
String clearTextString = map.getString("data");
byte[] plainText = clearTextString.getBytes("UTF-8");
byte[] contextInfo = fromBase64(SHARED_PREF_FILE_NAME);

HybridEncrypt hybridEncrypt = HybridEncryptFactory.getPrimitive(publicKeysetHandle);
byte[] cipherText = hybridEncrypt.encrypt(plainText, contextInfo);

promise.resolve(toBase64(cipherText));
} catch (Exception e) {
Log.e("ECCrypto", "encryption error", e);
promise.reject("ECCrypto error", e);
return;
}
}

@ReactMethod
public void decrypt(ReadableMap map, Promise promise) {
try {
KeysetHandle keysetHandle = getOrGenerateNewKeysetHandle(map.getString("label"));
String cipherTextString = map.getString("data");
byte[] cipherText = fromBase64(cipherTextString);
byte[] contextInfo = fromBase64(SHARED_PREF_FILE_NAME);

HybridDecrypt hybridDecrypt = HybridDecryptFactory.getPrimitive(keysetHandle);
byte[] clearText = hybridDecrypt.decrypt(cipherText, contextInfo);

promise.resolve(new String(clearText, "UTF-8"));
} catch (Exception e) {
Log.e("ECCrypto", "decrypt error", e);
promise.reject("ECCrypto error", e);
return;
}
}

private KeysetHandle getOrGenerateNewKeysetHandle(String keyUri) throws IOException, GeneralSecurityException {
return new AndroidKeysetManager.Builder()
.withSharedPref(getReactApplicationContext(), KEYSTORE_PROVIDER_NAME, SHARED_PREF_FILE_NAME)
.withKeyTemplate(HybridKeyTemplates.ECIES_P256_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256)
.withMasterKeyUri(getKeyUri(keyUri))
.build()
.getKeysetHandle();
}
}
48 changes: 48 additions & 0 deletions android/app/src/main/java/io/parity/signer/ECCryptoPackage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2015-2019 Parity Technologies (UK) Ltd.
// This file is part of Parity.

// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.

package io.parity.signer;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ECCryptoPackage implements ReactPackage {

@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();

modules.add(new ECCryptoModule(reactContext));

return modules;
}

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}

public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ protected List<ReactPackage> getPackages() {
new NetInfoPackage(),
new RandomBytesPackage(),
new EthkeyBridgePackage(),
new ECCryptoPackage(),
new RNGestureHandlerPackage()
);
}
Expand Down
16 changes: 16 additions & 0 deletions e2e/native.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import testIDs from "./testIDs";

describe('First test', () => {
beforeEach(async () => {
// await device.clearKeychain();
});

it('should pass all the eccrypto function', async () => {
await expect(element(by.id(testIDs.TacScreen.tacView))).toBeVisible();
await element(by.id(testIDs.TacScreen.nativeModuleTestButton)).tap();
await expect(element(by.id(testIDs.NativeTestScreen.nativeTestView))).toBeVisible();
await expect(element(by.id(testIDs.NativeTestScreen.succeedView))).toNotExist();
await element(by.id(testIDs.NativeTestScreen.startButton)).tap();
await expect(element(by.id(testIDs.NativeTestScreen.succeedView))).toExist();
})
});
51 changes: 51 additions & 0 deletions e2e/screens/NativeModuleTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2015-2019 Parity Technologies (UK) Ltd.
// This file is part of Parity.

// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.

import React, {useState} from 'react';
import {View} from 'react-native';
import {encryptWithSecureKeystore, decryptWithSecureKeystore} from "../../src/util/native";
import testIDs from "../testIDs";
import Button from "../../src/components/Button";

const testSeed = '0xf49cd2aa6bda43467abc6aa0a4f37c5b1378146855f80f491e5dd6d053fa4279';
const testPublicAddress = '0x5Cc5dc62be3c95C771C142C2e30358B398265de21111';

export default function NativeModuleTest() {

const [testSucceed, setTestResult] = useState(false);

const generateTestResult = (expectedResult, actualResult) => expectedResult === actualResult ? setTestResult(true) : setTestResult(false);

const testECCryptoModule = async () => {
const encryptedSeed = await encryptWithSecureKeystore(testSeed, testPublicAddress);
const decryptedText = await decryptWithSecureKeystore(encryptedSeed, testPublicAddress);
generateTestResult(testSeed, decryptedText)
};

const startTest = async () => {
try {
await testECCryptoModule();
} catch (e) {
console.log('error is', e);
setTestResult(false)
}
};

return <View testID={testIDs.NativeTestScreen.nativeTestView}>
<Button title="Start Test" onPress={startTest} testID={testIDs.NativeTestScreen.startButton}/>
{testSucceed && <View testID={testIDs.NativeTestScreen.succeedView}/>}
</View>
}
10 changes: 8 additions & 2 deletions e2e/testIDs.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ const testIDs = {
tacView: 'tac_view',
agreeTacButton: 'tac_agree',
agreePrivacyButton: 'tac_privacy',
nativeModuleTestButton: 'tac_native_test',
nextButton: 'tac_next'
},
AccountListScreen: {
accountList: 'accountList',
}
accountList: 'accountList'
},
NativeTestScreen: {
nativeTestView: 'native_test_view',
startButton: 'native_test_start',
succeedView: 'native_test_success'
},
};

export default testIDs;
33 changes: 33 additions & 0 deletions ios/ECCrypto/ECCrypto.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2015-2019 Parity Technologies (UK) Ltd.
// This file is part of Parity.

// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//
// ECCrypto.h
//

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

@interface ECCrypto : NSObject <RCTBridgeModule>

- (NSString * _Nonnull) toPublicIdentifier:(NSString * _Nonnull)keyPairTag;
- (SecKeyRef _Nullable) getPublicKeyRef:(NSString * _Nullable)publicKeyTag errMsg:(NSString *_Nullable*_Nullable)errMsg;
- (SecKeyRef _Nullable) getPrivateKeyRef:(NSString * _Nullable)privateKeyTag errMsg:(NSString *_Nullable*_Nullable)errMsg;
- (SecKeyRef _Nullable) getOrGenerateNewPublicKeyRef:(NSDictionary * _Nonnull) options
errMsg:(NSString *_Nullable*_Nullable)errMsg;
- (NSString * _Nonnull) uuidString;
- (NSData * _Nullable)encrypt:(NSDictionary* _Nonnull)options errMsg:(NSString *_Nullable*_Nullable) errMsg;
- (NSData * _Nullable)decrypt:(NSDictionary* _Nonnull)options errMsg:(NSString *_Nullable*_Nullable) errMsg;
@end
Loading