expo-secure-store 12.6.0 β 12.8.0
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/CHANGELOG.md +18 -0
- package/android/build.gradle +4 -4
- package/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt +2 -2
- package/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt +6 -0
- package/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt +8 -2
- package/android/src/main/java/expo/modules/securestore/encryptors/KeyBasedEncryptor.kt +1 -0
- package/ios/ExpoSecureStore.podspec +2 -2
- package/ios/SecureStoreModule.swift +54 -20
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,24 @@
|
|
|
10
10
|
|
|
11
11
|
### π‘ Others
|
|
12
12
|
|
|
13
|
+
## 12.8.0 β 2023-12-12
|
|
14
|
+
|
|
15
|
+
### π New features
|
|
16
|
+
|
|
17
|
+
- [iOS] Added possibility to store values that require authentication and ones that don't under the same `keychainService`. ([#23841](https://github.com/expo/expo/pull/23841) by [@behenate](https://github.com/behenate))
|
|
18
|
+
- [iOS] Added synchronous functions for storing and retrieving values from the store. ([#23841](https://github.com/expo/expo/pull/23841) by [@behenate](https://github.com/behenate))
|
|
19
|
+
|
|
20
|
+
## 12.7.0 β 2023-11-14
|
|
21
|
+
|
|
22
|
+
### π Breaking changes
|
|
23
|
+
|
|
24
|
+
- Bumped iOS deployment target to 13.4. ([#25063](https://github.com/expo/expo/pull/25063) by [@gabrieldonadel](https://github.com/gabrieldonadel))
|
|
25
|
+
- On `Android` bump `compileSdkVersion` and `targetSdkVersion` to `34`. ([#24708](https://github.com/expo/expo/pull/24708) by [@alanjhughes](https://github.com/alanjhughes))
|
|
26
|
+
|
|
27
|
+
### π‘ Others
|
|
28
|
+
|
|
29
|
+
- [Android] Enforce minimum authentication tag length for the `AESEncryptor` for improved security. ([#25294](https://github.com/expo/expo/pull/25294) by [@behenate](https://github.com/behenate))
|
|
30
|
+
|
|
13
31
|
## 12.6.0 β 2023-10-17
|
|
14
32
|
|
|
15
33
|
### π Breaking changes
|
package/android/build.gradle
CHANGED
|
@@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
|
|
|
3
3
|
apply plugin: 'maven-publish'
|
|
4
4
|
|
|
5
5
|
group = 'host.exp.exponent'
|
|
6
|
-
version = '12.
|
|
6
|
+
version = '12.8.0'
|
|
7
7
|
|
|
8
8
|
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
9
9
|
if (expoModulesCorePlugin.exists()) {
|
|
@@ -61,11 +61,11 @@ if (!safeExtGet("expoProvidesDefaultConfig", false)) {
|
|
|
61
61
|
android {
|
|
62
62
|
// Remove this if and it's contents, when support for SDK49 is dropped
|
|
63
63
|
if (!safeExtGet("expoProvidesDefaultConfig", false)) {
|
|
64
|
-
compileSdkVersion safeExtGet("compileSdkVersion",
|
|
64
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
|
65
65
|
|
|
66
66
|
defaultConfig {
|
|
67
67
|
minSdkVersion safeExtGet("minSdkVersion", 23)
|
|
68
|
-
targetSdkVersion safeExtGet("targetSdkVersion",
|
|
68
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
publishing {
|
|
@@ -94,7 +94,7 @@ android {
|
|
|
94
94
|
namespace "expo.modules.securestore"
|
|
95
95
|
defaultConfig {
|
|
96
96
|
versionCode 17
|
|
97
|
-
versionName '12.
|
|
97
|
+
versionName '12.8.0'
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -126,12 +126,12 @@ open class SecureStoreModule : Module() {
|
|
|
126
126
|
AESEncryptor.NAME -> {
|
|
127
127
|
val secretKeyEntry = getPreferredKeyEntry(SecretKeyEntry::class.java, mAESEncryptor, options, requireAuthentication, usesKeystoreSuffix)
|
|
128
128
|
?: throw DecryptException("Could not find a keychain for key $key$legacyReadFailedWarning", key, options.keychainService)
|
|
129
|
-
return mAESEncryptor.decryptItem(encryptedItem, secretKeyEntry, options, authenticationHelper)
|
|
129
|
+
return mAESEncryptor.decryptItem(key, encryptedItem, secretKeyEntry, options, authenticationHelper)
|
|
130
130
|
}
|
|
131
131
|
HybridAESEncryptor.NAME -> {
|
|
132
132
|
val privateKeyEntry = getPreferredKeyEntry(PrivateKeyEntry::class.java, hybridAESEncryptor, options, requireAuthentication, usesKeystoreSuffix)
|
|
133
133
|
?: throw DecryptException("Could not find a keychain for key $key$legacyReadFailedWarning", key, options.keychainService)
|
|
134
|
-
return hybridAESEncryptor.decryptItem(encryptedItem, privateKeyEntry, options, authenticationHelper)
|
|
134
|
+
return hybridAESEncryptor.decryptItem(key, encryptedItem, privateKeyEntry, options, authenticationHelper)
|
|
135
135
|
}
|
|
136
136
|
else -> {
|
|
137
137
|
throw DecryptException("The item for key $key in SecureStore has an unknown encoding scheme $scheme)", key, options.keychainService)
|
|
@@ -5,6 +5,7 @@ import android.security.keystore.KeyGenParameterSpec
|
|
|
5
5
|
import android.security.keystore.KeyProperties
|
|
6
6
|
import android.util.Base64
|
|
7
7
|
import expo.modules.securestore.AuthenticationHelper
|
|
8
|
+
import expo.modules.securestore.DecryptException
|
|
8
9
|
import expo.modules.securestore.SecureStoreModule
|
|
9
10
|
import expo.modules.securestore.SecureStoreOptions
|
|
10
11
|
import org.json.JSONException
|
|
@@ -108,6 +109,7 @@ class AESEncryptor : KeyBasedEncryptor<KeyStore.SecretKeyEntry> {
|
|
|
108
109
|
|
|
109
110
|
@Throws(GeneralSecurityException::class, JSONException::class)
|
|
110
111
|
override suspend fun decryptItem(
|
|
112
|
+
key: String,
|
|
111
113
|
encryptedItem: JSONObject,
|
|
112
114
|
keyStoreEntry: KeyStore.SecretKeyEntry,
|
|
113
115
|
options: SecureStoreOptions,
|
|
@@ -122,6 +124,9 @@ class AESEncryptor : KeyBasedEncryptor<KeyStore.SecretKeyEntry> {
|
|
|
122
124
|
val cipher = Cipher.getInstance(AES_CIPHER)
|
|
123
125
|
val requiresAuthentication = encryptedItem.optBoolean(AuthenticationHelper.REQUIRE_AUTHENTICATION_PROPERTY)
|
|
124
126
|
|
|
127
|
+
if (authenticationTagLength < MIN_GCM_AUTHENTICATION_TAG_LENGTH) {
|
|
128
|
+
throw DecryptException("Authentication tag length must be at least $MIN_GCM_AUTHENTICATION_TAG_LENGTH bits long", key, options.keychainService)
|
|
129
|
+
}
|
|
125
130
|
cipher.init(Cipher.DECRYPT_MODE, keyStoreEntry.secretKey, gcmSpec)
|
|
126
131
|
val unlockedCipher = authenticationHelper.authenticateCipher(cipher, requiresAuthentication, options.authenticationPrompt)
|
|
127
132
|
return String(unlockedCipher.doFinal(ciphertextBytes), StandardCharsets.UTF_8)
|
|
@@ -134,5 +139,6 @@ class AESEncryptor : KeyBasedEncryptor<KeyStore.SecretKeyEntry> {
|
|
|
134
139
|
private const val CIPHERTEXT_PROPERTY = "ct"
|
|
135
140
|
const val IV_PROPERTY = "iv"
|
|
136
141
|
private const val GCM_AUTHENTICATION_TAG_LENGTH_PROPERTY = "tlen"
|
|
142
|
+
private const val MIN_GCM_AUTHENTICATION_TAG_LENGTH = 96
|
|
137
143
|
}
|
|
138
144
|
}
|
|
@@ -80,7 +80,13 @@ class HybridAESEncryptor(private var mContext: Context, private val mAESEncrypto
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
@Throws(GeneralSecurityException::class, JSONException::class)
|
|
83
|
-
override suspend fun decryptItem(
|
|
83
|
+
override suspend fun decryptItem(
|
|
84
|
+
key: String,
|
|
85
|
+
encryptedItem: JSONObject,
|
|
86
|
+
keyStoreEntry: KeyStore.PrivateKeyEntry,
|
|
87
|
+
options: SecureStoreOptions,
|
|
88
|
+
authenticationHelper: AuthenticationHelper
|
|
89
|
+
): String {
|
|
84
90
|
// Decrypt the encrypted symmetric key
|
|
85
91
|
val encryptedSecretKeyString = encryptedItem.getString(ENCRYPTED_SECRET_KEY_PROPERTY)
|
|
86
92
|
val encryptedSecretKeyBytes = Base64.decode(encryptedSecretKeyString, Base64.DEFAULT)
|
|
@@ -92,7 +98,7 @@ class HybridAESEncryptor(private var mContext: Context, private val mAESEncrypto
|
|
|
92
98
|
|
|
93
99
|
// Decrypt the value with the symmetric key
|
|
94
100
|
val secretKeyEntry = KeyStore.SecretKeyEntry(secretKey)
|
|
95
|
-
return mAESEncryptor.decryptItem(encryptedItem, secretKeyEntry, options, authenticationHelper)
|
|
101
|
+
return mAESEncryptor.decryptItem(key, encryptedItem, secretKeyEntry, options, authenticationHelper)
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
@get:Throws(NoSuchAlgorithmException::class, NoSuchProviderException::class, NoSuchPaddingException::class)
|
|
@@ -10,7 +10,7 @@ Pod::Spec.new do |s|
|
|
|
10
10
|
s.license = package['license']
|
|
11
11
|
s.author = package['author']
|
|
12
12
|
s.homepage = package['homepage']
|
|
13
|
-
s.platform = :ios, '13.
|
|
13
|
+
s.platform = :ios, '13.4'
|
|
14
14
|
s.swift_version = '5.4'
|
|
15
15
|
s.source = { git: 'https://github.com/expo/expo.git' }
|
|
16
16
|
s.static_framework = true
|
|
@@ -22,7 +22,7 @@ Pod::Spec.new do |s|
|
|
|
22
22
|
'DEFINES_MODULE' => 'YES',
|
|
23
23
|
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
if !$ExpoUseSources&.include?(package['name']) && ENV['EXPO_USE_SOURCE'].to_i == 0 && File.exist?("#{s.name}.xcframework") && Gem::Version.new(Pod::VERSION) >= Gem::Version.new('1.10.0')
|
|
27
27
|
s.source_files = "**/*.h"
|
|
28
28
|
s.vendored_frameworks = "#{s.name}.xcframework"
|
|
@@ -17,20 +17,22 @@ public final class SecureStoreModule: Module {
|
|
|
17
17
|
])
|
|
18
18
|
|
|
19
19
|
AsyncFunction("getValueWithKeyAsync") { (key: String, options: SecureStoreOptions) -> String? in
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
20
|
+
return try get(with: key, options: options)
|
|
21
|
+
}
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
Function("getValueWithKeySync") { (key: String, options: SecureStoreOptions) -> String? in
|
|
24
|
+
return try get(with: key, options: options)
|
|
25
|
+
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
AsyncFunction("setValueWithKeyAsync") { (value: String, key: String, options: SecureStoreOptions) -> Bool in
|
|
28
|
+
guard let key = validate(for: key) else {
|
|
29
|
+
throw InvalidKeyException()
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
return
|
|
32
|
+
return try set(value: value, with: key, options: options)
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
Function("setValueWithKeySync") {(value: String, key: String, options: SecureStoreOptions) -> Bool in
|
|
34
36
|
guard let key = validate(for: key) else {
|
|
35
37
|
throw InvalidKeyException()
|
|
36
38
|
}
|
|
@@ -39,33 +41,61 @@ public final class SecureStoreModule: Module {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
AsyncFunction("deleteValueWithKeyAsync") { (key: String, options: SecureStoreOptions) in
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
+
let noAuthSearchDictionary = query(with: key, options: options, requireAuthentication: false)
|
|
45
|
+
let authSearchDictionary = query(with: key, options: options, requireAuthentication: true)
|
|
46
|
+
let legacySearchDictionary = query(with: key, options: options)
|
|
47
|
+
|
|
48
|
+
SecItemDelete(legacySearchDictionary as CFDictionary)
|
|
49
|
+
SecItemDelete(authSearchDictionary as CFDictionary)
|
|
50
|
+
SecItemDelete(noAuthSearchDictionary as CFDictionary)
|
|
44
51
|
}
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
private func get(with key: String, options: SecureStoreOptions) throws -> String? {
|
|
55
|
+
guard let key = validate(for: key) else {
|
|
56
|
+
throw InvalidKeyException()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if let unauthenticatedItem = try searchKeyChain(with: key, options: options, requireAuthentication: false) {
|
|
60
|
+
return String(data: unauthenticatedItem, encoding: .utf8)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if let authenticatedItem = try searchKeyChain(with: key, options: options, requireAuthentication: true) {
|
|
64
|
+
return String(data: authenticatedItem, encoding: .utf8)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if let legacyItem = try searchKeyChain(with: key, options: options) {
|
|
68
|
+
return String(data: legacyItem, encoding: .utf8)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return nil
|
|
72
|
+
}
|
|
73
|
+
|
|
47
74
|
private func set(value: String, with key: String, options: SecureStoreOptions) throws -> Bool {
|
|
48
|
-
var
|
|
75
|
+
var setItemQuery = query(with: key, options: options, requireAuthentication: options.requireAuthentication)
|
|
49
76
|
|
|
50
77
|
let valueData = value.data(using: .utf8)
|
|
51
|
-
|
|
78
|
+
setItemQuery[kSecValueData as String] = valueData
|
|
52
79
|
|
|
53
80
|
let accessibility = attributeWith(options: options)
|
|
54
81
|
|
|
55
82
|
if !options.requireAuthentication {
|
|
56
|
-
|
|
83
|
+
setItemQuery[kSecAttrAccessible as String] = accessibility
|
|
57
84
|
} else {
|
|
58
85
|
guard let _ = Bundle.main.infoDictionary?["NSFaceIDUsageDescription"] as? String else {
|
|
59
86
|
throw MissingPlistKeyException()
|
|
60
87
|
}
|
|
61
88
|
let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, SecAccessControlCreateFlags.biometryCurrentSet, nil)
|
|
62
|
-
|
|
89
|
+
setItemQuery[kSecAttrAccessControl as String] = accessOptions
|
|
63
90
|
}
|
|
64
91
|
|
|
65
|
-
let status = SecItemAdd(
|
|
92
|
+
let status = SecItemAdd(setItemQuery as CFDictionary, nil)
|
|
66
93
|
|
|
67
94
|
switch status {
|
|
68
95
|
case errSecSuccess:
|
|
96
|
+
// On success we want to remove the other key alias and legacy key (if they exist) to avoid conflicts during reads
|
|
97
|
+
SecItemDelete(query(with: key, options: options) as CFDictionary)
|
|
98
|
+
SecItemDelete(query(with: key, options: options, requireAuthentication: !options.requireAuthentication) as CFDictionary)
|
|
69
99
|
return true
|
|
70
100
|
case errSecDuplicateItem:
|
|
71
101
|
return try update(value: value, with: key, options: options)
|
|
@@ -75,7 +105,7 @@ public final class SecureStoreModule: Module {
|
|
|
75
105
|
}
|
|
76
106
|
|
|
77
107
|
private func update(value: String, with key: String, options: SecureStoreOptions) throws -> Bool {
|
|
78
|
-
var query = query(with: key, options: options)
|
|
108
|
+
var query = query(with: key, options: options, requireAuthentication: options.requireAuthentication)
|
|
79
109
|
|
|
80
110
|
let valueData = value.data(using: .utf8)
|
|
81
111
|
let updateDictionary = [kSecValueData as String: valueData]
|
|
@@ -93,8 +123,8 @@ public final class SecureStoreModule: Module {
|
|
|
93
123
|
}
|
|
94
124
|
}
|
|
95
125
|
|
|
96
|
-
private func searchKeyChain(with key: String, options: SecureStoreOptions) throws -> Data? {
|
|
97
|
-
var query = query(with: key, options: options)
|
|
126
|
+
private func searchKeyChain(with key: String, options: SecureStoreOptions, requireAuthentication: Bool? = nil) throws -> Data? {
|
|
127
|
+
var query = query(with: key, options: options, requireAuthentication: requireAuthentication)
|
|
98
128
|
|
|
99
129
|
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
100
130
|
query[kSecReturnData as String] = kCFBooleanTrue
|
|
@@ -119,8 +149,12 @@ public final class SecureStoreModule: Module {
|
|
|
119
149
|
}
|
|
120
150
|
}
|
|
121
151
|
|
|
122
|
-
private func query(with key: String, options: SecureStoreOptions) -> [String: Any] {
|
|
123
|
-
|
|
152
|
+
private func query(with key: String, options: SecureStoreOptions, requireAuthentication: Bool? = nil) -> [String: Any] {
|
|
153
|
+
var service = options.keychainService ?? "app"
|
|
154
|
+
if let requireAuthentication {
|
|
155
|
+
service.append(":\(requireAuthentication ? "auth" : "no-auth")")
|
|
156
|
+
}
|
|
157
|
+
|
|
124
158
|
let encodedKey = Data(key.utf8)
|
|
125
159
|
|
|
126
160
|
return [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-secure-store",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.8.0",
|
|
4
4
|
"description": "Provides a way to encrypt and securely store keyβvalue pairs locally on the device.",
|
|
5
5
|
"main": "build/SecureStore.js",
|
|
6
6
|
"types": "build/SecureStore.d.ts",
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"expo": "*"
|
|
44
44
|
},
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "6aca7ce098ddc667776a3d7cf612adbb985e264a"
|
|
46
46
|
}
|