eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1//go:build darwin && cgo
2
3package 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.
31static 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
60static 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.
79static 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
95static 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*/
104import "C"
105
106import (
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.
114const kekService = "com.elevenmessenger.eleven.vault-kek"
115
116const 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.
121func 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