diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..669a559 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '40 9 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java', 'javascript', 'ruby' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/README.md b/README.md index 7a01070..aaa94e0 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,6 @@ or $ yarn add react-native-secure-key-store ``` -### Mostly automatic installation - -```sh -$ react-native link react-native-secure-key-store -``` - ### Manual installation diff --git a/android/build.gradle b/android/build.gradle index 28daa45..335a1de 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,4 +1,8 @@ +def safeExtGet(prop, fallback) { + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback +} + buildscript { // The Android Gradle plugin is only required when opening the android folder stand-alone. // This avoids unnecessary downloads and potential conflicts when the library is included as a @@ -6,6 +10,10 @@ buildscript { if (project == rootProject) { repositories { google() + mavenCentral() + // JCenter is going read-only repository indefinitely + // Gradle is discouraging jcenter to avoid to avoid build issues - pipeline + // ref: https://blog.gradle.org/jcenter-shutdown jcenter() } @@ -18,12 +26,12 @@ buildscript { apply plugin: 'com.android.library' android { - compileSdkVersion 28 - buildToolsVersion "28.0.3" + compileSdkVersion safeExtGet('compileSdkVersion', 28) + buildToolsVersion safeExtGet('buildToolsVersion', '28.0.3') defaultConfig { - minSdkVersion 16 - targetSdkVersion 28 + minSdkVersion safeExtGet('minSdkVersion', 23) + targetSdkVersion safeExtGet('targetSdkVersion', 28) versionCode 1 versionName "1.0" } @@ -34,10 +42,11 @@ android { repositories { google() - jcenter() mavenCentral() + jcenter() } dependencies { implementation 'com.facebook.react:react-native:+' + implementation "androidx.security:security-crypto:1.0.0-rc03" } diff --git a/android/src/main/java/com/reactlibrary/securekeystore/RNSecureKeyStoreModule.java b/android/src/main/java/com/reactlibrary/securekeystore/RNSecureKeyStoreModule.java index b9daade..e20d488 100644 --- a/android/src/main/java/com/reactlibrary/securekeystore/RNSecureKeyStoreModule.java +++ b/android/src/main/java/com/reactlibrary/securekeystore/RNSecureKeyStoreModule.java @@ -6,38 +6,23 @@ package com.reactlibrary.securekeystore; -import android.content.Context; +import android.content.SharedPreferences; import android.os.Build; -import android.security.KeyPairGeneratorSpec; import android.util.Log; -import java.util.Locale; + +import androidx.annotation.Nullable; +import androidx.security.crypto.EncryptedSharedPreferences; +import androidx.security.crypto.MasterKeys; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.math.BigInteger; -import java.security.GeneralSecurityException; -import java.security.KeyPairGenerator; -import java.security.KeyStore; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.util.Calendar; -import androidx.annotation.Nullable; import com.facebook.react.bridge.ReadableMap; -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import javax.security.auth.x500.X500Principal; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.GeneralSecurityException; public class RNSecureKeyStoreModule extends ReactContextBaseJavaModule { @@ -56,7 +41,7 @@ public class RNSecureKeyStoreModule extends ReactContextBaseJavaModule { @ReactMethod public void set(String alias, String input, @Nullable ReadableMap options, Promise promise) { try { - setCipherText(alias, input); + getSecureSharedPreferences().edit().putString(alias, input).commit(); promise.resolve("stored ciphertext in app storage"); } catch (Exception e) { e.printStackTrace(); @@ -65,89 +50,26 @@ public class RNSecureKeyStoreModule extends ReactContextBaseJavaModule { } } - private PublicKey getOrCreatePublicKey(String alias) throws GeneralSecurityException, IOException { - Locale currentLocale = Locale.getDefault(); - Locale.setDefault(Locale.ENGLISH); - KeyStore keyStore = KeyStore.getInstance(getKeyStore()); - keyStore.load(null); - - if (!keyStore.containsAlias(alias) || keyStore.getCertificate(alias) == null) { - Log.i(Constants.TAG, "no existing asymmetric keys for alias"); - - Calendar start = Calendar.getInstance(); - Calendar end = Calendar.getInstance(); - end.add(Calendar.YEAR, 50); - KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(getContext()) - .setAlias(alias) - .setSubject(new X500Principal("CN=" + alias)) - .setSerialNumber(BigInteger.ONE) - .setStartDate(start.getTime()) - .setEndDate(end.getTime()) - .build(); - - KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", getKeyStore()); - generator.initialize(spec); - generator.generateKeyPair(); - - Locale.setDefault(currentLocale); - Log.i(Constants.TAG, "created new asymmetric keys for alias"); - } - - return keyStore.getCertificate(alias).getPublicKey(); - } - - private byte[] encryptRsaPlainText(PublicKey publicKey, byte[] plainTextBytes) throws GeneralSecurityException, IOException { - Cipher cipher = Cipher.getInstance(Constants.RSA_ALGORITHM); - cipher.init(Cipher.ENCRYPT_MODE, publicKey); - return encryptCipherText(cipher, plainTextBytes); - } - - private byte[] encryptAesPlainText(SecretKey secretKey, String plainText) throws GeneralSecurityException, IOException { - Cipher cipher = Cipher.getInstance(Constants.AES_ALGORITHM); - cipher.init(Cipher.ENCRYPT_MODE, secretKey); - return encryptCipherText(cipher, plainText); - } - - private byte[] encryptCipherText(Cipher cipher, String plainText) throws GeneralSecurityException, IOException { - return encryptCipherText(cipher, plainText.getBytes("UTF-8")); - } - - private byte[] encryptCipherText(Cipher cipher, byte[] plainTextBytes) throws GeneralSecurityException, IOException { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher); - cipherOutputStream.write(plainTextBytes); - cipherOutputStream.close(); - return outputStream.toByteArray(); - } - - private SecretKey getOrCreateSecretKey(String alias) throws GeneralSecurityException, IOException { - try { - return getSymmetricKey(alias); - } catch (FileNotFoundException fnfe) { - Log.i(Constants.TAG, "no existing symmetric key for alias"); - - KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); - //32bytes / 256bits AES key - keyGenerator.init(256); - SecretKey secretKey = keyGenerator.generateKey(); - PublicKey publicKey = getOrCreatePublicKey(alias); - Storage.writeValues(getContext(), Constants.SKS_KEY_FILENAME + alias, - encryptRsaPlainText(publicKey, secretKey.getEncoded())); - - Log.i(Constants.TAG, "created new symmetric keys for alias"); - return secretKey; - } - } - - private void setCipherText(String alias, String input) throws GeneralSecurityException, IOException { - Storage.writeValues(getContext(), Constants.SKS_DATA_FILENAME + alias, - encryptAesPlainText(getOrCreateSecretKey(alias), input)); + private SharedPreferences getSecureSharedPreferences() throws GeneralSecurityException, IOException { + return EncryptedSharedPreferences.create( + "secret_shared_prefs", + MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC), + reactContext, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ); } @ReactMethod public void get(String alias, Promise promise) { try { - promise.resolve(getPlainText(alias)); + String value = getSecureSharedPreferences().getString(alias, null); + if (value == null) { + //throw FileNotFoundException to keep match old behaviour when a value is missing + throw new FileNotFoundException(alias + " has not been set"); + } else { + promise.resolve(value); + } } catch (FileNotFoundException fnfe) { fnfe.printStackTrace(); promise.reject("404", "{\"code\":404,\"api-level\":" + Build.VERSION.SDK_INT + ",\"message\":" + fnfe.getMessage() + "}", fnfe); @@ -158,73 +80,15 @@ public class RNSecureKeyStoreModule extends ReactContextBaseJavaModule { } } - private PrivateKey getPrivateKey(String alias) throws GeneralSecurityException, IOException { - KeyStore keyStore = KeyStore.getInstance(getKeyStore()); - keyStore.load(null); - return (PrivateKey) keyStore.getKey(alias, null); - } - - private byte[] decryptRsaCipherText(PrivateKey privateKey, byte[] cipherTextBytes) throws GeneralSecurityException, IOException { - Cipher cipher = Cipher.getInstance(Constants.RSA_ALGORITHM); - cipher.init(Cipher.DECRYPT_MODE, privateKey); - return decryptCipherText(cipher, cipherTextBytes); - } - - private byte[] decryptAesCipherText(SecretKey secretKey, byte[] cipherTextBytes) throws GeneralSecurityException, IOException { - Cipher cipher = Cipher.getInstance(Constants.AES_ALGORITHM); - cipher.init(Cipher.DECRYPT_MODE, secretKey); - return decryptCipherText(cipher, cipherTextBytes); - } - - private byte[] decryptCipherText(Cipher cipher, byte[] cipherTextBytes) throws IOException { - ByteArrayInputStream bais = new ByteArrayInputStream(cipherTextBytes); - CipherInputStream cipherInputStream = new CipherInputStream(bais, cipher); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buffer = new byte[256]; - int bytesRead = cipherInputStream.read(buffer); - while (bytesRead != -1) { - baos.write(buffer, 0, bytesRead); - bytesRead = cipherInputStream.read(buffer); - } - return baos.toByteArray(); - } - - private SecretKey getSymmetricKey(String alias) throws GeneralSecurityException, IOException { - byte[] cipherTextBytes = Storage.readValues(getContext(), Constants.SKS_KEY_FILENAME + alias); - return new SecretKeySpec(decryptRsaCipherText(getPrivateKey(alias), cipherTextBytes), Constants.AES_ALGORITHM); - } - - private String getPlainText(String alias) throws GeneralSecurityException, IOException { - SecretKey secretKey = getSymmetricKey(alias); - byte[] cipherTextBytes = Storage.readValues(getContext(), Constants.SKS_DATA_FILENAME + alias); - return new String(decryptAesCipherText(secretKey, cipherTextBytes), "UTF-8"); - } - @ReactMethod public void remove(String alias, Promise promise) { - Storage.resetValues(getContext(), new String[] { - Constants.SKS_DATA_FILENAME + alias, - Constants.SKS_KEY_FILENAME + alias, - }); - promise.resolve("cleared alias"); - } - - private Context getContext() { - return getReactApplicationContext(); - } - - private String getKeyStore() { try { - KeyStore.getInstance(Constants.KEYSTORE_PROVIDER_1); - return Constants.KEYSTORE_PROVIDER_1; - } catch (Exception err) { - try { - KeyStore.getInstance(Constants.KEYSTORE_PROVIDER_2); - return Constants.KEYSTORE_PROVIDER_2; - } catch (Exception e) { - return Constants.KEYSTORE_PROVIDER_3; - } + getSecureSharedPreferences().edit().remove(alias).commit(); + promise.resolve("cleared alias"); + } catch (Exception e) { + e.printStackTrace(); + Log.e(Constants.TAG, "Exception: " + e.getMessage()); + promise.reject("{\"code\":6,\"api-level\":" + Build.VERSION.SDK_INT + ",\"message\":" + e.getMessage() + "}"); } } - } diff --git a/android/src/main/java/com/reactlibrary/securekeystore/Storage.java b/android/src/main/java/com/reactlibrary/securekeystore/Storage.java deleted file mode 100644 index 62184fe..0000000 --- a/android/src/main/java/com/reactlibrary/securekeystore/Storage.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.reactlibrary.securekeystore; - -// Helper function for storing keys to internal storage. - -import android.content.Context; - -import java.io.ByteArrayOutputStream; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; - -public final class Storage { - - public static void writeValues(Context context, String filename, byte[] bytes) throws IOException { - FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE); - fos.write(bytes); - fos.close(); - } - - public static byte[] readValues(Context context, String filename) throws IOException { - FileInputStream fis = context.openFileInput(filename); - ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); - byte[] buffer = new byte[1024]; - int bytesRead = fis.read(buffer); - while(bytesRead != -1) { - baos.write(buffer, 0, bytesRead); - bytesRead = fis.read(buffer); - } - return baos.toByteArray(); - } - - public static void resetValues(Context context, String[] filenames) { - for(String filename : filenames) { - context.deleteFile(filename); - } - } - -} \ No newline at end of file diff --git a/example/App.js b/example/App.js index 92ae662..5821706 100644 --- a/example/App.js +++ b/example/App.js @@ -8,68 +8,81 @@ */ import React, {Component} from 'react'; -import {Platform, StyleSheet, Text, View} from 'react-native'; +import {Platform, StyleSheet, Button, Text, TextInput, View} from 'react-native'; import RNSecureKeyStore, {ACCESSIBLE} from "react-native-secure-key-store"; -const instructions = Platform.select({ - ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu', - android: - 'Double tap R on your keyboard to reload,\n' + - 'Shake or press menu button for dev menu', -}); - type Props = {}; export default class App extends Component { + state = { + alias: 'hello', + value: 'world' + }; + + getValue() { + RNSecureKeyStore.get(this.state.alias) + .then((value) => { + this.setState({ + value, + }); + }) + .catch(console.error); + } + + setValue() { + RNSecureKeyStore.set(this.state.alias, this.state.value, {}) + .then(() => this.getValue()) + .catch(console.error); + } + + removeValue() { + RNSecureKeyStore.remove(this.state.alias) + .then(() => this.getValue()) + .catch(console.error); + } + render() { - - RNSecureKeyStore.set("key1", "value1", {accessible: ACCESSIBLE.ALWAYS_THIS_DEVICE_ONLY}) - .then((res) => { - console.log(res); - }, (err) => { - console.log(err); - }); - - RNSecureKeyStore.set("key2", "value2", {accessible: ACCESSIBLE.ALWAYS_THIS_DEVICE_ONLY}) - .then((res) => { - console.log(res); - }, (err) => { - console.log(err); - }); - - RNSecureKeyStore.get("key1") - .then((res) => { - console.log(res); - }, (err) => { - console.log(err); - }); - - RNSecureKeyStore.get("key2") - .then((res) => { - console.log(res); - }, (err) => { - console.log(err); - }); - - RNSecureKeyStore.remove("key1") - .then((res) => { - console.log(res); - }, (err) => { - console.log(err); - }); - - RNSecureKeyStore.remove("key2") - .then((res) => { - console.log(res); - }, (err) => { - console.log(err); - }); - return ( - Welcome to React Native! - To get started, edit App.js - {instructions} + + Alias: + this.setState({alias})} + value={this.state.alias} + /> + + + + Value: + this.setState({value})} + value={this.state.value} + /> + + + + +