Keychain - How items are stored

iOS KeyChain - How items are stored #

In the ever-evolving landscape of cybersecurity, understanding the intricacies of secure data storage is important. Among the critical components safeguarding sensitive information on iOS devices, the iOS Keychain is used to securely store, access and manage passwords, security certificates, private keys, passkeys, and secure notes.

In this article, we will details what happens when an item is added in the keychain, and how this items is stored in database. It will not cover all the details on how the access control is done as it is another big subject for another article.

Interaction with the keychain #

The user can interact with the keychain using, by example, the iOS password manager. An iOS application can store secrets in the keychain using the Apple API :

The securityd daemon is in charge of the keychain management. The SecItem* API functions call securityd through XPC when an application uses these functions.

Access control #

An application should access only the items it owns. securityd will ensure that an application cannot read items that doesn’t belongs to it. This security is mostly based on the application-identifier.

Keychain items #

User’s entries, such as passwords or cryptographic keys stored in the keychains, are called items. In addition with the data itself, the item contains publicly visible attributes (metadata) to control item’s accessibility and to make it searchable.

As shown in this figure from Apple Documentation, keychain service handles the data encryption and storage.

The items are represented by the SecKeychainItemRef opaque type.

The item types are defined by a kSecClass attribute. The possible values for the item types are :

With the help of the Security source code, it is possible to retrieve the value for these classes.

/* Class Value Constants */
SEC_CONST_DECL (kSecClassGenericPassword, "genp");
SEC_CONST_DECL (kSecClassInternetPassword, "inet");
SEC_CONST_DECL (kSecClassAppleSharePassword, "apls");
SEC_CONST_DECL (kSecClassCertificate, "cert");
SEC_CONST_DECL (kSecClassKey, "keys");
SEC_CONST_DECL (kSecClassIdentity, "idnt");

Each class will have its specific table in the keychain database.

Keychain items are encrypted using two different AES-256-GCM keys :

  • A key that is used for item’s metadata (kSecAttr*)
  • A key used for secret value (kSecValueData).

On disk storage #

All these items are stored in a Sqlite 3 database located in /private/var/Keychains/keychain-2.db

With the helps of a jailbroken device, it is possible to observe the content of the database. After adding an account for a website using the iPhone password manager, a new item is added into the table inet.

Here is an example of this type of entry :

We can observe that most of the fields are blob of encrypted data.

Apple API #

If an application wants to add a password into the keychain, it must fill a dictionary with some required values. It then can call SecItemAdd to add the items into the keychain.

let account = credentials.username
let password = credentials.password.data(using: String.Encoding.utf8)!
var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
                            kSecAttrAccount as String: account,
                            kSecAttrServer as String: server,
                            kSecValueData as String: password]
                            
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }

More information about API can be found here : Using the keychain to manager user secrets

How it works #

Let’s explain how it works under the hood. This explanations are focused on what is happening when we add a password to the iOS password manager. How the item is encrypted or decrypted, and how it is stored in the database. We used Apple’s Security source code, Frida, and various web articles to help us understanding that part of the keychain.

Please note that all the informations gave may be not up to date as iOS continuously evolves.

Open source Security code #

Firstly, It is possible to retrieve the source code of the Security module. This will help greatly as it contains the code for securityd.

It is possible to retrieve the version of the securityd binary by using this command :

~/work/keychain_experiments ยป otool -V -l ~/securityd  | grep version        
  version 857.1
  version 60420.140.26

There is a branch corresponding to 60420.140.26 on the repository : Security

Database field structure #

The items are stored in a sqlite 3 database. The database contains, among others thing, tables corresponding for each item classes. genp table for kSecClassGenericPassword, inet table for kSecClassInternetPassword etc… These tables contain several fields that are described in the file Security/keychain/securityd/SecItemSchema.c : Each SECDB_ATTR define a field in tables. The most interesting one is the data fields. It contains all the information for the stored items. This attributes also defines a function pointer to SecDbKeychainItemCopyEncryptedData. This is (one of) the function called when a item is being added.

Adding a new item #

Let’s try to add an item into the iOS password manager and describe what is happening. When adding a new item securityd receive an XPC and it dispatch it. Then the function SecDbKeychainItemCopyEncryptedData gets called.

CFTypeRef SecDbKeychainItemCopyEncryptedData(SecDbItemRef item, const SecDbAttr *attr, CFErrorRef *error)

