Sync Backend Certificate Fix for macOS from https://invent.kde.org/ruixuantu/kdeconnect-mac with commits f6dc4b7b and 1533d2da

This commit is contained in:
Ruixuan Tu
2025-01-09 02:48:32 -06:00
parent aa8f7ba120
commit 61f8ea895d
6 changed files with 209 additions and 8 deletions

View File

@@ -16,3 +16,6 @@
OSStatus generateSecIdentityForUUID(NSString *uuid);
NSData* getPublicKeyDERFromCertificate(SecCertificateRef certificate);
#if TARGET_OS_OSX
NSString* extractSecCertificateDigest(SecCertificateRef certificate);
#endif

View File

@@ -37,8 +37,12 @@ OSStatus generateSecIdentityForUUID(NSString *uuid)
"CertificateService");
// Force to remove the old identity, otherwise the new identity cannot be stored
#if !TARGET_OS_OSX
NSDictionary *spec = @{(__bridge id)kSecClass: (id)kSecClassIdentity};
SecItemDelete((__bridge CFDictionaryRef)spec);
#else
// Removal for macOS is called in CertificateService.swift before this function call
#endif
EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL);
@@ -123,9 +127,15 @@ OSStatus generateSecIdentityForUUID(NSString *uuid)
// Create p12 format data
PKCS12 *p12 = NULL;
#if !TARGET_OS_OSX
p12 = PKCS12_create(/* password */ "", /* name */ "KDE Connect", pkey, x509,
/* ca */ NULL, /* nid_key */ 0, /* nid_cert */ 0,
/* iter */ 0, /* mac_iter */ PKCS12_DEFAULT_ITER, /* keytype */ 0);
#else
p12 = PKCS12_create(/* password */ NULL, /* name */ "KDE Connect", pkey, x509,
/* ca */ NULL, /* nid_key */ 0, /* nid_cert */ 0,
/* iter */ 0, /* mac_iter */ PKCS12_DEFAULT_ITER, /* keytype */ 0);
#endif
if(!p12) {
@throw [[NSException alloc] initWithName:@"Fail getP12File" reason:@"Error creating PKCS#12 structure" userInfo:nil];
}
@@ -161,6 +171,7 @@ OSStatus generateSecIdentityForUUID(NSString *uuid)
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
OSStatus securityError = SecPKCS12Import((CFDataRef) p12Data,
(CFDictionaryRef)options, &items);
#if !TARGET_OS_OSX
SecIdentityRef identityApp;
if (securityError == noErr && CFArrayGetCount(items) > 0) {
CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
@@ -181,11 +192,16 @@ OSStatus generateSecIdentityForUUID(NSString *uuid)
return 1;
}
}
#endif
// Delete the temp file
[[NSFileManager defaultManager] removeItemAtPath:p12FilePath error:nil];
#if !TARGET_OS_OSX
return noErr;
#else
return securityError;
#endif
}
NSData* getPublicKeyDERFromCertificate(SecCertificateRef certificate) {
@@ -221,3 +237,18 @@ NSData* getPublicKeyDERFromCertificate(SecCertificateRef certificate) {
return spkiData;
}
#if TARGET_OS_OSX
NSString* extractSecCertificateDigest(SecCertificateRef certificate) {
// https://stackoverflow.com/questions/63350518/how-can-i-translate-seccertificateref-cert-object-to-openssls-x509-certificate
// https://stackoverflow.com/questions/8850524/seccertificateref-how-to-get-the-certificate-information
// https://stackoverflow.com/questions/15175917/free-string-returned-by-x509-name-oneline
NSData *certData = (__bridge NSData *) SecCertificateCopyData(certificate);
const unsigned char *certDataBytes = (const unsigned char *)[certData bytes];
X509 *certX509 = d2i_X509(NULL, &certDataBytes, [certData length]);
char *buffer = calloc(8192, 1);
X509_NAME_oneline(X509_get_subject_name(certX509), buffer, 8192);
NSString *digest = [NSString stringWithUTF8String:buffer];
return digest;
}
#endif

View File

@@ -104,7 +104,10 @@ Keychain API expects as a validly constructed container class.
[genericPasswordQuery setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
[genericPasswordQuery setObject:identifier forKey:(id)kSecAttrGeneric];
#if TARGET_OS_OSX
[genericPasswordQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
#endif
// The keychain access group attribute determines if this item can be shared
// amongst multiple apps whose code signing entitlements contain the same keychain access group.
if (accessGroup != nil)
@@ -138,6 +141,9 @@ Keychain API expects as a validly constructed container class.
// Add the generic attribute and the keychain access group.
[keychainItemData setObject:identifier forKey:(id)kSecAttrGeneric];
#if TARGET_OS_OSX
[keychainItemData setObject:@"kdeconnect-uuid" forKey:(id)kSecAttrLabel];
#endif
if (accessGroup != nil)
{
#if TARGET_IPHONE_SIMULATOR
@@ -246,7 +252,13 @@ Keychain API expects as a validly constructed container class.
// Acquire the password data from the attributes.
NSData *passwordData = NULL;
if (SecItemCopyMatching((CFDictionaryRef)returnDictionary, (CFTypeRef)&passwordData) == noErr)
bool status = false;
#if TARGET_OS_OSX
status = ((passwordData = [returnDictionary objectForKey:(id)kSecValueData]));
#else
status = SecItemCopyMatching((CFDictionaryRef)returnDictionary, (CFTypeRef)&passwordData) == noErr;
#endif
if (status)
{
// Remove the search, class, and identifier key/value, we don't need them anymore.
[returnDictionary removeObjectForKey:(id)kSecReturnData];

View File

@@ -21,6 +21,8 @@ import CryptoKit
}
static func loadIdentityFromKeychain() -> SecIdentity {
// ref: https://developer.apple.com/documentation/network/creating_an_identity_for_local_network_tls
#if !os(macOS)
let keychainItemQuery: CFDictionary = [
kSecClass: kSecClassIdentity,
kSecAttrLabel: KdeConnectSettings.getUUID() as Any,
@@ -29,11 +31,47 @@ import CryptoKit
var identityApp: AnyObject? = nil
let status: OSStatus = SecItemCopyMatching(keychainItemQuery, &identityApp)
Logger().info("getIdentityFromKeychain completed with \(status)")
#else
var identityApp: SecIdentity? = nil
let getquery = [
kSecClass: kSecClassCertificate,
kSecAttrLabel: KdeConnectSettings.getUUID() as Any,
kSecReturnRef: true,
kSecMatchLimit: kSecMatchLimitOne,
] as NSDictionary
var item: CFTypeRef?
func macFetchIdentity() {
// normally will print error -25300 at the first launch as there is no identity
let status = SecItemCopyMatching(getquery as CFDictionary, &item)
guard status == errSecSuccess else {
print("getHostIdentityFromKeychain status failed with \(status)")
return
}
print("loadIdentityFromKeychain status failed with \(status)")
let certificate = item as! SecCertificate
let identityStatus = SecIdentityCreateWithCertificate(nil, certificate, &identityApp)
guard identityStatus == errSecSuccess else {
print("loadIdentityFromKeychain identityStatus failed with \(identityStatus)")
return
}
}
macFetchIdentity()
#endif
if (identityApp == nil) {
Logger().info("generateSecIdentity")
#if !os(macOS)
if generateSecIdentityForUUID(KdeConnectSettings.getUUID()) == noErr {
SecItemCopyMatching(keychainItemQuery, &identityApp)
}
#else
// remove old identity on macOS
// normally will print error -25300 at the first launch as there is no identity
deleteHostCertificateFromKeychain()
if (generateSecIdentityForUUID(KdeConnectSettings.getUUID()) == noErr) {
// Refetch
macFetchIdentity()
}
#endif
}
return (identityApp as! SecIdentity)
}
@@ -107,12 +145,34 @@ import CryptoKit
}
// @discardableResult
@objc func deleteHostCertificateFromKeychain() -> OSStatus {
@objc static func deleteHostCertificateFromKeychain() -> OSStatus {
#if !os(macOS)
let keychainItemQuery: CFDictionary = [
kSecAttrLabel: KdeConnectSettings.getUUID() as Any,
kSecClass: kSecClassIdentity,
] as CFDictionary
return SecItemDelete(keychainItemQuery)
#else
let deleteCertQuery: NSDictionary = [
kSecAttrLabel: KdeConnectSettings.getUUID() as Any,
kSecClass: kSecClassCertificate,
] as NSDictionary
let deleteCertStatus = SecItemDelete(deleteCertQuery)
guard deleteCertStatus == errSecSuccess else {
print("deleteHostCertificateFromKeychain deleteCert failed with status \(deleteCertStatus)")
return deleteCertStatus
}
let deleteKeyQuery: NSDictionary = [
kSecAttrLabel: "KDE Connect" as Any,
kSecClass: kSecClassKey,
] as NSDictionary
let deleteKeyStatus = SecItemDelete(deleteKeyQuery)
guard deleteKeyStatus == errSecSuccess else {
print("deleteHostCertificateFromKeychain deleteKey failed with status \(deleteKeyStatus)")
return deleteKeyStatus
}
return errSecSuccess
#endif
}
// This function is called by LanLink and LanLinkProvider's didReceiveTrust
@@ -190,15 +250,61 @@ import CryptoKit
}
@objc func deleteAllItemsFromKeychain() -> Bool {
#if !os(macOS)
let allSecItemClasses: [CFString] = [kSecClassGenericPassword, kSecClassInternetPassword, kSecClassCertificate, kSecClassKey, kSecClassIdentity]
for itemClass in allSecItemClasses {
let keychainItemQuery: CFDictionary = [kSecClass: itemClass] as CFDictionary
let status: OSStatus = SecItemDelete(keychainItemQuery)
if (status != 0) {
logger.error("Failed to remove 1 certificate in keychain with error code \(status), continuing to attempt to remove all")
if (status != errSecSuccess) {
print("Failed to remove 1 certificate in \(itemClass) keychain with error code \(status), continuing to attempt to remove all")
}
}
return true
#else
print("deleteAllItemsFromKeychain - host cert")
let deleteHostCertificateFromKeychainStatus = Self.deleteHostCertificateFromKeychain()
if deleteHostCertificateFromKeychainStatus != errSecSuccess {
print("deleteAllItemsFromKeychain failed to remove host cert with status \(deleteHostCertificateFromKeychainStatus)")
}
print("deleteAllItemsFromKeychain - other certs")
let getCertsQuery: NSDictionary = [
kSecClass: kSecClassCertificate,
kSecReturnRef: true,
kSecMatchLimit: kSecMatchLimitAll,
] as NSDictionary
var certs: AnyObject? = nil
let getCertsQueryStatus: OSStatus = SecItemCopyMatching(getCertsQuery, &certs)
guard getCertsQueryStatus == errSecSuccess else {
print("deleteAllItemsFromKeychain getCertsQueryStatus failed with status \(getCertsQueryStatus)")
return (getCertsQueryStatus != 0)
}
let numCerts = (certs as! NSArray).count
for certIndex in 0...(numCerts - 1) {
let cert: SecCertificate = (certs as! NSArray)[certIndex] as! SecCertificate
let digest: String = extractSecCertificateDigest(cert)
if (digest.contains("/OU=Kde connect") && digest.contains("/O=KDE")) {
print("deleteAllItemsFromKeychain to remove cert with digest \(digest)")
let removeCertQuery: NSDictionary = [
kSecClass: kSecClassCertificate,
kSecValueRef: cert,
kSecMatchLimit: kSecMatchLimitOne,
] as NSDictionary
let removeCertQueryStatus: OSStatus = SecItemDelete(removeCertQuery)
if removeCertQueryStatus != errSecSuccess {
print("deleteAllItemsFromKeychain failed to remove this cert with status \(removeCertQueryStatus)")
}
}
}
print("deleteAllItemsFromKeychain - UUID")
let removeUUIDQuery: NSDictionary = [
kSecClass: kSecClassGenericPassword,
kSecAttrLabel: "kdeconnect-uuid",
] as NSDictionary
let removeUUIDQueryStatus: OSStatus = SecItemDelete(removeUUIDQuery)
if removeUUIDQueryStatus != errSecSuccess {
print("deleteAllItemsFromKeychain failed to remove UUID with status \(removeUUIDQueryStatus)")
}
#endif
return (errSecSuccess != 0)
}
// FIXME: the temp remote cert functions are here because I dind't find a way to do this from Objective-C inside LanLink.

View File

@@ -11,6 +11,7 @@ struct AdvancedSettingsView: View {
var body: some View {
VStack {
Spacer()
Button {
backgroundService.stopDiscovery()
CertificateService.shared.deleteAllItemsFromKeychain()
@@ -28,10 +29,12 @@ struct AdvancedSettingsView: View {
}
.foregroundColor(.red)
}.buttonStyle(.plain)
Spacer()
Button {
backgroundService.stopDiscovery()
CertificateService.shared.deleteHostCertificateFromKeychain()
CertificateService.deleteHostCertificateFromKeychain()
exit(0)
} label: {
HStack {
@@ -45,6 +48,31 @@ struct AdvancedSettingsView: View {
}
.foregroundColor(.red)
}.buttonStyle(.plain)
Spacer()
Button {
backgroundService.stopDiscovery()
// ref: https://stackoverflow.com/questions/43402032/how-to-remove-all-userdefaults-data-swift
let domain = Bundle.main.bundleIdentifier!
UserDefaults.standard.removePersistentDomain(forName: domain)
UserDefaults.standard.synchronize()
print("deleted settings, remaining: \(Array(UserDefaults.standard.dictionaryRepresentation().keys).count)")
CertificateService.shared.deleteAllItemsFromKeychain()
} label: {
HStack {
Image(systemName: "delete.right")
VStack(alignment: .leading) {
Text("Forget all")
.font(.headline)
Text("Delete all saved settings and devices. Requires the app to be fully restarted.")
.font(.caption)
}
}
.foregroundColor(.red)
}.buttonStyle(.plain)
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
}.padding(.all)
}

View File

@@ -69,7 +69,7 @@ struct SettingsAdvancedView: View {
Button {
notificationHapticsGenerator.notificationOccurred(.warning)
CertificateService.shared.deleteHostCertificateFromKeychain()
CertificateService.deleteHostCertificateFromKeychain()
} label: {
HStack {
Image(systemName: "delete.right")
@@ -82,6 +82,27 @@ struct SettingsAdvancedView: View {
}
.foregroundColor(.red)
}
Button {
notificationHapticsGenerator.notificationOccurred(.warning)
// ref: https://stackoverflow.com/questions/43402032/how-to-remove-all-userdefaults-data-swift
let domain = Bundle.main.bundleIdentifier!
UserDefaults.standard.removePersistentDomain(forName: domain)
UserDefaults.standard.synchronize()
print("deleted settings, remaining: \(Array(UserDefaults.standard.dictionaryRepresentation().keys).count)")
CertificateService.shared.deleteAllItemsFromKeychain()
} label: {
HStack {
Image(systemName: "delete.right")
VStack(alignment: .leading) {
Text("Forget all")
.font(.headline)
Text("Delete all saved settings and devices. Requires the app to be fully restarted.")
.font(.caption)
}
}
.foregroundColor(.red)
}
}
if kdeConnectSettings.isDebugging {