react-native-secreton 2.3.0 → 2.3.1

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 CHANGED
@@ -46,7 +46,7 @@ $ react-native link react-native-secreton
46
46
 
47
47
  ### Using Consul
48
48
  ```bash
49
- export ENV_SECRET_KEY=my-secret
49
+ export ENV_SECRET_KEY=secret-key-32-bytes
50
50
  export FETCH_ENV=consul
51
51
  export CONSUL_ADDR=http://consul.mycompany.com:8500
52
52
  export CONSUL_PATH=mobile/myapp
@@ -57,7 +57,7 @@ rn-secreton-cli .env
57
57
 
58
58
  ### Using Vault
59
59
  ```bash
60
- export ENV_SECRET_KEY=my-secret
60
+ export ENV_SECRET_KEY=secret-key-32-bytes
61
61
  export FETCH_ENV=vault
62
62
  export VAULT_ADDR=http://vault.mycompany.com:8200
63
63
  export VAULT_PATH=secret/data/mobile/myapp
@@ -18,9 +18,10 @@ buildscript {
18
18
 
19
19
  apply plugin: "com.android.library"
20
20
  apply plugin: "kotlin-android"
21
-
22
21
  apply plugin: "com.facebook.react"
23
22
 
23
+ apply from: "secreton.gradle"
24
+
24
25
  def getExtOrIntegerDefault(name) {
25
26
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["Secreton_" + name]).toInteger()
26
27
  }
