Skip to content

Commit 6cc701b

Browse files
authored
Merge pull request #53 from purejava/needAuthenticatedUser
Widen API to allow storing keychain entries for an authenticated user
2 parents 7393780 + 851965a commit 6cc701b

File tree

7 files changed

+49
-32
lines changed

7 files changed

+49
-32
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
<project.jdk.version>17</project.jdk.version>
3131

3232
<!-- runtime dependencies -->
33-
<api.version>1.3.1</api.version>
33+
<api.version>1.4.0</api.version>
3434
<slf4j.version>2.0.13</slf4j.version>
3535

3636
<!-- test dependencies -->

src/main/headers/org_cryptomator_macos_keychain_MacKeychain_Native.h

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main/java/org/cryptomator/macos/keychain/MacKeychain.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,17 @@ class MacKeychain {
1717
/**
1818
* Associates the specified password with the specified key in the system keychain.
1919
*
20-
* @param serviceName Service name
21-
* @param account Unique account identifier
22-
* @param password Passphrase to store
20+
* @param serviceName Service name
21+
* @param account Unique account identifier
22+
* @param password Passphrase to store
23+
* @param requireOsAuthentication Defines, whether the user needs to authenticate to store a passphrase
2324
* @see <a href="https://developer.apple.com/documentation/security/1398366-seckeychainaddgenericpassword">SecKeychainAddGenericPassword</a>
2425
*/
25-
public void storePassword(String serviceName, String account, CharSequence password) throws KeychainAccessException {
26+
public void storePassword(String serviceName, String account, CharSequence password, boolean requireOsAuthentication) throws KeychainAccessException {
2627
ByteBuffer pwBuf = UTF_8.encode(CharBuffer.wrap(password));
2728
byte[] pwBytes = new byte[pwBuf.remaining()];
2829
pwBuf.get(pwBytes);
29-
int errorCode = Native.INSTANCE.storePassword(serviceName.getBytes(UTF_8), account.getBytes(UTF_8), pwBytes);
30+
int errorCode = Native.INSTANCE.storePassword(serviceName.getBytes(UTF_8), account.getBytes(UTF_8), pwBytes, requireOsAuthentication);
3031
Arrays.fill(pwBytes, (byte) 0x00);
3132
Arrays.fill(pwBuf.array(), (byte) 0x00);
3233
if (errorCode != OSSTATUS_SUCCESS) {
@@ -75,7 +76,7 @@ private boolean tryMigratePassword(String account) {
7576
if (pwBytes == null) {
7677
return false;
7778
}
78-
int errorCode = Native.INSTANCE.storePassword(newServiceName, account.getBytes(UTF_8), pwBytes);
79+
int errorCode = Native.INSTANCE.storePassword(newServiceName, account.getBytes(UTF_8), pwBytes, false);
7980
Arrays.fill(pwBytes, (byte) 0x00);
8081
if (errorCode != OSSTATUS_SUCCESS) {
8182
return false;
@@ -111,7 +112,7 @@ private Native() {
111112
NativeLibLoader.loadLib();
112113
}
113114

114-
public native int storePassword(byte[] service, byte[] account, byte[] value);
115+
public native int storePassword(byte[] service, byte[] account, byte[] value, boolean requireOsAuthentication);
115116

116117
public native byte[] loadPassword(byte[] service, byte[] account);
117118

src/main/java/org/cryptomator/macos/keychain/MacSystemKeychainAccess.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ public String displayName() {
3535
}
3636

3737
@Override
38-
public void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException {
39-
keychain.storePassword(SERVICE_NAME, key, passphrase);
38+
public void storePassphrase(String key, String displayName, CharSequence passphrase, boolean requireOsAuthentication) throws KeychainAccessException {
39+
keychain.storePassword(SERVICE_NAME, key, passphrase, requireOsAuthentication);
4040
}
4141

4242
@Override
@@ -62,7 +62,7 @@ public void deletePassphrase(String key) throws KeychainAccessException {
6262
@Override
6363
public void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException {
6464
if (keychain.deletePassword(SERVICE_NAME, key)) {
65-
keychain.storePassword(SERVICE_NAME, key, passphrase);
65+
keychain.storePassword(SERVICE_NAME, key, passphrase, false);
6666
}
6767
}
6868

src/main/native/org_cryptomator_macos_keychain_MacKeychain_Native.m

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,36 @@
99
#import "org_cryptomator_macos_keychain_MacKeychain_Native.h"
1010
#import <Foundation/Foundation.h>
1111
#import <Security/Security.h>
12+
#import <LocalAuthentication/LocalAuthentication.h>
1213

13-
JNIEXPORT jint JNICALL Java_org_cryptomator_macos_keychain_MacKeychain_00024Native_storePassword(JNIEnv *env, jobject thisObj, jbyteArray service, jbyteArray key, jbyteArray password) {
14+
static LAContext *sharedContext = nil;
15+
static LAContext* getSharedLAContext(void) {
16+
static dispatch_once_t onceToken;
17+
dispatch_once(&onceToken, ^{
18+
sharedContext = [[LAContext alloc] init];
19+
});
20+
return sharedContext;
21+
}
22+
23+
JNIEXPORT jint JNICALL Java_org_cryptomator_macos_keychain_MacKeychain_00024Native_storePassword(JNIEnv *env, jobject thisObj, jbyteArray service, jbyteArray key, jbyteArray password, jboolean requireOsAuthentication) {
1424
jbyte *serviceStr = (*env)->GetByteArrayElements(env, service, NULL);
1525
jbyte *keyStr = (*env)->GetByteArrayElements(env, key, NULL);
1626
jbyte *pwStr = (*env)->GetByteArrayElements(env, password, NULL);
1727
jsize length = (*env)->GetArrayLength(env, password);
1828

1929
// find existing:
20-
NSDictionary *query = @{
30+
NSMutableDictionary *query = [@{
2131
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
2232
(__bridge id)kSecAttrService: [NSString stringWithCString:(char *)serviceStr encoding:NSUTF8StringEncoding],
2333
(__bridge id)kSecAttrAccount: [NSString stringWithCString:(char *)keyStr encoding:NSUTF8StringEncoding],
2434
(__bridge id)kSecReturnAttributes: @YES,
2535
(__bridge id)kSecReturnData: @YES,
2636
(__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne
27-
};
37+
} mutableCopy];
38+
if (requireOsAuthentication) {
39+
LAContext *context = getSharedLAContext();
40+
query[(__bridge id)kSecUseAuthenticationContext] = context;
41+
}
2842
CFDictionaryRef result = NULL;
2943
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
3044
if (status == errSecSuccess && result != NULL) {
@@ -35,12 +49,15 @@ JNIEXPORT jint JNICALL Java_org_cryptomator_macos_keychain_MacKeychain_00024Nati
3549
status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)attributesToUpdate);
3650
} else if (status == errSecItemNotFound) {
3751
// add new:
38-
NSDictionary *attributes = @{
52+
NSMutableDictionary *attributes = [@{
3953
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
4054
(__bridge id)kSecAttrService: [NSString stringWithCString:(char *)serviceStr encoding:NSUTF8StringEncoding],
4155
(__bridge id)kSecAttrAccount: [NSString stringWithCString:(char *)keyStr encoding:NSUTF8StringEncoding],
4256
(__bridge id)kSecValueData: [NSData dataWithBytes:pwStr length:length]
43-
};
57+
} mutableCopy];
58+
if (requireOsAuthentication) {
59+
attributes[(__bridge id)kSecAttrAccessControl] = (__bridge_transfer id)SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlocked, kSecAccessControlUserPresence, NULL);
60+
}
4461
status = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL);
4562
} else {
4663
NSLog(@"Error storing item in keychain. Status code: %d", (int)status);
@@ -57,13 +74,15 @@ JNIEXPORT jbyteArray JNICALL Java_org_cryptomator_macos_keychain_MacKeychain_000
5774
jbyte *keyStr = (*env)->GetByteArrayElements(env, key, NULL);
5875

5976
// find existing:
77+
LAContext *context = getSharedLAContext();
6078
NSDictionary *query = @{
6179
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
6280
(__bridge id)kSecAttrService: [NSString stringWithCString:(char *)serviceStr encoding:NSUTF8StringEncoding],
6381
(__bridge id)kSecAttrAccount: [NSString stringWithCString:(char *)keyStr encoding:NSUTF8StringEncoding],
6482
(__bridge id)kSecReturnAttributes: @YES,
6583
(__bridge id)kSecReturnData: @YES,
66-
(__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne
84+
(__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne,
85+
(__bridge id)kSecUseAuthenticationContext: context
6786
};
6887
CFDictionaryRef result = NULL;
6988
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
@@ -88,18 +107,15 @@ JNIEXPORT jint JNICALL Java_org_cryptomator_macos_keychain_MacKeychain_00024Nati
88107
jbyte *keyStr = (*env)->GetByteArrayElements(env, key, NULL);
89108

90109
// find existing:
110+
LAContext *context = getSharedLAContext();
91111
NSDictionary *query = @{
92112
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
93113
(__bridge id)kSecAttrService: [NSString stringWithCString:(char *)serviceStr encoding:NSUTF8StringEncoding],
94114
(__bridge id)kSecAttrAccount: [NSString stringWithCString:(char *)keyStr encoding:NSUTF8StringEncoding],
95-
(__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne
115+
(__bridge id)kSecUseAuthenticationContext: context
96116
};
97-
CFDictionaryRef result = NULL;
98-
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
99-
if (status == errSecSuccess && result != NULL) {
100-
// delete:
101-
status = SecItemDelete((__bridge CFDictionaryRef)query);
102-
} else if (status != errSecItemNotFound) {
117+
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
118+
if (status != errSecSuccess) {
103119
NSLog(@"Error deleting item from keychain. Status code: %d", (int)status);
104120
}
105121

src/test/java/org/cryptomator/macos/keychain/MacKeychainTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class WithStoredPassword {
1717

1818
@BeforeEach
1919
public void setup() throws KeychainAccessException {
20-
keychain.storePassword("service", "account", storedPw);
20+
keychain.storePassword("service", "account", storedPw, false);
2121
}
2222

2323
@Test

src/test/java/org/cryptomator/macos/keychain/MacSystemKeychainAccessTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,17 @@ public void testDisplayName() {
3131
public void testStoreSuccess() throws KeychainAccessException {
3232
keychainAccess.storePassphrase("key", "pass");
3333

34-
Mockito.verify(keychain).storePassword("Cryptomator", "key", "pass");
34+
Mockito.verify(keychain).storePassword("Cryptomator", "key", "pass", false);
3535
}
3636

3737
@Test
3838
@DisplayName("storePassphrase() fails")
3939
public void testStoreError() throws KeychainAccessException {
4040
KeychainAccessException e = new KeychainAccessException("fail.");
41-
Mockito.doThrow(e).when(keychain).storePassword(Mockito.eq("Cryptomator"), Mockito.any(), Mockito.any());
41+
Mockito.doThrow(e).when(keychain).storePassword(Mockito.eq("Cryptomator"), Mockito.any(), Mockito.any(), Mockito.eq(false));
4242

4343
KeychainAccessException thrown = Assertions.assertThrows(KeychainAccessException.class, () -> {
44-
keychainAccess.storePassphrase("key", "pass");
44+
keychainAccess.storePassphrase("key", "", "pass", false);
4545
});
4646
Assertions.assertSame(thrown, e);
4747
}
@@ -93,7 +93,7 @@ public void testChangeSuccess() throws KeychainAccessException {
9393

9494
keychainAccess.changePassphrase("key", "newpass");
9595

96-
Mockito.verify(keychain).storePassword("Cryptomator", "key", "newpass");
96+
Mockito.verify(keychain).storePassword("Cryptomator", "key", "newpass", false);
9797
}
9898

9999
@Test
@@ -103,7 +103,7 @@ public void testChangeNotFound() throws KeychainAccessException {
103103

104104
keychainAccess.changePassphrase("key", "newpass");
105105

106-
Mockito.verify(keychain, Mockito.never()).storePassword("Cryptomator", "key", "newpass");
106+
Mockito.verify(keychain, Mockito.never()).storePassword("Cryptomator", "key", "newpass", false);
107107
}
108108

109109
@Test

0 commit comments

Comments
 (0)