-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdb_key.h
More file actions
319 lines (292 loc) Β· 12.9 KB
/
Copy pathdb_key.h
File metadata and controls
319 lines (292 loc) Β· 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
// db_key.h β File-based SQLCipher database encryption key management
//
// PURPOSE
// Provides a single 256-bit (32-byte hex) AES key used to encrypt all SQLite
// databases in the project via SQLCipher. The key is generated once, stored
// in a file (db_key.hex) next to the executable, and retrieved on subsequent
// startups. On macOS, if no file is found, a one-time migration from the
// legacy macOS Keychain storage is attempted before generating a new key.
//
// USAGE
// #include "db_key.h"
// ...
// sqlite3* db = nullptr;
// prodigy_db::db_open_encrypted("/path/to/db.sqlite", &db);
// // Use db normally β all reads/writes are transparently AES-256 encrypted.
//
// SECURITY PROPERTIES
// β’ AES-256-CBC page-level encryption via SQLCipher (OpenSSL 3.x backend).
// β’ Key material (32 bytes) generated with arc4random_buf() β a CSPRNG seeded
// by the macOS kernel; each device gets a unique key on first run.
// β’ Key stored in db_key.hex with mode 0600 (owner read/write only).
// β’ fsync() is called after writing to prevent key loss on crash.
// β’ SQLITE_TEMP_STORE=2 compiled in: temp tables go to memory, never to disk
// in plaintext.
// β’ Migration: existing plaintext SQLite databases are encrypted in-place via
// sqlite3_rekey() the first time they are opened with this helper.
//
// APPLE SILICON PERFORMANCE NOTES
// Apple M1/M2/M3/M4 chips implement the ARMv8.2 Cryptography Extension with
// hardware AES acceleration. OpenSSL 3.x detects and uses this automatically
// via the AES instruction set (AESE/AESMC), so SQLCipher encryption overhead
// is negligible (< 3% for typical database workloads on M-series hardware).
//
// CROSS-PROCESS COMPATIBILITY
// Both `frontend` and `tomedo-crawl` open the shared tomedo-crawl.db file.
// Because both processes read the same db_key.hex file, they use an identical
// key and can interoperate with full SQLite WAL multi-reader concurrency.
//
// THREAD SAFETY
// get_db_key() is safe to call from multiple threads β a std::once_flag
// ensures the key file is read exactly once per process lifetime.
#pragma once
#include <cstdio>
#include <cstring>
#include <mutex>
#include <string>
#include <fcntl.h>
#include <unistd.h>
#ifdef __APPLE__
# include <Security/Security.h>
# include <mach-o/dyld.h>
# include <stdlib.h> // arc4random_buf
#endif
// These defines must be set before including sqlite3.h so that SQLCipher's
// sqlite3_key() / sqlite3_rekey() declarations become visible.
// They are also passed as -D flags in CMakeLists.txt for sqlite3.c itself.
#ifndef SQLITE_HAS_CODEC
# define SQLITE_HAS_CODEC
#endif
#include "sqlite3.h"
namespace prodigy_db {
// βββ Secure memory zeroing ββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Writes zeros to a memory region in a way the compiler cannot optimise away.
// Use this for key material and sensitive buffers before they go out of scope.
// portable β works on every C++17 platform without __STDC_WANT_LIB_EXT1__.
static inline void secure_zero(void* ptr, std::size_t len) noexcept
{
volatile unsigned char* p = static_cast<volatile unsigned char*>(ptr);
while (len--) *p++ = 0;
}
// βββ Internal constants ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
static constexpr size_t HEX_KEY_LEN = 64;
static const char* KEYCHAIN_SERVICE = "com.prodigy.db_encryption";
static const char* KEYCHAIN_ACCOUNT = "db_key";
// βββ Key state ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
static std::string g_cached_key;
static std::once_flag g_key_once;
// βββ Key generation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Returns a 64-character lowercase hex string (256-bit key).
static std::string generate_hex_key()
{
unsigned char raw[32];
#ifdef __APPLE__
arc4random_buf(raw, sizeof(raw)); // Kernel CSPRNG, always available
#else
FILE* f = std::fopen("/dev/urandom", "rb");
if (!f || std::fread(raw, 1, sizeof(raw), f) != sizeof(raw)) {
if (f) std::fclose(f);
std::fprintf(stderr, "[db_key] FATAL: cannot read /dev/urandom\n");
secure_zero(raw, sizeof(raw));
return {};
}
std::fclose(f);
#endif
char hex[HEX_KEY_LEN + 1];
for (int i = 0; i < 32; ++i) std::snprintf(hex + 2 * i, 3, "%02x", raw[i]);
hex[HEX_KEY_LEN] = '\0';
std::string result(hex, HEX_KEY_LEN);
// Zero sensitive key material from stack before returning.
secure_zero(raw, sizeof(raw));
secure_zero(hex, sizeof(hex));
return result;
}
// βββ Keychain helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
static std::string keychain_load()
{
#ifdef __APPLE__
CFStringRef cfSvc = CFStringCreateWithCString(nullptr, KEYCHAIN_SERVICE, kCFStringEncodingUTF8);
CFStringRef cfAcc = CFStringCreateWithCString(nullptr, KEYCHAIN_ACCOUNT, kCFStringEncodingUTF8);
CFMutableDictionaryRef q = CFDictionaryCreateMutable(
nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(q, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(q, kSecAttrService, cfSvc);
CFDictionarySetValue(q, kSecAttrAccount, cfAcc);
CFDictionarySetValue(q, kSecReturnData, kCFBooleanTrue);
CFDictionarySetValue(q, kSecMatchLimit, kSecMatchLimitOne);
CFDataRef data = nullptr;
OSStatus st = SecItemCopyMatching(q, reinterpret_cast<CFTypeRef*>(&data));
CFRelease(q);
CFRelease(cfSvc);
CFRelease(cfAcc);
if (st == errSecSuccess && data) {
std::string key(reinterpret_cast<const char*>(CFDataGetBytePtr(data)),
static_cast<std::size_t>(CFDataGetLength(data)));
CFRelease(data);
return key;
}
if (data) CFRelease(data);
#endif
return {};
}
static std::string db_key_file_path()
{
#ifdef __APPLE__
char path[4096];
uint32_t size = sizeof(path);
if (_NSGetExecutablePath(path, &size) == 0) {
char* last_slash = strrchr(path, '/');
if (last_slash) { *last_slash = '\0'; }
return std::string(path) + "/db_key.hex";
}
#endif
return "./db_key.hex";
}
static std::string file_key_load()
{
std::string path = db_key_file_path();
FILE* f = std::fopen(path.c_str(), "r");
if (!f) return {};
char buf[128] = {0};
size_t n = std::fread(buf, 1, sizeof(buf) - 1, f);
std::fclose(f);
while (n > 0 && (buf[n-1] == '\n' || buf[n-1] == '\r' || buf[n-1] == ' '))
buf[--n] = '\0';
std::string result;
if (n == HEX_KEY_LEN) result.assign(buf, HEX_KEY_LEN);
secure_zero(buf, sizeof(buf));
return result;
}
static bool file_key_store(const std::string& key)
{
std::string path = db_key_file_path();
int fd = open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fd < 0) return false;
ssize_t w = write(fd, key.c_str(), key.size());
if (w != (ssize_t)key.size()) { close(fd); return false; }
fsync(fd);
close(fd);
return true;
}
// βββ Public API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
static const std::string& get_db_key()
{
std::call_once(g_key_once, []() {
g_cached_key = file_key_load();
if (!g_cached_key.empty()) {
std::fprintf(stderr,
"[db_key] Loaded AES-256 database key from file (%s)\n",
db_key_file_path().c_str());
return;
}
#ifdef __APPLE__
g_cached_key = keychain_load();
if (!g_cached_key.empty()) {
std::fprintf(stderr,
"[db_key] Migrating key from macOS Keychain to file (%s)\n",
db_key_file_path().c_str());
if (!file_key_store(g_cached_key)) {
std::fprintf(stderr,
"[db_key] WARNING: failed to persist migrated key to disk\n");
}
return;
}
#endif
g_cached_key = generate_hex_key();
if (g_cached_key.empty()) {
std::fprintf(stderr, "[db_key] FATAL: key generation failed\n");
return;
}
if (!file_key_store(g_cached_key)) {
std::fprintf(stderr,
"[db_key] FATAL: could not persist key to %s β "
"database will be unreadable on next startup\n",
db_key_file_path().c_str());
g_cached_key.clear();
return;
}
std::fprintf(stderr,
"[db_key] Generated new 256-bit AES database key and stored in file (%s)\n",
db_key_file_path().c_str());
});
return g_cached_key;
}
// Drop-in replacement for sqlite3_open().
//
// Opens `path` and applies the SQLCipher AES-256 encryption key immediately.
// Handles three cases:
// 1. New database (file does not exist yet) β created encrypted from scratch.
// 2. Existing encrypted database β key applied, ready to use.
// 3. Existing legacy plaintext database β encrypted in-place via
// sqlite3_rekey() and a log
// message is emitted.
//
// Returns SQLITE_OK on success or a SQLite error code on open failure.
// On SQLITE_OK the caller owns *db and must call sqlite3_close() when done.
static inline int db_open_encrypted(const char* path, sqlite3** db)
{
int rc = sqlite3_open_v2(path, db,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,
nullptr);
if (rc != SQLITE_OK) return rc;
const std::string& key = get_db_key();
if (key.empty()) {
std::fprintf(stderr,
"[db_key] Warning: no encryption key available β %s is UNENCRYPTED\n", path);
return SQLITE_OK;
}
// Apply key β on a new/encrypted DB this succeeds immediately.
sqlite3_key(*db, key.c_str(), static_cast<int>(key.size()));
// Probe: verify the key is correct (or the DB is freshly created/empty).
sqlite3_stmt* stmt = nullptr;
rc = sqlite3_prepare_v2(*db, "SELECT count(*) FROM sqlite_master", -1, &stmt, nullptr);
if (rc == SQLITE_OK) {
sqlite3_finalize(stmt);
return SQLITE_OK; // Correctly keyed or brand new
}
sqlite3_finalize(stmt);
// Probe failed β the DB might be a legacy plaintext SQLite file.
// Close and re-open without a key to check.
sqlite3_close(*db);
*db = nullptr;
rc = sqlite3_open_v2(path, db,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,
nullptr);
if (rc != SQLITE_OK) return rc;
stmt = nullptr;
rc = sqlite3_prepare_v2(*db, "SELECT count(*) FROM sqlite_master", -1, &stmt, nullptr);
if (rc == SQLITE_OK) {
sqlite3_finalize(stmt);
// DB is readable as plaintext β migrate in-place.
//
// Flush any existing WAL file into the main database file first.
// If the DB was previously opened in WAL mode, the *.db-wal file may
// contain unwritten pages that would be left in plaintext on disk after
// sqlite3_rekey() rewrites the main file. Checkpointing guarantees all
// data is captured in the encrypted rewrite.
sqlite3_exec(*db, "PRAGMA wal_checkpoint(TRUNCATE)", nullptr, nullptr, nullptr);
int rkey_rc = sqlite3_rekey(*db, key.c_str(), static_cast<int>(key.size()));
if (rkey_rc == SQLITE_OK) {
std::fprintf(stderr,
"[db_key] Migrated plaintext database to SQLCipher AES-256 encryption: %s\n",
path);
} else {
std::fprintf(stderr,
"[db_key] Warning: in-place encryption of %s failed (rc=%d) β "
"database remains plaintext\n", path, rkey_rc);
}
return SQLITE_OK;
}
sqlite3_finalize(stmt);
// Neither correctly keyed nor readable as plaintext β file is corrupted or
// was created by an incompatible SQLCipher version. Close the handle and
// return an error so callers can surface a meaningful message rather than
// proceeding with a broken database.
std::fprintf(stderr,
"[db_key] Error: %s is neither a correctly-keyed SQLCipher database nor "
"a readable plaintext SQLite file β possible corruption or key mismatch\n", path);
sqlite3_close(*db);
*db = nullptr;
return SQLITE_NOTADB;
}
} // namespace prodigy_db