@@ -33,6 +34,7 @@ android {
33
34
  defaultConfig {
34
35
  minSdkVersion getExtOrIntegerDefault("minSdkVersion")
35
36
  targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
37
+ buildConfigField "String", "ENV_SECRET_KEY", "\"${getSecretKeyFromEnv(project)}\""
36
38
  }
37
39
 
38
40
  buildFeatures {
@@ -38,18 +38,30 @@ ext.loadEnvFile = { File file ->
38
38
  return map
39
39
  }
40
40
 
41
+ ext.getEnvFile = { appProject ->
42
+ def androidDir = appProject.rootProject.projectDir
43
+ def workspaceRoot = findWorkspaceRoot(androidDir)
44
+
45
+ def envFileName = appProject.findProperty("ENVFILE") ?: System.getenv("ENVFILE") ?: ".env"
46
+ def envFile = new File(workspaceRoot, envFileName)
47
+
48
+ return envFile
49
+ }
50
+
51
+ ext.getSecretKeyFromEnv = { appProject ->
52
+ def envFile = getEnvFile(appProject)
53
+ def envMap = loadEnvFile(envFile)
54
+
55
+ return envMap["ENV_SECRET_KEY"] ?: ""
56
+ }
57
+
41
58
  ext.ensureLocalEnv = { appProject ->
42
59
  if (appProject.ext.has("localEnv")) return
43
60
 
44
61
  appProject.ext.localEnv = [:]
45
62
  appProject.ext.env = [:]
46
-
47
- def androidDir = appProject.rootProject.projectDir
48
- def workspaceRoot = findWorkspaceRoot(androidDir)
49
- def rootDir = workspaceRoot
50
-
51
- def envFileName = appProject.findProperty("ENVFILE") ?: System.getenv("ENVFILE") ?: ".env"
52
- def envFile = new File(rootDir, envFileName)
63
+
64
+ def envFile = getEnvFile(appProject)
53
65
 
54
66
  if (envFile.exists()) {
55
67
  appProject.ext.localEnv.putAll(loadEnvFile(envFile))
@@ -85,15 +97,8 @@ ext.decryptValue = { String encrypted, String secretKey ->
85
97
 
86
98
  ext.loadAndDecryptEnv = { appProject ->
87
99
  ensureLocalEnv(appProject)
88
-
89
- def androidDir = appProject.rootProject.projectDir
90
- def rootDir = findWorkspaceRoot(androidDir)
91
-
92
- def envFileName =
93
- appProject.findProperty("ENVFILE")
94
- ?: System.getenv("ENVFILE")
95
- ?: ".env"
96
- def envFile = new File(rootDir, envFileName)
100
+
101
+ def envFile = getEnvFile(appProject)
97
102
  if (!envFile.exists()) return
98
103
 
99
104
  def secretKey = getSecretKey(appProject)
@@ -102,7 +107,7 @@ ext.loadAndDecryptEnv = { appProject ->
102
107
  def envMap = loadEnvFile(envFile)
103
108
  envMap.each { k, v ->
104
109
  def value = v
105
- if (v instanceof String && v.startsWith("secreton:")) {
110
+ if (v instanceof String && v.startsWith("Secreton:")) {
106
111
  value = decryptValue(v.substring(9), secretKey)
107
112
  }
108
113
  if (value == null) {
@@ -2,8 +2,8 @@ package com.secreton
2
2
 
3
3
  import com.facebook.react.bridge.ReactApplicationContext
4
4
  import com.facebook.react.module.annotations.ReactModule
5
-
6
5
  import android.util.Base64
6
+ import java.security.SecureRandom
7
7
  import javax.crypto.Cipher
8
8
  import javax.crypto.SecretKeyFactory
9
9
  import javax.crypto.spec.IvParameterSpec
@@ -11,46 +11,87 @@ import javax.crypto.spec.PBEKeySpec
11
11
  import javax.crypto.spec.SecretKeySpec
12
12
 
13
13
  @ReactModule(name = SecretonModule.NAME)
14
- class SecretonModule(reactContext: ReactApplicationContext) :
15
- NativeSecretonSpec(reactContext) {
14
+ class SecretonModule(
15
+ reactContext: ReactApplicationContext
16
+ ) : NativeSecretonSpec(reactContext) {
16
17
 
17
18
  companion object {
18
19
  const val NAME = "Secreton"
20
+ private const val PREFIX = "$NAME:"
21
+ private const val ITERATIONS = 100_000
19
22
  }
20
23
 
21
- override fun getName(): String {
22
- return NAME
24
+ override fun getName() = NAME
25
+
26
+ override fun encrypt(value: String): String {
27
+ val encrypted = encryptOpenSSL(value, getSecretKey())
28
+ return PREFIX + encrypted
23
29
  }
24
30
 
25
- override fun encrypt(value: String, key: String): String {
26
- val salt = ByteArray(16)
27
- val iv = ByteArray(16)
31
+ override fun decrypt(value: String): String {
32
+ if (!value.startsWith(PREFIX)) return value
33
+ return decryptOpenSSL(value.removePrefix(PREFIX), getSecretKey())
34
+ }
35
+
36
+ private fun getSecretKey(): String {
37
+ return BuildConfig.ENV_SECRET_KEY
38
+ }
39
+
40
+ private fun encryptOpenSSL(value: String, key: String): String {
41
+ val salt = ByteArray(8)
42
+ SecureRandom().nextBytes(salt)
28
43
 
29
44
  val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
30
- val spec = PBEKeySpec(key.toCharArray(), salt, 100000, 256)
31
- val tmp = factory.generateSecret(spec)
32
- val secretKey = SecretKeySpec(tmp.encoded, "AES")
45
+ val spec = PBEKeySpec(
46
+ key.toCharArray(),
47
+ salt,
48
+ ITERATIONS,
49
+ (32 + 16) * 8 // key + iv in bits
50
+ )
51
+
52
+ val keyIv = factory.generateSecret(spec).encoded
53
+ val aesKey = SecretKeySpec(keyIv.copyOfRange(0, 32), "AES")
54
+ val iv = IvParameterSpec(keyIv.copyOfRange(32, 48))
33
55
 
34
56
  val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
35
- cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
57
+ cipher.init(Cipher.ENCRYPT_MODE, aesKey, iv)
36
58
 
37
- val encrypted = cipher.doFinal(value.toByteArray())
38
- return Base64.encodeToString(encrypted, Base64.NO_WRAP)
59
+ val encrypted = cipher.doFinal(value.toByteArray(Charsets.UTF_8))
60
+
61
+ val output = ByteArray(16 + encrypted.size)
62
+ System.arraycopy("Salted__".toByteArray(), 0, output, 0, 8)
63
+ System.arraycopy(salt, 0, output, 8, 8)
64
+ System.arraycopy(encrypted, 0, output, 16, encrypted.size)
65
+
66
+ return Base64.encodeToString(output, Base64.NO_WRAP)
39
67
  }
40
68
 
41
- override fun decrypt(encrypted: String, key: String): String {
42
- val salt = ByteArray(16)
43
- val iv = ByteArray(16)
69
+ private fun decryptOpenSSL(encrypted: String, key: String): String {
70
+ val decoded = Base64.decode(encrypted, Base64.NO_WRAP)
71
+
72
+ val magic = String(decoded.copyOfRange(0, 8))
73
+ if (magic != "Salted__") {
74
+ throw IllegalArgumentException("Invalid OpenSSL salt header")
75
+ }
76
+
77
+ val salt = decoded.copyOfRange(8, 16)
78
+ val cipherText = decoded.copyOfRange(16, decoded.size)
44
79
 
45
80
  val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
46
- val spec = PBEKeySpec(key.toCharArray(), salt, 100000, 256)
47
- val tmp = factory.generateSecret(spec)
48
- val secretKey = SecretKeySpec(tmp.encoded, "AES")
81
+ val spec = PBEKeySpec(
82
+ key.toCharArray(),
83
+ salt,
84
+ ITERATIONS,
85
+ (32 + 16) * 8
86
+ )
87
+
88
+ val keyIv = factory.generateSecret(spec).encoded
89
+ val aesKey = SecretKeySpec(keyIv.copyOfRange(0, 32), "AES")
90
+ val iv = IvParameterSpec(keyIv.copyOfRange(32, 48))
49
91
 
50
92
  val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
51
- cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
93
+ cipher.init(Cipher.DECRYPT_MODE, aesKey, iv)
52
94
 
53
- val decoded = Base64.decode(encrypted, Base64.NO_WRAP)
54
- return String(cipher.doFinal(decoded))
95
+ return String(cipher.doFinal(cipherText), Charsets.UTF_8)
55
96
  }
56
97
  }
@@ -64,7 +64,7 @@ export async function generateEnv(options) {
64
64
  }
65
65
  const newLines = entries
66
66
  .filter(({ key }) => !existingKeys.has(key))
67
- .map(({ key, value }) => `${key}=secreton:${encrypt(value, secretKey)}`);
67
+ .map(({ key, value }) => `${key}=Secreton:${encrypt(value, secretKey)}`);
68
68
  if (newLines.length === 0) {
69
69
  console.log('ℹ️ [Node] Secreton no new env keys to add');
70
70
  return;
package/ios/Secreton.mm CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  @implementation Secreton
6
6
 
7
+ static NSString * const kPrefix = @"Secreton:";
8
+
7
9
  + (NSString *)moduleName
8
10
  {
9
11
  return @"Secreton";
@@ -11,14 +13,30 @@
11
13
 
12
14
  #pragma mark - TurboModule methods
13
15
 
14
- - (NSString *)encrypt:(NSString *)value key:(NSString *)key
16
+ - (NSString *)encrypt:(NSString *)value
15
17
  {
16
- return [self aes:kCCEncrypt value:value key:key];
18
+ NSString *key = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"ENV_SECRET_KEY"];
19
+ if (!key || key.length == 0) {
20
+ return nil;
21
+ }
22
+
23
+ NSString *encrypted = [self aes:kCCEncrypt value:value key:key];
24
+ return [kPrefix stringByAppendingString:encrypted];
17
25
  }
18
26
 
19
- - (NSString *)decrypt:(NSString *)value key:(NSString *)key
27
+ - (NSString *)decrypt:(NSString *)value
20
28
  {
21
- return [self aes:kCCDecrypt value:value key:key];
29
+ if (![value hasPrefix:kPrefix]) {
30
+ return value;
31
+ }
32
+
33
+ NSString *key = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"ENV_SECRET_KEY"];
34
+ if (!key || key.length == 0) {
35
+ return nil;
36
+ }
37
+
38
+ NSString *raw = [value substringFromIndex:kPrefix.length];
39
+ return [self aes:kCCDecrypt value:raw key:key];
22
40
  }
23
41
 
24
42
  #pragma mark - AES core
@@ -27,13 +45,19 @@
27
45
  value:(NSString *)value
28
46
  key:(NSString *)key
29
47
  {
30
- NSData *data = [value dataUsingEncoding:NSUTF8StringEncoding];
48
+ NSData *data = nil;
49
+
50
+ if (operation == kCCEncrypt) {
51
+ data = [value dataUsingEncoding:NSUTF8StringEncoding];
52
+ } else {
53
+ data = [[NSData alloc] initWithBase64EncodedString:value options:0];
54
+ if (!data) return nil;
55
+ }
31
56
 
32
- // FIX: key 32 byte (AES-256)
57
+ // Key 32 byte (AES-256)
33
58
  NSMutableData *keyData = [NSMutableData dataWithLength:kCCKeySizeAES256];
34
59
  NSData *rawKey = [key dataUsingEncoding:NSUTF8StringEncoding];
35
- NSUInteger copyLen = MIN(rawKey.length, kCCKeySizeAES256);
36
- memcpy(keyData.mutableBytes, rawKey.bytes, copyLen);
60
+ memcpy(keyData.mutableBytes, rawKey.bytes, MIN(rawKey.length, kCCKeySizeAES256));
37
61
 
38
62
  size_t outLength = 0;
39
63
  NSMutableData *outData = [NSMutableData dataWithLength:data.length + kCCBlockSizeAES128];
@@ -44,7 +68,7 @@
44
68
  kCCOptionPKCS7Padding,
45
69
  keyData.bytes,
46
70
  kCCKeySizeAES256,
47
- NULL, // IV = zero (compat Swift)
71
+ NULL, // IV zero
48
72
  data.bytes,
49
73
  data.length,
50
74
  outData.mutableBytes,
@@ -71,7 +71,7 @@ export async function generateEnv(options) {
71
71
  }) => !existingKeys.has(key)).map(({
72
72
  key,
73
73
  value
74
- }) => `${key}=secreton:${encrypt(value, secretKey)}`);
74
+ }) => `${key}=Secreton:${encrypt(value, secretKey)}`);
75
75
  if (newLines.length === 0) {
76
76
  console.log('ℹ️ [Node] Secreton no new env keys to add');
77
77
  return;
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
 
3
3
  import Secreton from "./NativeSecreton.js";
4
- export function encrypt(value, key) {
5
- return Secreton.encrypt(value, key);
4
+ export function encrypt(value) {
5
+ return Secreton.encrypt(value);
6
6
  }
7
- export function decrypt(encrypted, key) {
8
- return Secreton.decrypt(encrypted, key);
7
+ export function decrypt(encrypted) {
8
+ return Secreton.decrypt(encrypted);
9
9
  }
10
10
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["Secreton","encrypt","value","key","decrypt","encrypted"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,OAAOA,QAAQ,MAAM,qBAAkB;AAEvC,OAAO,SAASC,OAAOA,CAACC,KAAa,EAAEC,GAAW,EAAU;EAC1D,OAAOH,QAAQ,CAACC,OAAO,CAACC,KAAK,EAAEC,GAAG,CAAC;AACrC;AAEA,OAAO,SAASC,OAAOA,CAACC,SAAiB,EAAEF,GAAW,EAAU;EAC9D,OAAOH,QAAQ,CAACI,OAAO,CAACC,SAAS,EAAEF,GAAG,CAAC;AACzC","ignoreList":[]}
1
+ {"version":3,"names":["Secreton","encrypt","value","decrypt","encrypted"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,OAAOA,QAAQ,MAAM,qBAAkB;AAEvC,OAAO,SAASC,OAAOA,CAACC,KAAa,EAAU;EAC7C,OAAOF,QAAQ,CAACC,OAAO,CAACC,KAAK,CAAC;AAChC;AAEA,OAAO,SAASC,OAAOA,CAACC,SAAiB,EAAU;EACjD,OAAOJ,QAAQ,CAACG,OAAO,CAACC,SAAS,CAAC;AACpC","ignoreList":[]}
@@ -1,7 +1,7 @@
1
1
  import { type TurboModule } from 'react-native';
2
2
  export interface Spec extends TurboModule {
3
- encrypt(value: string, key: string): string;
4
- decrypt(encrypted: string, key: string): string;
3
+ encrypt(value: string): string;
4
+ decrypt(encrypted: string): string;
5
5
  }
6
6
  declare const _default: Spec;
7
7
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"NativeSecreton.d.ts","sourceRoot":"","sources":["../../../src/NativeSecreton.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAErE,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5C,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACjD;;AAED,wBAAkE"}
1
+ {"version":3,"file":"NativeSecreton.d.ts","sourceRoot":"","sources":["../../../src/NativeSecreton.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAErE,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;CACpC;;AAED,wBAAkE"}
@@ -1,3 +1,3 @@
1
- export declare function encrypt(value: string, key: string): string;
2
- export declare function decrypt(encrypted: string, key: string): string;
1
+ export declare function encrypt(value: string): string;
2
+ export declare function decrypt(encrypted: string): string;
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAEA,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE9D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAEA,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEjD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-secreton",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "Config secret variables for React Native apps",
5
5
  "keywords": [
6
6
  "react-native",
@@ -1,8 +1,8 @@
1
1
  import { TurboModuleRegistry, type TurboModule } from 'react-native';
2
2
 
3
3
  export interface Spec extends TurboModule {
4
- encrypt(value: string, key: string): string;
5
- decrypt(encrypted: string, key: string): string;
4
+ encrypt(value: string): string;
5
+ decrypt(encrypted: string): string;
6
6
  }
7
7
 
8
8
  export default TurboModuleRegistry.getEnforcing<Spec>('Secreton');
@@ -118,7 +118,7 @@ export async function generateEnv(options: GenerateEnvOptions) {
118
118
 
119
119
  const newLines = entries
120
120
  .filter(({ key }) => !existingKeys.has(key))
121
- .map(({ key, value }) => `${key}=secreton:${encrypt(value, secretKey)}`);
121
+ .map(({ key, value }) => `${key}=Secreton:${encrypt(value, secretKey)}`);
122
122
 
123
123
  if (newLines.length === 0) {
124
124
  console.log('ℹ️ [Node] Secreton no new env keys to add');
package/src/index.tsx CHANGED
@@ -1,9 +1,9 @@
1
1
  import Secreton from './NativeSecreton';
2
2
 
3
- export function encrypt(value: string, key: string): string {
4
- return Secreton.encrypt(value, key);
3
+ export function encrypt(value: string): string {
4
+ return Secreton.encrypt(value);
5
5
  }
6
6
 
7
- export function decrypt(encrypted: string, key: string): string {
8
- return Secreton.decrypt(encrypted, key);
7
+ export function decrypt(encrypted: string): string {
8
+ return Secreton.decrypt(encrypted);
9
9
  }