Skip to content

Instantly share code, notes, and snippets.

@ChiChou
Created January 7, 2026 14:05
Show Gist options
  • Select an option

  • Save ChiChou/e1a6760745229d525a890dde475c014c to your computer and use it in GitHub Desktop.

Select an option

Save ChiChou/e1a6760745229d525a890dde475c014c to your computer and use it in GitHub Desktop.
iOS KeyChain ACL parser. Code written with the help of AI
#import <Foundation/Foundation.h>
#import <Security/Security.h>
#import "Priv.h"
#define kACMKeyAclParamBioCatacombUUID "pbioc"
#define kACMKeyAclConstraintBio "cbio"
#define kACMKeyAclConstraintPolicy "cpo"
#define kACMKeyAclConstraintUserPasscode "cup"
#define kACMKeyAclConstraintWatch "cwtch"
#define kACMPolicyDeviceOwnerAuthentication "DeviceOwnerAuthentication"
#define kAKSKeyOpDecrypt "od"
#define kAKSKeyOpEncrypt "oe"
#define kAKSKeyOpDelete "odel"
#define kAKSKeyOpComputeKey "ock"
#define kAKSKeyOpSign "osgn"
#define kAKSKeyOpAttest "oa"
#define kACMKeyAclParamKofN "pkofn"
#define kAKSKeyOpKEMDecapsulate "okd"
void dump(SecAccessControlRef r) {
if (!r) return;
NSMutableArray *flags = [NSMutableArray array];
// 1. Check Application Password (stored at the top level as 'prp')
if (SecAccessControlGetRequirePassword(r)) {
[flags addObject:@"kSecAccessControlApplicationPassword"];
}
SecAccessConstraintRef c = SecAccessControlGetConstraints(r);
if (c) {
NSDictionary *dict = (__bridge NSDictionary *)c;
// 2. Determine which operation key to inspect.
// If PrivateKeyUsage is active, constraints are duplicated across osgn, ock, and okd.
// We can check 'osgn' as the representative for PrivateKeyUsage constraints.
BOOL isPrivateKey = (dict[@kAKSKeyOpAttest] != nil);
if (isPrivateKey) {
[flags addObject:@"kSecAccessControlPrivateKeyUsage"];
}
// Identify the container for the actual logic flags (UserPresence, Bio, etc.)
// Case A: Standard (Decrypt 'od')
// Case B: PrivateKey (Sign 'osgn')
// Case C: Single Constraint (Top level 'cpo', 'cup', etc. - though the C code usually nests them)
NSDictionary *opDict = dict[@kAKSKeyOpDecrypt];
if (isPrivateKey) {
opDict = dict[@kAKSKeyOpSign];
}
// If no nested opDict, the constraints might be at the top level (constraints_count == 1 case)
if (!opDict || ![opDict isKindOfClass:[NSDictionary class]]) {
opDict = dict;
}
// 3. Parse Constraints from the identified dictionary
// Device Owner Authentication / UserPresence
NSString *policy = opDict[@kACMKeyAclConstraintPolicy];
if ([policy isEqualToString:@kACMPolicyDeviceOwnerAuthentication]) {
[flags addObject:@"kSecAccessControlUserPresence"];
} else if (policy) {
[flags addObject:[NSString stringWithFormat:@"Policy: %@", policy]];
}
// Passcode
if (opDict[@kACMKeyAclConstraintUserPasscode]) {
[flags addObject:@"kSecAccessControlDevicePasscode"];
}
// Biometrics
NSDictionary *cbio = opDict[@kACMKeyAclConstraintBio];
if (cbio) {
// BiometryCurrentSet has both pbioc (catacomb) and pbioh (hash/history)
// BiometryAny usually just has pbioc
[flags addObject:(cbio.count > 1 ? @"kSecAccessControlBiometryCurrentSet" : @"kSecAccessControlBiometryAny")];
}
// Companion (Watch)
if (opDict[@kACMKeyAclConstraintWatch]) {
[flags addObject:@"kSecAccessControlCompanion"];
}
// Logic Operators (pkofn)
NSNumber *pkofn = opDict[@kACMKeyAclParamKofN];
if (pkofn) {
// k-of-n: if k=1, it's an OR. If k=N, it's an AND.
if (pkofn.intValue == 1) {
[flags addObject:@"kSecAccessControlOr"];
} else {
[flags addObject:@"kSecAccessControlAnd"];
}
}
CFShow(c);
}
NSLog(@"parsed flags: %@", flags);
CFRelease(r);
}
int main(int argc, const char * argv[]) {
CFErrorRef error = NULL;
CFAllocatorRef alloc = kCFAllocatorDefault;
CFTypeRef protection = kSecAttrAccessibleAfterFirstUnlock;
// 1. Basic Presence & Biometrics
@autoreleasepool {
puts("--- kSecAccessControlUserPresence ---");
dump(SecAccessControlCreateWithFlags(alloc, protection, kSecAccessControlUserPresence, &error));
}
@autoreleasepool {
puts("--- kSecAccessControlBiometryAny ---");
dump(SecAccessControlCreateWithFlags(alloc, protection, kSecAccessControlBiometryAny, &error));
}
@autoreleasepool {
puts("--- kSecAccessControlBiometryCurrentSet ---");
dump(SecAccessControlCreateWithFlags(alloc, protection, kSecAccessControlBiometryCurrentSet, &error));
}
// 2. Device & Companion
@autoreleasepool {
puts("--- kSecAccessControlDevicePasscode ---");
dump(SecAccessControlCreateWithFlags(alloc, protection, kSecAccessControlDevicePasscode, &error));
}
@autoreleasepool {
puts("--- kSecAccessControlCompanion (Watch) ---");
dump(SecAccessControlCreateWithFlags(alloc, protection, kSecAccessControlCompanion, &error));
}
// 3. Logic Operators (Or / And)
@autoreleasepool {
puts("--- kSecAccessControlUserPresence | kSecAccessControlOr (Standard UserPresence) ---");
dump(SecAccessControlCreateWithFlags(alloc, protection, kSecAccessControlDevicePasscode | kSecAccessControlBiometryAny | kSecAccessControlOr, &error));
}
@autoreleasepool {
puts("--- kSecAccessControlDevicePasscode | kSecAccessControlBiometryAny | kSecAccessControlAnd ---");
dump(SecAccessControlCreateWithFlags(alloc, protection, kSecAccessControlDevicePasscode | kSecAccessControlBiometryAny | kSecAccessControlAnd, &error));
}
// 4. Usage Flags
@autoreleasepool {
puts("--- kSecAccessControlPrivateKeyUsage ---");
dump(SecAccessControlCreateWithFlags(alloc, protection, kSecAccessControlPrivateKeyUsage, &error));
}
@autoreleasepool {
puts("--- kSecAccessControlApplicationPassword ---");
dump(SecAccessControlCreateWithFlags(alloc, protection, kSecAccessControlApplicationPassword, &error));
}
// 5. Complex Combinations
@autoreleasepool {
puts("--- Combined: PrivateKeyUsage | BiometryAny | DevicePasscode | And ---");
dump(SecAccessControlCreateWithFlags(alloc, protection,
kSecAccessControlPrivateKeyUsage | kSecAccessControlBiometryAny | kSecAccessControlDevicePasscode | kSecAccessControlAnd, &error));
}
return EXIT_SUCCESS;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment