diff --git a/pom.xml b/pom.xml index 065d806..d042841 100644 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ 17 - 1.3.1 + 1.4.0 2.0.13 diff --git a/src/main/headers/org_cryptomator_macos_keychain_MacKeychain_Native.h b/src/main/headers/org_cryptomator_macos_keychain_MacKeychain_Native.h index 6718362..7864238 100644 --- a/src/main/headers/org_cryptomator_macos_keychain_MacKeychain_Native.h +++ b/src/main/headers/org_cryptomator_macos_keychain_MacKeychain_Native.h @@ -10,10 +10,10 @@ extern "C" { /* * Class: org_cryptomator_macos_keychain_MacKeychain_Native * Method: storePassword - * Signature: ([B[B[B)I + * Signature: ([B[B[BZ)I */ JNIEXPORT jint JNICALL Java_org_cryptomator_macos_keychain_MacKeychain_00024Native_storePassword - (JNIEnv *, jobject, jbyteArray, jbyteArray, jbyteArray); + (JNIEnv *, jobject, jbyteArray, jbyteArray, jbyteArray, jboolean); /* * Class: org_cryptomator_macos_keychain_MacKeychain_Native diff --git a/src/main/java/org/cryptomator/macos/keychain/MacKeychain.java b/src/main/java/org/cryptomator/macos/keychain/MacKeychain.java index 12422bb..49cf008 100644 --- a/src/main/java/org/cryptomator/macos/keychain/MacKeychain.java +++ b/src/main/java/org/cryptomator/macos/keychain/MacKeychain.java @@ -17,16 +17,17 @@ class MacKeychain { /** * Associates the specified password with the specified key in the system keychain. * - * @param serviceName Service name - * @param account Unique account identifier - * @param password Passphrase to store + * @param serviceName Service name + * @param account Unique account identifier + * @param password Passphrase to store + * @param requireOsAuthentication Defines, whether the user needs to authenticate to store a passphrase * @see SecKeychainAddGenericPassword */ - public void storePassword(String serviceName, String account, CharSequence password) throws KeychainAccessException { + public void storePassword(String serviceName, String account, CharSequence password, boolean requireOsAuthentication) throws KeychainAccessException { ByteBuffer pwBuf = UTF_8.encode(CharBuffer.wrap(password)); byte[] pwBytes = new byte[pwBuf.remaining()]; pwBuf.get(pwBytes); - int errorCode = Native.INSTANCE.storePassword(serviceName.getBytes(UTF_8), account.getBytes(UTF_8), pwBytes); + int errorCode = Native.INSTANCE.storePassword(serviceName.getBytes(UTF_8), account.getBytes(UTF_8), pwBytes, requireOsAuthentication); Arrays.fill(pwBytes, (byte) 0x00); Arrays.fill(pwBuf.array(), (byte) 0x00); if (errorCode != OSSTATUS_SUCCESS) { @@ -75,7 +76,7 @@ private boolean tryMigratePassword(String account) { if (pwBytes == null) { return false; } - int errorCode = Native.INSTANCE.storePassword(newServiceName, account.getBytes(UTF_8), pwBytes); + int errorCode = Native.INSTANCE.storePassword(newServiceName, account.getBytes(UTF_8), pwBytes, false); Arrays.fill(pwBytes, (byte) 0x00); if (errorCode != OSSTATUS_SUCCESS) { return false; @@ -111,7 +112,7 @@ private Native() { NativeLibLoader.loadLib(); } - public native int storePassword(byte[] service, byte[] account, byte[] value); + public native int storePassword(byte[] service, byte[] account, byte[] value, boolean requireOsAuthentication); public native byte[] loadPassword(byte[] service, byte[] account); diff --git a/src/main/java/org/cryptomator/macos/keychain/MacSystemKeychainAccess.java b/src/main/java/org/cryptomator/macos/keychain/MacSystemKeychainAccess.java index c2f676e..201d7fe 100644 --- a/src/main/java/org/cryptomator/macos/keychain/MacSystemKeychainAccess.java +++ b/src/main/java/org/cryptomator/macos/keychain/MacSystemKeychainAccess.java @@ -35,8 +35,8 @@ public String displayName() { } @Override - public void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { - keychain.storePassword(SERVICE_NAME, key, passphrase); + public void storePassphrase(String key, String displayName, CharSequence passphrase, boolean requireOsAuthentication) throws KeychainAccessException { + keychain.storePassword(SERVICE_NAME, key, passphrase, requireOsAuthentication); } @Override @@ -62,7 +62,7 @@ public void deletePassphrase(String key) throws KeychainAccessException { @Override public void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { if (keychain.deletePassword(SERVICE_NAME, key)) { - keychain.storePassword(SERVICE_NAME, key, passphrase); + keychain.storePassword(SERVICE_NAME, key, passphrase, false); } } diff --git a/src/main/native/org_cryptomator_macos_keychain_MacKeychain_Native.m b/src/main/native/org_cryptomator_macos_keychain_MacKeychain_Native.m index 5309050..24d1a5e 100644 --- a/src/main/native/org_cryptomator_macos_keychain_MacKeychain_Native.m +++ b/src/main/native/org_cryptomator_macos_keychain_MacKeychain_Native.m @@ -9,22 +9,36 @@ #import "org_cryptomator_macos_keychain_MacKeychain_Native.h" #import #import +#import -JNIEXPORT jint JNICALL Java_org_cryptomator_macos_keychain_MacKeychain_00024Native_storePassword(JNIEnv *env, jobject thisObj, jbyteArray service, jbyteArray key, jbyteArray password) { +static LAContext *sharedContext = nil; +static LAContext* getSharedLAContext(void) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedContext = [[LAContext alloc] init]; + }); + return sharedContext; +} + +JNIEXPORT jint JNICALL Java_org_cryptomator_macos_keychain_MacKeychain_00024Native_storePassword(JNIEnv *env, jobject thisObj, jbyteArray service, jbyteArray key, jbyteArray password, jboolean requireOsAuthentication) { jbyte *serviceStr = (*env)->GetByteArrayElements(env, service, NULL); jbyte *keyStr = (*env)->GetByteArrayElements(env, key, NULL); jbyte *pwStr = (*env)->GetByteArrayElements(env, password, NULL); jsize length = (*env)->GetArrayLength(env, password); // find existing: - NSDictionary *query = @{ + NSMutableDictionary *query = [@{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService: [NSString stringWithCString:(char *)serviceStr encoding:NSUTF8StringEncoding], (__bridge id)kSecAttrAccount: [NSString stringWithCString:(char *)keyStr encoding:NSUTF8StringEncoding], (__bridge id)kSecReturnAttributes: @YES, (__bridge id)kSecReturnData: @YES, (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne - }; + } mutableCopy]; + if (requireOsAuthentication) { + LAContext *context = getSharedLAContext(); + query[(__bridge id)kSecUseAuthenticationContext] = context; + } CFDictionaryRef result = NULL; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); if (status == errSecSuccess && result != NULL) { @@ -35,12 +49,15 @@ JNIEXPORT jint JNICALL Java_org_cryptomator_macos_keychain_MacKeychain_00024Nati status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)attributesToUpdate); } else if (status == errSecItemNotFound) { // add new: - NSDictionary *attributes = @{ + NSMutableDictionary *attributes = [@{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService: [NSString stringWithCString:(char *)serviceStr encoding:NSUTF8StringEncoding], (__bridge id)kSecAttrAccount: [NSString stringWithCString:(char *)keyStr encoding:NSUTF8StringEncoding], (__bridge id)kSecValueData: [NSData dataWithBytes:pwStr length:length] - }; + } mutableCopy]; + if (requireOsAuthentication) { + attributes[(__bridge id)kSecAttrAccessControl] = (__bridge_transfer id)SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlocked, kSecAccessControlUserPresence, NULL); + } status = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL); } else { 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 jbyte *keyStr = (*env)->GetByteArrayElements(env, key, NULL); // find existing: + LAContext *context = getSharedLAContext(); NSDictionary *query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService: [NSString stringWithCString:(char *)serviceStr encoding:NSUTF8StringEncoding], (__bridge id)kSecAttrAccount: [NSString stringWithCString:(char *)keyStr encoding:NSUTF8StringEncoding], (__bridge id)kSecReturnAttributes: @YES, (__bridge id)kSecReturnData: @YES, - (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne + (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne, + (__bridge id)kSecUseAuthenticationContext: context }; CFDictionaryRef result = NULL; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); @@ -88,18 +107,15 @@ JNIEXPORT jint JNICALL Java_org_cryptomator_macos_keychain_MacKeychain_00024Nati jbyte *keyStr = (*env)->GetByteArrayElements(env, key, NULL); // find existing: + LAContext *context = getSharedLAContext(); NSDictionary *query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService: [NSString stringWithCString:(char *)serviceStr encoding:NSUTF8StringEncoding], (__bridge id)kSecAttrAccount: [NSString stringWithCString:(char *)keyStr encoding:NSUTF8StringEncoding], - (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne + (__bridge id)kSecUseAuthenticationContext: context }; - CFDictionaryRef result = NULL; - OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); - if (status == errSecSuccess && result != NULL) { - // delete: - status = SecItemDelete((__bridge CFDictionaryRef)query); - } else if (status != errSecItemNotFound) { + OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query); + if (status != errSecSuccess) { NSLog(@"Error deleting item from keychain. Status code: %d", (int)status); } diff --git a/src/test/java/org/cryptomator/macos/keychain/MacKeychainTest.java b/src/test/java/org/cryptomator/macos/keychain/MacKeychainTest.java index 8ec41ac..da6daee 100644 --- a/src/test/java/org/cryptomator/macos/keychain/MacKeychainTest.java +++ b/src/test/java/org/cryptomator/macos/keychain/MacKeychainTest.java @@ -17,7 +17,7 @@ public class WithStoredPassword { @BeforeEach public void setup() throws KeychainAccessException { - keychain.storePassword("service", "account", storedPw); + keychain.storePassword("service", "account", storedPw, false); } @Test diff --git a/src/test/java/org/cryptomator/macos/keychain/MacSystemKeychainAccessTest.java b/src/test/java/org/cryptomator/macos/keychain/MacSystemKeychainAccessTest.java index 58aeb36..a3a8642 100644 --- a/src/test/java/org/cryptomator/macos/keychain/MacSystemKeychainAccessTest.java +++ b/src/test/java/org/cryptomator/macos/keychain/MacSystemKeychainAccessTest.java @@ -31,17 +31,17 @@ public void testDisplayName() { public void testStoreSuccess() throws KeychainAccessException { keychainAccess.storePassphrase("key", "pass"); - Mockito.verify(keychain).storePassword("Cryptomator", "key", "pass"); + Mockito.verify(keychain).storePassword("Cryptomator", "key", "pass", false); } @Test @DisplayName("storePassphrase() fails") public void testStoreError() throws KeychainAccessException { KeychainAccessException e = new KeychainAccessException("fail."); - Mockito.doThrow(e).when(keychain).storePassword(Mockito.eq("Cryptomator"), Mockito.any(), Mockito.any()); + Mockito.doThrow(e).when(keychain).storePassword(Mockito.eq("Cryptomator"), Mockito.any(), Mockito.any(), Mockito.eq(false)); KeychainAccessException thrown = Assertions.assertThrows(KeychainAccessException.class, () -> { - keychainAccess.storePassphrase("key", "pass"); + keychainAccess.storePassphrase("key", "", "pass", false); }); Assertions.assertSame(thrown, e); } @@ -93,7 +93,7 @@ public void testChangeSuccess() throws KeychainAccessException { keychainAccess.changePassphrase("key", "newpass"); - Mockito.verify(keychain).storePassword("Cryptomator", "key", "newpass"); + Mockito.verify(keychain).storePassword("Cryptomator", "key", "newpass", false); } @Test @@ -103,7 +103,7 @@ public void testChangeNotFound() throws KeychainAccessException { keychainAccess.changePassphrase("key", "newpass"); - Mockito.verify(keychain, Mockito.never()).storePassword("Cryptomator", "key", "newpass"); + Mockito.verify(keychain, Mockito.never()).storePassword("Cryptomator", "key", "newpass", false); } @Test