Skip to content

Commit

Permalink
Merge branch 'pradeep1991singh:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
marcosrdz authored Mar 1, 2023
2 parents 63ab38c + 80c35ae commit 2076b48
Show file tree
Hide file tree
Showing 15 changed files with 244 additions and 308 deletions.
72 changes: 72 additions & 0 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 0 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
19 changes: 14 additions & 5 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@

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
// module dependency in an application project.
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()
}

Expand All @@ -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"
}
Expand All @@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 com.facebook.react.bridge.ReadableMap;

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;

public class RNSecureKeyStoreModule extends ReactContextBaseJavaModule {

Expand All @@ -56,7 +41,7 @@ public String getName() {
@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();
Expand All @@ -65,89 +50,26 @@ public void set(String alias, String input, @Nullable ReadableMap options, Promi
}
}

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);
Expand All @@ -158,73 +80,15 @@ public void get(String alias, Promise promise) {
}
}

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() + "}");
}
}

}
Loading

0 comments on commit 2076b48

Please sign in to comment.