| 1 | //go:build darwin && cgo |
| 2 | |
| 3 | package main |
| 4 | |
| 5 | // π€ The macOS vault KEK (docs/locked-vaults.md): 32 random bytes living in the |
| 6 | // login keychain, released to `eleven vault` only after a LocalAuthentication |
| 7 | // check β Touch ID when the Mac has it, the account password otherwise (that's |
| 8 | // LAPolicyDeviceOwnerAuthentication; a cancelled sheet is a hard failure). |
| 9 | // |
| 10 | // Honest scope: the biometric here is an AUTHENTICATION GATE in this process, |
| 11 | // plus keychain encryption at rest β not a Secure-Enclave-bound key. The |
| 12 | // enclave binding (kSecAccessControlBiometryCurrentSet in the data-protection |
| 13 | // keychain) requires a signed binary carrying an application-identifier |
| 14 | // entitlement, which a plain `go build` doesn't have β and a CLI that only |
| 15 | // works signed would break the anyone-can-build rule. Signed release builds |
| 16 | // can upgrade this to the enclave path (#950's hardening track) without |
| 17 | // changing the wrap format: the KEK is 32 raw bytes either way. |
| 18 | |
| 19 | /* |
| 20 | #cgo CFLAGS: -x objective-c -fobjc-arc |
| 21 | #cgo LDFLAGS: -framework Security -framework CoreFoundation -framework LocalAuthentication -framework Foundation |
| 22 | #include <stdlib.h> |
| 23 | #include <string.h> |
| 24 | #include <dispatch/dispatch.h> |
| 25 | #include <CoreFoundation/CoreFoundation.h> |
| 26 | #include <Security/Security.h> |
| 27 | #import <LocalAuthentication/LocalAuthentication.h> |
| 28 | |
| 29 | // eleven_authenticate runs one LocalAuthentication ceremony (Touch ID or the |
| 30 | // account password) and blocks until it resolves. 1 = authenticated. |
| 31 | static int eleven_authenticate(const char *reason, char **errOut) { |
| 32 | LAContext *ctx = [[LAContext alloc] init]; |
| 33 | NSError *canErr = nil; |
| 34 | if (![ctx canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&canErr]) { |
| 35 | if (errOut && canErr) *errOut = strdup([[canErr localizedDescription] UTF8String]); |
| 36 | return 0; |
| 37 | } |
| 38 | __block int ok = 0; |
| 39 | __block char *msg = NULL; |
| 40 | dispatch_semaphore_t sem = dispatch_semaphore_create(0); |
| 41 | [ctx evaluatePolicy:LAPolicyDeviceOwnerAuthentication |
| 42 | localizedReason:[NSString stringWithUTF8String:reason] |
| 43 | reply:^(BOOL success, NSError *error) { |
| 44 | if (success) { |
| 45 | ok = 1; |
| 46 | } else if (error) { |
| 47 | msg = strdup([[error localizedDescription] UTF8String]); |
| 48 | } |
| 49 | dispatch_semaphore_signal(sem); |
| 50 | }]; |
| 51 | dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); |
| 52 | if (!ok && errOut) { |
| 53 | *errOut = msg; |
| 54 | } else if (msg) { |
| 55 | free(msg); |
| 56 | } |
| 57 | return ok; |
| 58 | } |
| 59 | |
| 60 | static CFDictionaryRef eleven_query(const char *service, const char *account, int forAdd, CFDataRef value) { |
| 61 | CFStringRef svc = CFStringCreateWithCString(NULL, service, kCFStringEncodingUTF8); |
| 62 | CFStringRef acct = CFStringCreateWithCString(NULL, account, kCFStringEncodingUTF8); |
| 63 | CFMutableDictionaryRef q = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); |
| 64 | CFDictionarySetValue(q, kSecClass, kSecClassGenericPassword); |
| 65 | CFDictionarySetValue(q, kSecAttrService, svc); |
| 66 | CFDictionarySetValue(q, kSecAttrAccount, acct); |
| 67 | if (forAdd) { |
| 68 | CFDictionarySetValue(q, kSecValueData, value); |
| 69 | } else { |
| 70 | CFDictionarySetValue(q, kSecReturnData, kCFBooleanTrue); |
| 71 | CFDictionarySetValue(q, kSecMatchLimit, kSecMatchLimitOne); |
| 72 | } |
| 73 | CFRelease(svc); |
| 74 | CFRelease(acct); |
| 75 | return q; |
| 76 | } |
| 77 | |
| 78 | // eleven_keychain_read copies the item's bytes into a malloc'd buffer. |
| 79 | static int eleven_keychain_read(const char *service, const char *account, unsigned char **data, size_t *len) { |
| 80 | CFDictionaryRef q = eleven_query(service, account, 0, NULL); |
| 81 | CFTypeRef out = NULL; |
| 82 | OSStatus st = SecItemCopyMatching(q, &out); |
| 83 | CFRelease(q); |
| 84 | if (st != errSecSuccess) { |
| 85 | return (int)st; |
| 86 | } |
| 87 | CFDataRef d = (CFDataRef)out; |
| 88 | *len = (size_t)CFDataGetLength(d); |
| 89 | *data = (unsigned char *)malloc(*len); |
| 90 | memcpy(*data, CFDataGetBytePtr(d), *len); |
| 91 | CFRelease(out); |
| 92 | return 0; |
| 93 | } |
| 94 | |
| 95 | static int eleven_keychain_add(const char *service, const char *account, const unsigned char *data, size_t len) { |
| 96 | CFDataRef d = CFDataCreate(NULL, data, (CFIndex)len); |
| 97 | CFDictionaryRef q = eleven_query(service, account, 1, d); |
| 98 | OSStatus st = SecItemAdd(q, NULL); |
| 99 | CFRelease(q); |
| 100 | CFRelease(d); |
| 101 | return (int)st; |
| 102 | } |
| 103 | */ |
| 104 | import "C" |
| 105 | |
| 106 | import ( |
| 107 | "crypto/rand" |
| 108 | "fmt" |
| 109 | "unsafe" |
| 110 | ) |
| 111 | |
| 112 | // kekService namespaces every vault KEK this CLI keeps in the login keychain; |
| 113 | // the account (host + thread id) picks the vault. |
| 114 | const kekService = "com.elevenmessenger.eleven.vault-kek" |
| 115 | |
| 116 | const errSecItemNotFound = -25300 |
| 117 | |
| 118 | // vaultKEK returns the 32-byte KEK for one vault, behind a fresh |
| 119 | // LocalAuthentication check. With create, a missing key is minted and stored; |
| 120 | // without, missing means this machine was never enrolled. |
| 121 | func vaultKEK(account string, create bool) ([]byte, error) { |
| 122 | reason := C.CString("unlock the Eleven vault") |
| 123 | defer C.free(unsafe.Pointer(reason)) |
| 124 | var authErr *C.char |
| 125 | if C.eleven_authenticate(reason, &authErr) == 0 { |
| 126 | msg := "authentication failed" |
| 127 | if authErr != nil { |
| 128 | msg = C.GoString(authErr) |
| 129 | C.free(unsafe.Pointer(authErr)) |
| 130 | } |
| 131 | return nil, fmt.Errorf("vault unlock: %s", msg) |
| 132 | } |
| 133 | |
| 134 | service := C.CString(kekService) |
| 135 | acct := C.CString(account) |
| 136 | defer C.free(unsafe.Pointer(service)) |
| 137 | defer C.free(unsafe.Pointer(acct)) |
| 138 | |
| 139 | var data *C.uchar |
| 140 | var length C.size_t |
| 141 | switch st := C.eleven_keychain_read(service, acct, &data, &length); st { |
| 142 | case 0: |
| 143 | kek := C.GoBytes(unsafe.Pointer(data), C.int(length)) |
| 144 | C.free(unsafe.Pointer(data)) |
| 145 | if len(kek) != 32 { |
| 146 | return nil, fmt.Errorf("vault key in the keychain is %d bytes, want 32 β it may be corrupt", len(kek)) |
| 147 | } |
| 148 | return kek, nil |
| 149 | case errSecItemNotFound: |
| 150 | if !create { |
| 151 | return nil, fmt.Errorf("no vault key on this machine yet β `eleven vault enroll` requests access first") |
| 152 | } |
| 153 | default: |
| 154 | return nil, fmt.Errorf("keychain read failed (OSStatus %d)", int(st)) |
| 155 | } |
| 156 | |
| 157 | kek := make([]byte, 32) |
| 158 | if _, err := rand.Read(kek); err != nil { |
| 159 | return nil, err |
| 160 | } |
| 161 | if st := C.eleven_keychain_add(service, acct, (*C.uchar)(unsafe.Pointer(&kek[0])), C.size_t(len(kek))); st != 0 { |
| 162 | return nil, fmt.Errorf("keychain store failed (OSStatus %d)", int(st)) |
| 163 | } |
| 164 | return kek, nil |
| 165 | } |
| 166 | |