diff --git a/mobile/ios/Info.plist b/mobile/ios/Info.plist
index e4c57449723..5cb6c76ced7 100644
--- a/mobile/ios/Info.plist
+++ b/mobile/ios/Info.plist
@@ -59,5 +59,7 @@
Photos access is required to give you the ability to send images.
NSAppleMusicUsageDescription
Status uses Media Library to save and send Images. The Media Library module internally requires permissions to Apple Music
+ NSFaceIDUsageDescription
+ Log in securely to your account.
diff --git a/mobile/wrapperApp/Status-tablet.pro b/mobile/wrapperApp/Status-tablet.pro
index 96b1078509c..1f3e72228c1 100644
--- a/mobile/wrapperApp/Status-tablet.pro
+++ b/mobile/wrapperApp/Status-tablet.pro
@@ -46,4 +46,10 @@ ios {
QMAKE_ASSET_CATALOGS += $$PWD/../ios/Images.xcassets
LIBS += -L$$PWD/../lib/$$LIB_PREFIX -lnim_status_client -lDOtherSideStatic -lstatusq -lstatus -lssl_3 -lcrypto_3 -lqzxing -lresolv -lqrcodegen
+
+ # --- iOS frameworks required by keychain_apple.mm ---
+ LIBS += -framework LocalAuthentication \
+ -framework Security \
+ -framework UIKit \
+ -framework Foundation
}
diff --git a/ui/StatusQ/CMakeLists.txt b/ui/StatusQ/CMakeLists.txt
index e9db300456e..2a77c7fd823 100644
--- a/ui/StatusQ/CMakeLists.txt
+++ b/ui/StatusQ/CMakeLists.txt
@@ -222,13 +222,13 @@ if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
target_link_libraries(${PROJECT_NAME} PRIVATE ${AppKit} ${Foundation} ${Security} ${LocalAuthentication})
target_sources(StatusQ PRIVATE
src/statuswindow_osx.mm
- src/keychain_osx.mm
+ src/keychain_apple.mm
)
elseif (${CMAKE_SYSTEM_NAME} MATCHES "iOS")
target_sources(StatusQ PRIVATE
src/ios_utils.mm
src/statuswindow_other.cpp
- src/keychain_other.cpp
+ src/keychain_apple.mm
)
else ()
target_sources(StatusQ PRIVATE
diff --git a/ui/StatusQ/include/StatusQ/keychain.h b/ui/StatusQ/include/StatusQ/keychain.h
index 158ace08c83..359c22c1f5e 100644
--- a/ui/StatusQ/include/StatusQ/keychain.h
+++ b/ui/StatusQ/include/StatusQ/keychain.h
@@ -72,7 +72,7 @@ class Keychain : public QObject {
QFuture m_future;
LAContext *m_activeAuthContext;
-#ifdef Q_OS_MACOS
+#if defined(Q_OS_MACOS) || defined(Q_OS_IOS)
Status getCredential(const QString &reason, const QString &account, QString *out);
void reevaluateAvailability();
#endif
diff --git a/ui/StatusQ/src/keychain_osx.mm b/ui/StatusQ/src/keychain_apple.mm
similarity index 78%
rename from ui/StatusQ/src/keychain_osx.mm
rename to ui/StatusQ/src/keychain_apple.mm
index de6c1988828..26bd4726b17 100644
--- a/ui/StatusQ/src/keychain_osx.mm
+++ b/ui/StatusQ/src/keychain_apple.mm
@@ -10,13 +10,24 @@
#include
#include
+#if TARGET_OS_OSX
const static auto authPolicy =
-#if defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 150000
- LAPolicyDeviceOwnerAuthenticationWithBiometricsOrCompanion;
-#elif defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 101202
- LAPolicyDeviceOwnerAuthenticationWithBiometrics;
-#else
+ #if defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 150000
+ LAPolicyDeviceOwnerAuthenticationWithBiometricsOrCompanion;
+ #elif defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 101202
+ LAPolicyDeviceOwnerAuthenticationWithBiometrics;
+ #else
+ LAPolicyDeviceOwnerAuthentication;
+ #endif
+#elif TARGET_OS_IPHONE
+const static LAPolicy authPolicy =
+ #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
+ LAPolicyDeviceOwnerAuthenticationWithBiometrics;
+ #else
LAPolicyDeviceOwnerAuthentication;
+ #endif
+#else
+const static LAPolicy authPolicy = LAPolicyDeviceOwnerAuthentication;
#endif
static Keychain::Status convertStatus(OSStatus status)
@@ -26,8 +37,16 @@
return Keychain::StatusSuccess;
case errSecItemNotFound:
return Keychain::StatusNotFound;
+#if defined(errSecCSCancelled)
+ // Present on macOS SDKs
case errSecCSCancelled:
return Keychain::StatusCancelled;
+#endif
+#if defined(errSecUserCanceled)
+ // Present on iOS (and also macOS); treat as the same "user cancelled" outcome
+ case errSecUserCanceled:
+ return Keychain::StatusCancelled;
+#endif
default:
return Keychain::StatusGenericError;
}
@@ -138,7 +157,16 @@
Keychain::Status Keychain::saveCredential(const QString &account, const QString &password)
{
CFErrorRef error = NULL;
- auto flags = kSecAccessControlBiometryCurrentSet | kSecAccessControlOr | kSecAccessControlWatch;
+
+ // On iOS there is no Apple Watch companion unlock; keep flags minimal.
+ // We still create an access control object even if it's not added to the query (left commented below),
+ // to keep parity with macOS and make it easy to enable later.
+ #if TARGET_OS_OSX
+ auto flags = kSecAccessControlBiometryCurrentSet | kSecAccessControlOr | kSecAccessControlWatch;
+ #else
+ auto flags = kSecAccessControlBiometryCurrentSet;
+ #endif
+
auto accessControl = SecAccessControlCreateWithFlags(NULL,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
flags,
@@ -157,7 +185,8 @@
(__bridge id) kSecAttrService: m_service.toNSString(),
(__bridge id) kSecAttrAccount: account.toNSString(),
(__bridge id) kSecValueData: [password.toNSString() dataUsingEncoding:NSUTF8StringEncoding],
- // (__bridge id)kSecAttrAccessControl: (__bridge id)accessControl,
+ // (__bridge id)kSecAttrAccessControl: (__bridge id)accessControl, // enable if you want Keychain to enforce biometrics
+
};
SecItemDelete((__bridge CFDictionaryRef) query); // Ensure old item is removed
@@ -208,16 +237,27 @@
(__bridge id) kSecUseAuthenticationContext: m_activeAuthContext,
};
+ // Use the LAContext with Keychain when available. iOS 11+/macOS 10.13+ support kSecUseAuthenticationContext.
+ #if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000) || \
+ (defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300)
+ NSMutableDictionary *mutableQuery = [query mutableCopy];
+ mutableQuery[(__bridge id)kSecUseAuthenticationContext] = m_activeAuthContext;
+ query = mutableQuery;
+ #endif
+
CFDataRef data = NULL;
const auto status = SecItemCopyMatching((__bridge CFDictionaryRef) query, (CFTypeRef *) &data);
+ // Convert and release CF data on success.
if (out != nullptr) {
auto dataString = [[NSString alloc] initWithData:(__bridge NSData *) data
encoding:NSUTF8StringEncoding];
*out = QString::fromNSString(dataString);
}
+ if (data) CFRelease(data);
+
return convertStatus(status);
}