Xamarin.iOS KeyChain Identity not persisting - ios

I have created a self-signed .pfx file that when I drag onto the emulator, the import screen appears, I can click through and then using select * from cert; on the emulators Keychain, I can see the imported certificate.
When I load this certificate in code and attempt to add it, I am getting a Success back from the save operation but on subsequent attempts to access the identity, it is returning ItemNotFound, further to that, when I inspect the KeyChain after the import there is no cert in the table either.
await certStream.CopyToAsync(ms);
var options = NSDictionary.FromObjectsAndKeys(new[]
{
NSObject.FromObject("CertPassword")
}, new[]
{
SecImportExport.Passphrase
});
var result = SecImportExport.ImportPkcs12(ms.ToArray(), options, out
NSDictionary[] imports);
if (result != SecStatusCode.Success)
throw new Exception("Failed importing certificate identity");
var identity = imports[0][SecImportExport.Identity];
certRecord.SetValueRef(new SecIdentity(identity.Handle));
var identityImportResult = SecKeyChain.Add(certRecord);
var savedIdentity = SecKeyChain.QueryAsRecord(new
SecRecord(SecKind.Identity)
{
Label = "ApplicationIdentityCertificate"
}, out SecStatusCode savedResult);
Reading round, I have ensured that there is a Entitlements associated with the emulator and that also, it has the keychain sharing entitlement as there appears to be anecdotal evidence that this can sometimes also impact the ability to save to the Keychain.

So I finally managed to get this working. It turns out I was missing a pretty simple method that is available on the SecKeyChain class.
var identity = imports[0][SecImportExport.Identity];
SecKeyChain.AddIdentity(new SecIdentity(identity.Handle));
The above snippet is the change I needed to make and now the identity is being stored in the Keychain with the correct bundle ID and our application can load the certificate and the private key, for whatever reason, adding it via the SecKeyChain.Add() method was not working for identities.

Related

Issue Saving Certificate to iOS Keychain -25300 (not found) if deleting, but -25299 (duplicate item) if adding

I've hit an interesting issue with the Apple keychain and am wondering what I am doing wrong.
func saveCert(accessGroup: String? = nil, certData: Data, label: String? = nil) -> Error? {
var query = createKeychainAddQueryDict()
if let accessGroup = accessGroup {
query[String(kSecAttrAccessGroup)] = accessGroup
}
query[String(kSecValueData)] = certData
query[String(kSecClass)] = kSecClassCertificate
if let label = label {
query[String(kSecAttrLabel)] = label
}
var status = SecItemDelete(query as CFDictionary)
if status != noErr {
print("Error deleting cer from keychain. Error: \(status)")
}
let resultCode = SecItemAdd(query as CFDictionary, nil)
return getErrorFromKeychainCode(code: resultCode)
}
I'm saving a self signed certificate, but I've verified the serial number is different for each item I'm trying to store.
I get a -25300 error (cannot find item) when I try to delete the cert out, but I get a -25299 error (duplicate item already exists) when I try to save into the keychain.
I'm stumped as to why or how, loading or deleting the key out of that location are both failing, and saving is declaring the position is taken.
Any insight? I've experimented with hardcoding a number of random labels that I've never used before, and they too get the duplicate entry error.
I found two solutions:
Request values for a particular key later. Use async delayed. From time to time the Keychain doesn't provide the result with -25300. The keychain is an SQLite database too. It seems the database is currently busy. So, request the data later.
You have already written something into this key but, you used another protection level. To avoid this, use keys with protection level inside its name. E.g., someKey into someKey-afterFirstUnlock
I tried lots of things, and none of them worked. I eventually found out that the kSecCertificate class uses the issuer and serial number attributes to compute its uniqueId.
Because I'm using a self-signed certificate the SecCertificateCreateWithData operation fails with result nil. I believe this is causing all my certificates to evaluate to the same empty - empty id. I tried storing this same data in a kSecGenericPassword, and set a distinct account attribute, and the issue has gone away.

Apple, iOS 13, CryptoKit, Secure Enclave - Enforce biometric authentication ahead of private key usage