This function calls ks_encrypt_data. ks_encrypt_data is the function in charge of encrypting the item attributes and the secret part.

bool ks_encrypt_data(keybag_handle_t keybag, SecAccessControlRef _Nullable access_control, CFDataRef _Nullable acm_context,
                     CFDictionaryRef _Nonnull secretData, CFDictionaryRef _Nonnull attributes, CFDictionaryRef _Nonnull authenticated_attributes, CFDataRef _Nonnull *pBlob, bool useDefaultIV, CFErrorRef *error) {
...
        /* Mergin `attributes` and `authenticated_attributes` into `metadataAttributes` */
        NSMutableDictionary* metadataAttributes = attributes ? [(__bridge NSDictionary*)attributes mutableCopy] : [NSMutableDictionary dictionary];
        [metadataAttributes addEntriesFromDictionary:(__bridge NSDictionary*)authenticated_attributes];
        metadataAttributes[@"SecAccessControl"] = (__bridge_transfer NSData*)SecAccessControlCopyData(access_control);
...
        SecDbKeychainItemV7* item = [[SecDbKeychainItemV7 alloc] initWithSecretAttributes:(__bridge NSDictionary*_Nonnull)secretData metadataAttributes:metadataAttributes tamperCheck:tamperCheck keyclass:key_class];
...
        NSData* encryptedBlob = [item encryptedBlobWithKeybag:keybag accessControl:access_control acmContext:(__bridge NSData*)acm_context error:&localError];
        if (encryptedBlob) {
            NSMutableData* encryptedBlobWithVersion = [NSMutableData dataWithLength:encryptedBlob.length + sizeof(uint32_t)];
...
            // The encrypted blob is here -> starting at blob+4
            memcpy((uint32_t*)encryptedBlobWithVersion.mutableBytes + 1, encryptedBlob.bytes, encryptedBlob.length);
            *pBlob = (__bridge_retained CFDataRef)encryptedBlobWithVersion;
            ...

With the helps of Frida, it is possible to observe what is inside the arguments secretData, attributes and authenticated_attributes :

ks_encrypt_data(
	 CFDictionaryRef _Nonnull secretData : 0xe8cb4e170
		key  v_Data value {length = 8, bytes = 0x70617373776f7264}
            0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
e8cb4e420  70 61 73 73 77 6f 72 64                          password
	 CFDictionaryRef _Nonnull attributes
		key  labl value website.fr (toto)
		key  acct value toto
		key  atyp value form
		key  path value 
		key  sdmn value 
		key  cdat value 2024-01-15 15:44:27 +0000
		key  srvr value website.fr
		key  mdat value 2024-01-15 15:44:27 +0000
		key  pdmn value ak
		key  ptcl value htps
		key  port value 0
	 CFDictionaryRef _Nonnull authenticated_attributes
		key  musr value {length = 0, bytes = 0x}
		key  agrp value com.apple.cfnetwork
		key  persistref value {length = 16, bytes = 0x7afb5d44345f46b2a291de87f34dd848}
		key  UUID value 1E963C7B-EC0D-4D9F-B66E-73E7F7B6F978
		key  sync value 1
		key  sha1 value {length = 20, bytes = 0xc3a2126f1abe3f4d56129984e854789f235a9c83}
		key  tomb value 0

attributes and authenticated_attributes are containing all values other then the kSecValueData and the item’s metadata. secretData is containing the kSecValueData (in this example password).

The function is merging attributes and authenticated_attributes into metadataAttributes. From this, it’s creating a SecDbKeychainItemV7 item, initialised with the secretData and the metadataAttributes, and will start the encryption process by calling encryptedBlobWithVersion

SecDbKeychainItemV7* item = [[SecDbKeychainItemV7 alloc] initWithSecretAttributes:(__bridge NSDictionary*_Nonnull)secretData metadataAttributes:metadataAttributes tamperCheck:tamperCheck keyclass:key_class];
...
        NSData* encryptedBlob = [item encryptedBlobWithKeybag:keybag accessControl:access_control acmContext:(__bridge NSData*)acm_context error:&localError];

The function encryptedBlobWithKeybag will encrypt :

  • The metadata using the function : encryptMetadataWithKeybag
  • The secret data using the function : encryptSecretDataWithKeybag
- (NSData*)encryptedBlobWithKeybag:(keybag_handle_t)keybag accessControl:(SecAccessControlRef)accessControl acmContext:(NSData*)acmContext error:(NSError**)error
{
...
    BOOL success = [self encryptMetadataWithKeybag:keybag error:&localError];
...
    success = [self encryptSecretDataWithKeybag:keybag accessControl:accessControl acmContext:acmContext error:&localError];

After that, it returns the bytes from the object containing encrypted metadata and secret. It will be detailed later.

    SecDbKeychainSerializedItemV7* serializedItem = [[SecDbKeychainSerializedItemV7 alloc] init];
    serializedItem.encryptedMetadata = self.encryptedMetadataBlob;
    serializedItem.encryptedSecretData = self.encryptedSecretDataBlob;
    return serializedItem.data;
}

Metadata encryption #

Metadata are encrypted using the function encryptMetadataWithKeybag.

- (BOOL)encryptMetadataWithKeybag:(keybag_handle_t)keybag error:(NSError**)error
{
    SFAESKey* key = [[SFAESKey alloc] initRandomKeyWithSpecifier:[self.class keySpecifier] error:error];
...
    NSMutableDictionary* attributesToEncrypt = _metadataAttributes.mutableCopy;
    attributesToEncrypt[SecDBTamperCheck] = _tamperCheck;
    NSData* metadata = (__bridge_transfer NSData*)CFPropertyListCreateDERData(NULL, (__bridge CFDictionaryRef)attributesToEncrypt, NULL);
...
    SFAuthenticatedCiphertext* ciphertext = [metadata withKey:key error:error];
    SFAESKey* metadataClassKey = [self metadataClassKeyWithKeybag:keybag
                                                      allowWrites:true
                                                            error:error];
    if (metadataClassKey) {
        SFAuthenticatedCiphertext* wrappedKey = [encryptionOperation encrypt:key.keyData withKey:metadataClassKey error:error];
        _encryptedMetadata = [[SecDbKeychainMetadata alloc] initWithCiphertext:ciphertext wrappedKey:wrappedKey tamperCheck:_tamperCheck error:error];
    }

    return _encryptedMetadata != nil;
}

This function uses the SF* functions from the SecurityFoundation private framework to create a key ([SFAESKey initRandomKeyWithSpecifier]), encrypts the metadata with this key using [metadata withKey], and wrap it using [encryptionOperation encrypt]. Let’s explain what is the concept of key wrapping.

Key Wrapping #

Key wrapping is a technique used to protect cryptographic keys during storage or transmission by encrypting them with another key, known as a wrapping key. Here there are several metadata’s wrapping key (one wrapping key for each item class).

Metadata wrapping key #

In the keychain, metadata wrapping keys are cached. If no wrapping key is found in the cache, it will try to retrieve a key from the metadatakeys table. If no key is found in the table, a new one will be created. We can observe this by analysing the function the function metadataClassKeyWithKeybag. This is the function used to retrieve a metadata key. This function is calling the function keyForKeyclass.

keyForKeyclass will verify if a key for this class exists is in the cache, and will return it.

// try our cache first and rejoice if that succeeds
        bool allowCaching = [SecDbKeychainMetadataKeyStore cachingEnabled];

        key = allowCaching ? self->_keysDict[@(keyclass)] : nil;
        if (key) {
            return;     // Cache contains validated key for class, excellent!
        }

If the key is not in cache, it will try to retrieve it from database using fetchKeyForClass

- (SFAESKey* _Nullable)fetchKeyForClass:(keyclass_t)keyclass
                       fromDb:(SecDbConnectionRef)dbt
                       keybag:(keybag_handle_t)keybag
                    specifier:(SFAESKeySpecifier*)keySpecifier
                  allowWrites:(BOOL)allowWrites
                        error:(NSError**)error
{
    dispatch_assert_queue(_queue);

    NSData* wrappedKey;
    keyclass_t actualKeyClass = key_class_none;
    if (![self readKeyDataForClass:keyclass fromDb:dbt actualKeyclass:&actualKeyClass wrappedKey:&wrappedKey error:error]) {
        return nil;
    }
...

    return key;
}

Function fetchKeyForClass will call readKeyDataForClass to request a key in the database.

- (BOOL)readKeyDataForClass:(keyclass_t)keyclass
                     fromDb:(SecDbConnectionRef)dbt
             actualKeyclass:(keyclass_t*)actualKeyclass
                 wrappedKey:(NSData**)outWrappedKey
                      error:(NSError**)error
{
...
    NSString* sql = @"SELECT data, actualKeyclass FROM metadatakeys WHERE keyclass = ?";
    ...
    ok &= SecDbPrepare(dbt, (__bridge CFStringRef)sql, &cfError, ^(sqlite3_stmt *stmt) {
        ok &= SecDbBindObject(stmt, 1, (__bridge CFNumberRef)@(keyclass), &cfError);
        ok &= SecDbStep(dbt, stmt, &cfError, ^(bool *stop) {
            wrappedKey = [[NSData alloc] initWithBytes:sqlite3_column_blob(stmt, 0) length:sqlite3_column_bytes(stmt, 0)];
            *actualKeyclass = sqlite3_column_int(stmt, 1);
            found = true;
...
}

It uses the request SELECT data, actualKeyclass FROM metadatakeys WHERE keyclass = ? and set the outWrappedKey if the key is found. The metadata keys are stored wrapped in the database, so if a key has been found, it now needs to unwrap it. This is done using the function validateWrappedKey :

...
    if (wrappedKey.length > 0) {
        classFromDisk = actualKeyClass;
        key = [self validateWrappedKey:wrappedKey forKeyClass:keyclass actualKeyClass:&actualKeyClass keybag:keybag keySpecifier:keySpecifier error:error];
...

The function validateWrappedKey will unwrap the key using the function aksDecryptWithKeybag. This function will call ks_crypt with the kAKSKeyOpDecrypt operation.

+ (bool)aksDecryptWithKeybag:(keybag_handle_t)keybag keyclass:(keyclass_t)keyclass ciphertext:(NSData*)ciphertext
                 outKeyclass:(keyclass_t*)outKeyclass plaintext:(NSMutableData*)plaintext personaId:(const void*)personaId personaIdLength:(size_t)personaIdLength error:(NSError**)error
{
...
        result = ks_crypt(kAKSKeyOpDecrypt, keybag, keyclass, (uint32_t)ciphertext.length, ciphertext.bytes, outKeyclass, (__bridge CFMutableDataRef)plaintext, &cfError);
...
}

ks_crypt for kAKSKeyOpDecrypt will call the function aks_unwrap_key.

/* Wrap takes a 128 - 256 bit key as input and returns output of
 inputsize + 64 bits.
 In bytes this means that a
 16 byte (128 bit) key returns a 24 byte wrapped key
 24 byte (192 bit) key returns a 32 byte wrapped key
 32 byte (256 bit) key returns a 40 byte wrapped key  */
bool ks_crypt(CFTypeRef operation, keybag_handle_t keybag,
              keyclass_t keyclass, uint32_t textLength, const uint8_t *source, keyclass_t *actual_class, CFMutableDataRef dest, CFErrorRef *error) {
...
    } else if (CFEqual(operation, kAKSKeyOpDecrypt) || CFEqual(operation, kAKSKeyOpDelete)) {
        kernResult = aks_unwrap_key(source, textLength, keyclass, keybag, CFDataGetMutableBytePtr(dest), &dest_len);
...    

The code for this function is not available, but by doing some reverse engineering it’s easy to find ks_crypt and to find aks_unwrap_key.

__int64 __fastcall aks_unwrap_key(const void *source, int textlen, int keyclass, int keybag, void *dest, int *dest_len)
{
...
  con_apple_key_store = open_connection_AppleKeyStore();
...
          input[0] = keybag;
          input[1] = keyclass;
          outputStructCnt = *dest_len;
          v12 = IOConnectCallMethod(
                  con_apple_key_store,
                  0xBu,
                  (const uint64_t *)input,
                  2u,
                  source,
                  textlen,
                  0LL,
                  0LL,
                  dest,
                  &outputStructCnt);
...
  return v12;
}

The aks_unwrap_key will communicate with the AppleKeyStore service exposed by the kernel. It will use the selector 0xB. It sends the wrapped key through this, and retrieves the unwrapped key. This unwrapped key is then used to encrypt the metadata.

If no key has been found, keyForKeyclass will call newKeyForKeyclass to create a new key. This function will create a new key, wrap the key using the function aksEncryptWithKeybag and ks_crypt.

bool ks_crypt(CFTypeRef operation, keybag_handle_t keybag,
              keyclass_t keyclass, uint32_t textLength, const uint8_t *source, keyclass_t *actual_class, CFMutableDataRef dest, CFErrorRef *error) {
#if USE_KEYSTORE
    kern_return_t kernResult = kAKSReturnBadArgument;
    
    int dest_len = (int)CFDataGetLength(dest);
    if (CFEqual(operation, kAKSKeyOpEncrypt)) {
        kernResult = aks_wrap_key(source, textLength, keyclass, keybag, CFDataGetMutableBytePtr(dest), &dest_len, actual_class);
...

This time ks_crypt will call the function aks_wrap_key. By doing reverse engineering, We observed that aks_wrap_key will communicate with the AppleKeyStore service exposed by the kernel, but this time using the selector 0xA.

__int64 __fastcall aks_wrap_key(
        const void *source,
        int textLength,
        int keyclass,
        int keybag,
        void *dest,
        int *dest_len,
        _DWORD *actual_class)
{
...
  v15 = open_connection_AppleKeyStore();
...
        input[0] = keybag;
        input[1] = keyclass;
        output = 0LL;
        outputCnt = 1;
        outputStructCnt = *dest_len;
        v16 = IOConnectCallMethod(
                v15,
                0xAu,
                (const uint64_t *)input,
                2u,
                source,
                textLength,
                &output,
                &outputCnt,
                dest,
                &outputStructCnt);
...
}

From this, it retrieves a wrapped key. This wrapped key will be inserted into the metadatakeys table.

Storing the encrypted metadata and the wrapped key. #

After all that, the function encryptMetadataWithKeybag calls [SecDbKeychainMetadata -initWithCiphertext]

if (metadataClassKey) {
        SFAuthenticatedCiphertext* wrappedKey = [encryptionOperation encrypt:key.keyData withKey:metadataClassKey error:error];
        _encryptedMetadata = [[SecDbKeychainMetadata alloc] initWithCiphertext:ciphertext wrappedKey:wrappedKey tamperCheck:_tamperCheck error:error];
    }

initWithCiphertext will create a SecDbKeychainMetadata object. This object will contain the encrypted data and the wrapped key object that has been serialized. This is done with [NSKeyedArchiver archivedDataWithRootObject]

@implementation SecDbKeychainMetadata {
    SecDbKeychainSerializedMetadata* _serializedHolder;
}

- (instancetype)initWithCiphertext:(SFAuthenticatedCiphertext*)ciphertext
                        wrappedKey:(SFAuthenticatedCiphertext*)wrappedKey
                       tamperCheck:(NSString*)tamperCheck
                             error:(NSError**)error
{
...
        _serializedHolder = [[SecDbKeychainSerializedMetadata alloc] init];
        _serializedHolder.ciphertext = [NSKeyedArchiver archivedDataWithRootObject:ciphertext requiringSecureCoding:YES error:error];
        _serializedHolder.wrappedKey = [NSKeyedArchiver archivedDataWithRootObject:wrappedKey requiringSecureCoding:YES error:error];
...

    return self;
}

This object will be stored into _encryptedMetadata.

Secret data encryption #

The function encryptedBlobWithKeybag continues the encryption process, and calls encryptSecretDataWithKeybag to encrypt the secret data.

- (BOOL)encryptSecretDataWithKeybag:(keybag_handle_t)keybag accessControl:(SecAccessControlRef)accessControl acmContext:(NSData*)acmContext error:(NSError**)error
{
    /* init a new AES key */
    SFAESKey* key = [[SFAESKey alloc] initRandomKeyWithSpecifier:[self.class keySpecifier] error:error];
    if (!key) {
        return NO;
    }
    SFAuthenticatedEncryptionOperation* encryptionOperation = [self.class encryptionOperation];
    
    /* DER encode the secret data */
    NSMutableDictionary* attributesToEncrypt = _secretAttributes.mutableCopy;
    attributesToEncrypt[SecDBTamperCheck] = _tamperCheck;
    NSMutableData* secretData = [(__bridge_transfer NSData*)CFPropertyListCreateDERData(NULL, (__bridge CFDictionaryRef)attributesToEncrypt, NULL) mutableCopy];

    if (secretData.length > REASONABLE_SECRET_DATA_SIZE) {
        NSString *agrp = _metadataAttributes[(__bridge NSString *)kSecAttrAccessGroup];
        secwarning("SecDbKeychainItemV7: item's secret data exceeds reasonable size (%lu bytes) (%@)", (unsigned long)secretData.length, agrp);
    }

    /* Set padding */
    int8_t paddingLength = KEYCHAIN_ITEM_PADDING_MODULUS - (secretData.length % KEYCHAIN_ITEM_PADDING_MODULUS);
    int8_t paddingBytes[KEYCHAIN_ITEM_PADDING_MODULUS];
    for (int i = 0; i < KEYCHAIN_ITEM_PADDING_MODULUS; i++) {
        paddingBytes[i] = paddingLength;
    }
    [secretData appendBytes:paddingBytes length:paddingLength];

    /* Encrypt the secret data */
    SFAuthenticatedCiphertext* ciphertext = [encryptionOperation encrypt:secretData withKey:key error:error];

    /* Wrap the AES Key */
    SecDbKeychainAKSWrappedKey* wrappedKey = [self wrapToAKS:key withKeybag:keybag accessControl:accessControl acmContext:acmContext error:error];

    /* Create a SecDbKeychainSecretData object with encrypt secret and wrapped key */
    _encryptedSecretData = [[SecDbKeychainSecretData alloc] initWithCiphertext:ciphertext
                                                                    wrappedKey:wrappedKey
                                                                   tamperCheck:_tamperCheck
                                                                         error:error];
    return _encryptedSecretData != nil;
}

This function creates a new AES key using [SFAESKey initRandomKeyWithSpecifier] , encodes the secret data, manages the padding and encrypts the secret data using [encryptionOperation encrypt]. After that, It calls wrapToAKS to wrap the key that has been used.

- (SecDbKeychainAKSWrappedKey*)wrapToAKS:(SFAESKey*)key withKeybag:(keybag_handle_t)keybag accessControl:(SecAccessControlRef)accessControl acmContext:(NSData*)acmContext error:(NSError**)error
{
...
    NSMutableData* wrappedKey = [[NSMutableData alloc] initWithLength:APPLE_KEYSTORE_MAX_SYM_WRAPPED_KEY_LEN];
    bool success = [SecAKSObjCWrappers aksEncryptWithKeybag:keybag keyclass:_keyclass plaintext:keyData outKeyclass:&_keyclass ciphertext:wrappedKey personaId:NULL personaIdLength:0 error:error];
    return success ? [[SecDbKeychainAKSWrappedKey alloc] initRegularWrappedKeyWithData:wrappedKey] : nil;
#endif
}

The key is wrapped using the same process as for the metadata key, but this time, there is no cached key nor stored key. aksEncryptWithKeybag directly calls ks_crypt. The AppleKeyStore selector 0xa is used again to retrieve a wrapped key.

Lastly, it creates a new SecDbKeychainSecretData object. This item contains the encrypted data and the wrapped key serialised using NSKeyedArchiver archivedDataWithRootObject

@implementation SecDbKeychainSecretData {
    SecDbKeychainSerializedSecretData* _serializedHolder;
}

- (instancetype)initWithCiphertext:(SFAuthenticatedCiphertext*)ciphertext
                        wrappedKey:(SecDbKeychainAKSWrappedKey*)wrappedKey
                       tamperCheck:(NSString*)tamperCheck
                             error:(NSError**)error
{
    if (self = [super init]) {
        _serializedHolder = [[SecDbKeychainSerializedSecretData alloc] init];
        _serializedHolder.ciphertext = [NSKeyedArchiver archivedDataWithRootObject:ciphertext requiringSecureCoding:YES error:error];
        _serializedHolder.wrappedKey = wrappedKey.serializedRepresentation;
        _serializedHolder.tamperCheck = tamperCheck;
        if (!_serializedHolder.ciphertext || !_serializedHolder.wrappedKey || !_serializedHolder.tamperCheck) {
            self = nil;
        }
    }

    return self;
}

After all that processing, encryptedBlobWithKeybag returns the encrypted blob.

- (NSData*)encryptedBlobWithKeybag:(keybag_handle_t)keybag accessControl:(SecAccessControlRef)accessControl acmContext:(NSData*)acmContext error:(NSError**)error
{
...

    SecDbKeychainSerializedItemV7* serializedItem = [[SecDbKeychainSerializedItemV7 alloc] init];
    serializedItem.encryptedMetadata = self.encryptedMetadataBlob;
    serializedItem.encryptedSecretData = self.encryptedSecretDataBlob;
...
    return serializedItem.data;
}

From this, ks_encrypt_data will prefix the encrypted blob with a version and will returns it.

 if (encryptedBlob) {
            NSMutableData* encryptedBlobWithVersion = [NSMutableData dataWithLength:encryptedBlob.length + sizeof(uint32_t)];
            // This is the version we see in the db
            *((uint32_t*)encryptedBlobWithVersion.mutableBytes) = encryption_version;
            // The encrypted blob is here -> starting at blob+4
            memcpy((uint32_t*)encryptedBlobWithVersion.mutableBytes + 1, encryptedBlob.bytes, encryptedBlob.length);
            *pBlob = (__bridge_retained CFDataRef)encryptedBlobWithVersion;
            success = true;
        }

This is this blob that is stored in the Keychain-2.db on the filesystem. We can observe that the first 4 bytes in the data field of the inet table is the version.

We also observe the NSKeyedArchiver object that indicates the serialised objects.

Encryption process summary #

Let’s summarise the encryption process :

  1. securityd receives an item to store.
  2. The metadata are encrypted.
  3. The metadata key is wrapped
  4. The secret is encrypted.
  5. The secret key is wrapped.
  6. The metadata and secret items are serialised.
  7. The blob is written into the data field.

Decryption #

Now that the encryption has been explained, the decryption is easier to understand. When decrypting an item, the function ks_decrypt_data is called.

bool ks_decrypt_data(keybag_handle_t keybag, CFTypeRef cryptoOp, SecAccessControlRef *paccess_control, CFDataRef acm_context,
                     CFDataRef blob, const SecDbClass *db_class, CFArrayRef caller_access_groups,
                     CFMutableDictionaryRef *attributes_p, uint32_t *version_p, bool decryptSecretData, keyclass_t* outKeyclass, CFErrorRef *error) {

ks_decrypt_data is in charge of decrypting the stored data, and returning the secret data and the attributes as a dictionary.

Metadata decryption #

In ks_decrypt_data we see that it creates a SecDbKeychainItemV7 from the encrypted blob using the function initWithData, and It then decrypts the metadata using the function [item metadataAttributesWithError].

            SecDbKeychainItemV7* item = [[SecDbKeychainItemV7 alloc] initWithData:encryptedBlob decryptionKeybag:keybag error:&localError];
            if (outKeyclass) {
                *outKeyclass = item.keyclass;
            }

            /* Decrypt metadata attributes */
            NSMutableDictionary* itemAttributes = [[item metadataAttributesWithError:&localError] mutableCopy];

The function initWithData deserialises the previously serialised items using [NSKeyedUnarchiver unarchivedObjectOfClass] :

@implementation SecDbKeychainMetadata {
...
- (SFAuthenticatedCiphertext*)ciphertext
{
    NSError* error = nil;
    SFAuthenticatedCiphertext* ciphertext =  [NSKeyedUnarchiver unarchivedObjectOfClass:[SFAuthenticatedCiphertext class] fromData:_serializedHolder.ciphertext error:&error];
...

    return ciphertext;
}

- (SFAuthenticatedCiphertext*)wrappedKey
{
    NSError* error = nil;
    SFAuthenticatedCiphertext* wrappedKey =  [NSKeyedUnarchiver unarchivedObjectOfClass:[SFAuthenticatedCiphertext class] fromData:_serializedHolder.wrappedKey error:&error];
..

    return wrappedKey;
}
...

The SecDbKeychainItemV7 is defined as :

    @implementation SecDbKeychainItemV7 {
    SecDbKeychainSecretData* _encryptedSecretData;
    SecDbKeychainMetadata* _encryptedMetadata;
    NSDictionary* _secretAttributes;
    NSDictionary* _metadataAttributes;
    NSString* _tamperCheck;
    keyclass_t _keyclass;
    keybag_handle_t _keybag;
}

After that, we continue in ks_decrypt_data, and the metadata is decrypted using function metadataAttributesWithError

- (NSDictionary*)metadataAttributesWithError:(NSError**)error
{
    if (!_metadataAttributes) {
        SFAESKey* metadataClassKey = [self metadataClassKeyWithKeybag:_keybag
                                                          allowWrites:false
                                                                error:error];
        if (metadataClassKey) {
            NSError* localError = nil;
            NSData* keyData = [[self.class decryptionOperation] decrypt:_encryptedMetadata.wrappedKey withKey:metadataClassKey error:&localError];
...
            SFAESKey* key = [[SFAESKey alloc] initWithData:keyData specifier:[self.class keySpecifier] error:error];
...
            NSData* metadata = [[self.class decryptionOperation] decrypt:_encryptedMetadata.ciphertext withKey:key error:&localError];
...
            NSMutableDictionary* decryptedAttributes = dictionaryFromDERData(metadata).mutableCopy;
            NSString* tamperCheck = decryptedAttributes[SecDBTamperCheck];
            if ([tamperCheck isEqualToString:_encryptedMetadata.tamperCheck]) {
                [decryptedAttributes removeObjectForKey:SecDBTamperCheck];
                _metadataAttributes = decryptedAttributes;
            }
...
        }
    }

    return _metadataAttributes;
}

This function retrieves a metadataClassKey using the function metadataClassKeyWithKeybag. It is the same process as for the encryption. It tries to either retrieve the corresponding metadata wrapping key from the cache, or the database, and then it will decrypt the metadata using this key. After decryption, it returns a dictionary containing the metadata attributes.

Secret decryption #

Later in ks_decrypt_data, it’s decrypting the secret data using function secretAttributesWithAcmContext :

NSDictionary* secretAttributes = [item secretAttributesWithAcmContext:(__bridge NSData*)acm_context accessControl:access_control callerAccessGroups:(__bridge NSArray*)caller_access_groups keyDiversify:keyDiversify error:&localError];

This function will follow the same steps to decrypt the secret data, it will unwrap the secret key using unwrapFromAKS, then it will decrypt the secret data, and returns it.

- (NSDictionary*)secretAttributesWithAcmContext:(NSData*)acmContext accessControl:(SecAccessControlRef)accessControl callerAccessGroups:(NSArray*)callerAccessGroups keyDiversify:(bool)keyDiversify error:(NSError**)error
{
    if (!_secretAttributes) {
        SFAESKey* key = [self unwrapFromAKS:_encryptedSecretData.wrappedKey accessControl:accessControl acmContext:acmContext callerAccessGroups:callerAccessGroups delete:NO keyDiversify:keyDiversify error:error];
        if (key) {
            NSError* localError = nil;
            NSData* secretDataWithPadding = [[self.class decryptionOperation] decrypt:_encryptedSecretData.ciphertext withKey:key error:&localError];
            ...
            int8_t paddingLength = *((int8_t*)secretDataWithPadding.bytes + secretDataWithPadding.length - 1);
            NSData* secretDataWithoutPadding = [secretDataWithPadding subdataWithRange:NSMakeRange(0, secretDataWithPadding.length - paddingLength)];

            NSMutableDictionary* decryptedAttributes = dictionaryFromDERData(secretDataWithoutPadding).mutableCopy;
...
    return _secretAttributes;
}

Secret key unwrapping #

There is a difference here with the metadata key. As we saw for the encryption, there is no cached key here. This is why during this step, we observe a call to the AppleKeyStore kernel module with the selector 0xB. This call allows to unwrap the secret key.

Here is an exemple of what we observed using a Frida hook :

################# Unwrapping secret key using selector 0xB
IOConnectCallMethod()
		 connection : 0xe07
		Selector : 0xb
		 inputCnt + 0x2
		 input :
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  00 00                                            ..
		inputStruct 0x96aee1be0
		inputStructCnt 0x28
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  b1 fb d7 00 d3 fa 34 25 5d d1 00 49 5f b9 df 44  ......4%]..I_..D
00000010  f2 cc 7c 47 ed fd 6a 39 18 8f 2a 1f fb 40 be 51  ..|G..j9..*..@.Q
00000020  0d ed 51 c8 3a ce 11 1f                          ..Q.:...
debug : this.outputStructCnt 32
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  51 8c bf b9 60 09 b3 e7 36 7a d5 8f 9e 20 70 f6  Q...`...6z... p.
00000010  fb f9 40 88 9f 85 e3 b1 61 13 eb 76 78 3c f1 90  ..@.....a..vx<..
leaving IOConnectCallMethod(
#########

After all have been decrypted, the functions merges the decrypted metadata dictionary and the decrypted secret data dictionary into the attributes dictionary, that is retrieved to the user, or the application.

Conclusions #

In this article, we saw how items are encrypted and decrypted by securityd The Apple Keychain offers a good protection as everything is encrypted in the keychain, and the raw access to the keychain-2.db file would give nothing to an attacker. On the other hand, it could be possible, with a root access on the device (which is not the easiest thing to obtain), to retrieve the data from the database and use the AppleKeyStore kernel module to unwrap the keys used for encryption. Some parts have been omitted in this articles, like the access control, the different keychains versions and could make some articles in the futures.