Merge branch 'pradeep1991singh:master' into master
Some checks failed
CodeQL / Analyze (java) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (ruby) (push) Has been cancelled

This commit is contained in:
Marcos Rodriguez Vélez 2023-02-28 21:30:50 -04:00 committed by GitHub
commit 2076b4849e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 250 additions and 314 deletions

72
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -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

View File

@ -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

View File

@ -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"
}

View File

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

View File

@ -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);
}
}
}

View File

@ -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<Props> {
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 (
<View style={styles.container}>
<Text style={styles.welcome}>Welcome to React Native!</Text>
<Text style={styles.instructions}>To get started, edit App.js</Text>
<Text style={styles.instructions}>{instructions}</Text>
<View style={styles.row}>
<Text>Alias:</Text>
<TextInput
style={styles.textInput}
onChangeText={alias => this.setState({alias})}
value={this.state.alias}
/>
</View>
<View style={styles.row}>
<Text>Value:</Text>
<TextInput
style={styles.textInput}
onChangeText={value => this.setState({value})}
value={this.state.value}
/>
</View>
<View style={styles.row}>
<View style={styles.button}>
<Button
onPress={() => this.getValue()}
title='Get'
/>
</View>
<View style={styles.button}>
<Button
onPress={() => this.setValue()}
title='Set'
/>
</View>
<View style={styles.button}>
<Button
onPress={() => this.removeValue()}
title='Remove'
/>
</View>
</View>
</View>
);
}
@ -82,14 +95,20 @@ const styles = StyleSheet.create({
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: 20
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
textInput: {
height: 40,
width: 200,
borderColor: 'gray',
borderWidth: 1,
marginLeft: 10
},
button: {
marginLeft: 10
}
});

View File

@ -3,7 +3,7 @@
buildscript {
ext {
buildToolsVersion = "28.0.2"
minSdkVersion = 16
minSdkVersion = 23
compileSdkVersion = 28
targetSdkVersion = 27
supportLibVersion = "28.0.0"

View File

@ -16,3 +16,5 @@
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.useAndroidX=true
android.enableJetifier=true

12
example/metro.config.js Normal file
View File

@ -0,0 +1,12 @@
const path = require("path");
const packagePath = path.resolve(__dirname, "../");
module.exports = {
resolver: {
extraNodeModules: {
"react-native-secure-key-store": packagePath,
},
},
watchFolders: [packagePath],
};

View File

@ -9,7 +9,7 @@
"dependencies": {
"react": "16.6.3",
"react-native": "0.58.1",
"react-native-secure-key-store": "../../react-native-secure-key-store"
"react-native-secure-key-store": "file:.."
},
"devDependencies": {
"babel-core": "^7.0.0-bridge.0",

View File

@ -3389,9 +3389,9 @@ lodash.throttle@^4.1.1:
integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.6.1:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
version "4.17.19"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
version "1.4.0"

18
index.d.ts vendored
View File

@ -1,19 +1,19 @@
declare module 'react-native-secure-key-store' {
export const ACCESSIBLE = {
AFTER_FIRST_UNLOCK: 'AccessibleAfterFirstUnlock',
AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY:
export enum ACCESSIBLE {
AFTER_FIRST_UNLOCK = 'AccessibleAfterFirstUnlock',
AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY =
'AccessibleAfterFirstUnlockThisDeviceOnly',
ALWAYS: 'AccessibleAlways',
ALWAYS_THIS_DEVICE_ONLY: 'AccessibleAlwaysThisDeviceOnly',
WHEN_PASSCODE_SET_THIS_DEVICE_ONLY:
ALWAYS = 'AccessibleAlways',
ALWAYS_THIS_DEVICE_ONLY = 'AccessibleAlwaysThisDeviceOnly',
WHEN_PASSCODE_SET_THIS_DEVICE_ONLY =
'AccessibleWhenPasscodeSetThisDeviceOnly',
WHEN_UNLOCKED: 'AccessibleWhenUnlocked',
WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'AccessibleWhenUnlockedThisDeviceOnly',
WHEN_UNLOCKED = 'AccessibleWhenUnlocked',
WHEN_UNLOCKED_THIS_DEVICE_ONLY = 'AccessibleWhenUnlockedThisDeviceOnly',
}
interface RNSecureKeyStore {
get: (key: string) => Promise<any>
set: (key: string, value: string, accessible?: ACCESSIBLE) => Promise<any>
set: (key: string, value: string, options?: { accessible?: ACCESSIBLE }) => Promise<any>
remove: (key: string) => Promise<any>
setResetOnAppUninstallTo: (enabled: boolean) => boolean
}

View File

@ -1,22 +0,0 @@
Pod::Spec.new do |s|
s.name = "RNSecureKeyStore"
s.version = "1.0.0"
s.summary = "A package for secure storage on android and ios"
s.description = "A package for secure storage on android and ios. Stores using the keystore on Android devices, and the keychain on iOS devices."
s.homepage = "https://github.com/pradeep1991singh/react-native-secure-key-store#readme"
s.license = "MIT"
# s.license = { :type => "MIT", :file => "FILE_LICENSE" }
s.author = { "author" => "pradeep1991singh" }
s.platform = :ios, "7.0"
s.source = { :git => "https://github.com/pradeep1991singh/react-native-secure-key-store", :tag => "master" }
s.source_files = "**/*.{h,m}"
s.requires_arc = true
s.dependency "React"
#s.dependency "others"
end

View File

@ -1,6 +1,6 @@
{
"name": "react-native-secure-key-store",
"version": "2.0.7",
"version": "2.0.10",
"description": "React Native Library for securely storing keys to iOS and Android devices in KeyChain and KeyStore respectively.",
"main": "index.js",
"publishConfig": {

View File

@ -0,0 +1,24 @@
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
Pod::Spec.new do |s|
s.name = package['name']
s.version = package['version']
s.summary = package['description']
s.description = package['description']
s.homepage = package['homepage']
s.license = package['license']
s.authors = package['author']
s.platform = :ios, "7.0"
s.source = { :git => "https://github.com/pradeep1991singh/react-native-secure-key-store", :tag => "master" }
s.source_files = "ios/**/*.{h,m}"
s.requires_arc = true
s.dependency "React-Core"
#s.dependency "others"
end