I am working with Apple's new cryptokit library and am trying to get a basic use case to work.
Goal: I would like to create a private key in the secure enclave via the cryptokit, store the key's reference in the iOS device's key chain and ensure that the key can only be reinitialized in the secure enclave after the user has authenticated himself via some biometric authentication method.
Current state: So far, I am able to initialize a private key in the secure enclave via the following code:
var privateKeyReference = try CryptoKit.SecureEnclave.P256.KeyAgreement.PrivateKey.init();
Furthermore, I can store and retrieve the corresponding private key's reference from the key chain. After retrieving the reference, I can reinitialize the private key in the secure enclave with the following code:
var privateKeyReference = getPrivateKeyReferenceFromKeyChain();
var privateKey = try CryptoKit.SecureEnclave.P256.KeyAgreement.PrivateKey.init(
dataRepresentation: privateKeyReference
);
So far everything works as expected and all cryptographic operations with the private key succeed.
Now, as far as I understand the spare documentation by Apple, I should be able to modify the first initialization of the private key to something as follows.
let authContext = LAContext();
let accessCtrl = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccesibleWhenUnlockedThisDeviceOnly,
[.privateKeyUsage, .userPresence, .biometryCurrentSet],
nil
);
var privateKeyReference = try CryptoKit.SecureEnclave.P256.KeyAgreement.PrivateKey.init(
accessControl: accessCtrl!,
authenticationContext: authContext
);
Thereby, ensuring that the private key can only be reinitialized, when the user authenticates himself via some biometric authentication method. The initial initialization stil works without any errors.
Problem: However, adding the previous code, I do not get any biometric authentication prompt and can not use the private key at all after reinitialization. The following error is logged whenever I try to execute some cryptographic operation with the reinitialized key, here for example some signing:
Error Domain=CryptoTokenKit Code=-9 "setoken: unable to sign digest" UserInfo={NSLocalizedDescription=setoken: unable to sign digest})
As far as I could guess from here, I think that Code=-9 refers to the "authenticationNeeded" error.
Question: Can someone point me to some documentation or tutorial how to achieve what I am looking for or explain to me what I am missing?
Thanks!
Cross-Post: https://forums.developer.apple.com/message/387746
After a couple of days of patience I was able to obtain an answer from the Apple development support. They suggested the following method which only differs a little bit from my approach:
var error: Unmanaged<CFError>? = nil;
let accessCtrl = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccesibleAfterFirstUnlockThisDeviceOnly,
[.privateKeyUsage, .biometryCurrentSet],
&error
);
var privateKeyReference = try CryptoKit.SecureEnclave.P256.KeyAgreement.PrivateKey.init(
accessControl: accessCtrl
);
Additionally, in the meantime iOS version 13.1.3 was released and, after upgrading my device, the above code started working. So either there is a subtle difference between mine and Apple's code or it is related to the update. Nevertheless, it is working now.

How can I use keychain when I close my ios app?

I'm developing an iOS app and want user only type username:password pair only once (unless he logs out). Currently I use keychain-swift framework for storing emails/passwords.
I basically run:
let keychain = KeychainSwift()
keychain.set("johndoe", forKey: "my key") // on a successful login
keychain.get("my key")
When I run my app in a simulator I have to type password all the time (i.e., it doesn't look like it saves the password in keychain between the sessions).
Is it expected? What framework will allow me to save the data even when I close the app such that a user won't have to type username:password pairs every time to sign in?
I have never used KeychainSwift but at a guess you could do something like:
let keychain = KeychainSwift(keyPrefix: "com.Daniel.myIOSapp.")
keychain.set("johndoe", forKey: "username")
keychain.set("where is jane", forKey: "password")
which will create two "generic password" keychain items com.Daniel.myIOSapp.username and com.Daniel.myIOSapp.password and the associated values.
You normally store the username/password pair as a single keychain item. You can do that with KeychainSwift using something like:
keychain.set("where is jane", forKey: "johndoe")
which creates a single generic password item in the keychain and you probably want to store "johndoe" in your preferences under a suitable key.
HTH
Haven't used this framework myself, but from looking at the code it appears that it will save, and therefore be accessible after app is restarted.
You might want to confirm the set function is working properly. From the readme here:
https://github.com/evgenyneu/keychain-swift/blob/master/README.md
...it states:
Check if operation was successful
One can verify if set, delete and clear methods finished successfully
by checking their return values. Those methods return true on success
and false on error.
if keychain.set("hello world", forKey: "my key") { // Keychain item
is saved successfully } else { // Report error }
Also, just to confirm, the get function should read:
let myKey = keychain.get("my key")
...where myKey is put into your text field(s).

Firebase Custom Messages iOS

I'm referring to the interpreting messages portion in https://firebase.google.com/docs/cloud-messaging/ios/receive .
Where in my code can I change the text for notifications in Firebase?
In order to send a push notification to a device, You need to have a script (or a piece of code) ideally hosted on a server that would send a push notification on your behalf.
There you can customize the message and even play an audio upon receiving a notification.
Here is a code snippet in java that can be used to send a push notification to a device (or a group of devices).
private Map sendPush(String to, String from, String title, String message,
String sound) throws IOException {
sound = (sound != null) ? sound : "default"; // set default audio file name
// Creating the URL and connection
URL url = new URL(FCM_URL); // your firebase URL
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Authorization", "key=" + FCM_KEY); // the firebase project key
conn.setDoOutput(true);
// set the notification body
Map<String, String> notificationBody = new HashMap();
notificationBody.put("title", title); // notification title
notificationBody.put("body", message); // notification message
notificationBody.put("sound", sound);
notificationBody.put("badge", "1");
Map<String, String> dataBody = new HashMap();
dataBody.put("sender", from); // sender id
Map<String, Object> pushBody = new HashMap();
pushBody.put("notification", notificationBody);
pushBody.put("data", dataBody);
pushBody.put("to", to); // receiver(s) id
pushBody.put("priority", "high");
// convert your dictionary to json string using Google Gson library (similar to JsonSerialization class in swift)
String input = new Gson().toJson(pushBody);
// write input bytes in request body
try (OutputStream os = conn.getOutputStream()) {
os.write(input.getBytes());
os.flush();
}
StringBuilder responseString;
Reader reader = new InputStreamReader(conn.getInputStream()); // send request and receive response
// parse response
try (BufferedReader in = new BufferedReader(reader)) {
String inputLine;
responseString = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
responseString.append(inputLine);
}
}
// using Google Gson to convert json string into Map (similar to JsonSerialization class in swift)
Map<String, Object> responseObject = new Gson().fromJson(responseString.toString(),
Map.class);
return responseObject;
}
Since this is a java code, so i have hosted it in a java application deployed on Apache Tomcat Server.
You can find several similar implementations in various languages like php or node.js etc.
Hope this helps
first create p.12 certificate and upload in firebase ->project settings ->cloud messaging tab ->select your iOS app ->add APNS certificate.
A. Create a (.certSigningRequest) CSR file
Open Keychain Access from Utilities
From Keychain Access toolbar select Keychain Access -> Preference
In the pop up window select Certificates tab
Set both “Online Certificate Status Protocol” and “Certificate Revocation List” to “Off"
Close this window
Now from toolbar, open Keychain Access > Certificate Assistant > Request a Certificate From a Certificate Authority
Enter email address and common name that you used to register in the iOS Developer Program
Keep CA Email blank and select “Saved to disk” and “Let me specify key pair information”
Click Continue
Choose a filename & destination on your hard drive
Click Save
In the next window, set “Key Size” value to “2048 bits”
Set “Algorithm” to “RSA”
Click Continue
This will create and save your certSigningRequest file (CSR) to your hard drive. A public and private key will also be created in Keychain Access with the Common Name entered.
B. Create ".cer" file in iOS developer account
Login to apple developer account Click “Certificates, Identifiers & Profiles”
Click “Provisioning Profiles”
In the “Certificates” section click “Production”
Click the “Add” (+) button at the top-right of the main panel
Now, choose “App Store and Ad Hoc”
Click Continue
Click “Choose File” & find CSR file you’ve made from your hard drive
Click Generate
Click Download to get the file
C. Install .cer and generate .p12 certificate
Find .cer file you’ve downloaded and double-click
Set Login drop-down to “login" and Click Add
Open up KeyChain Access and you'll find profile created in Step A
You can expand “private key” profile (shows certificate you added)
Select only these two items (not the public key)
Right click and click “Export 2 items…” from popup
Now make sure file format is “.p12” and choose filename and destination on your hard drive
Click Save. Now, you’ll be prompted to set a password but keep these both blank
Click OK. Now, you have a .p12 file on your hard drive
and open your Xcode project and select target->capabilities->pusnotification->on
next do this stuff https://firebase.google.com/docs/cloud-messaging/ios/receive
next push message from firebase cloud messaging console with message and title and select your app target->user segment->your app.
then your app will able

Notification Hub Devices disappearing after Push (APNS)

We have an Azure Notification Hub set up, with APNS Configured in Production Mode, using our Production APNS Certificate.
We register our test devices (using TestFlight / Production build, and certificate) to APNS, and then to ANH with a tag dealer-1. We can send notifications using our production certificate and registered Device ID with success when using APNS directly, however, when we use 'Test Send' we get a 'Successful send' to 1 device (or however many we have registered). The notification is not received. If we then run 'Test Send' again, the are 0 devices to send to.
In the logs, we see 'APNS Errors' per device, per test send. I cannot see any way to view what the errors actually are though so this is an absolutely useless metric.
I have ran through all the troubleshooting steps and confirmed many times that everything is setup in 'Production'.
Having reviewed other questions, the answers have been along the lines of:
.. registering a sandbox certificate and then changing it to production. Unfortunately we created this hub from scratch as Production in an attempt to work around that potential issue.
.. registering sandbox devices (and thus tokens) against the production certificate. Unfortunately I have controlled this closely and ensured that we are only registering TestFlight builds (thus Production) against the ANH.
.. uploading the wrong certificate. I have confirmed with the Push Notification Tester, as above, that the certificate is correct (thumbprint confirmed, re-uploaded, etc) and works to send to the devices via Production APNS endpoint.
The resource name is: eight-technology/react-push-notification-hub
In-app registration process is as follows:
Device registers for push notifications
Registration event is handled in iOS Project (AppDelegate event)..
public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
{
//base.RegisteredForRemoteNotifications(application, deviceToken);
App.ConfigurePushNotifications(deviceToken.ToString());
}
ConfigurePushNotifications is in the XF Shared Project..
public static void ConfigurePushNotifications(string deviceToken)
{
var azureComm = DependencyService.Get<Interop.IAzureCommunication>();
azureComm.RegisterForPushTags(
"sb://eight-technology.servicebus.windows.net/",
".. token ..",
"react-push-notification-hub",
deviceToken,
StateManager.SelectedNodes.Select(m => "dealer-" + m).ToArray());
}
The implementation is pretty much as per the sample code provided (contained in iOS project)
public class AzureCommunication : DealerwebReact.Interop.IAzureCommunication
{
public void RegisterForPushTags(string url, string key, string hubName, string deviceToken, string[] tags)
{
var cs = SBConnectionString.CreateListenAccess(new NSUrl(url), key);
var hub = new SBNotificationHub(cs, hubName);
hub.RegisterNativeAsync(deviceToken, new NSSet(tags), err =>
{
if (err != null)
Console.WriteLine("Error: " + err.Description);
else
Console.WriteLine("Success");
});
}
}
After a frustrating few days, and thanks to the help of Nikita G. and hvaughan3 I finally got to the root cause of my issue. As anticipated it wasn't any of the issues actually outlined, but was to do with the way we handled the cross-plat aspect of the registrations with Xamarin Forms.
That is, we stored our token in a class as a string. The NSData that is received as part of the iOS Device registration in RegisteredForRemoteNotifications has a ToString() method that is incompatible with sending to ANH. Furthermore, RegisterNativeAsync method from the Azure library requires an NSData which I assume Xamarin can morph a string into without warning or error, hence it was unknown that the two were somewhat incompatible.
Basically, to maintain cross platform functionality, we are now simply passing the token around as an object and performing the translation back to the original type in the platform-specific implementation of our push configuration method.
Our registration method now looks like this, note the explicit use of the NSData type so that it remains untouched whilst passing through the Xamarin Forms layer:
public void RegisterForPushTags(string url, string key, string hubName, object deviceToken, string[] tags)
{
var cs = SBConnectionString.CreateListenAccess(new NSUrl(url), key);
var hub = new SBNotificationHub(cs, hubName);
hub.RegisterNativeAsync((NSData)deviceToken, new NSSet(tags), err =>
{
if (err != null)
Console.WriteLine("Error: " + err.Description);
else
Console.WriteLine("Success");
});
}
Is this the guide you used for troubleshooting?
Is there a chance you somehow do any string (or any other type of) processing on the APN handle before you register your device? The 'APNS errors' you're seeing seem to be 'invalid token size'. Since I don't know what's going on in your code, it's hard to suggest what exactly might it be, but maybe would help you.
A similar thing happened to me when the device would register correctly but as soon as a notification was sent, the devices would disappear from the list. It always turned out to be an issue with the APNS certificate that was configured within the Notification Hub was not connected properly to the App ID and/or the app was not being signed with the correct provisioning profile.

Resources