phylax 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1024 @@
1
+ /*
2
+ * phylax — misuse-resistant cryptography for Ruby, backed by the Windows CNG
3
+ * primitive provider (bcrypt.dll) and DPAPI (crypt32.dll).
4
+ *
5
+ * This is a PURE C extension (no C++), so rb_raise/longjmp is the normal, safe
6
+ * error mechanism — none of the /EHsc unwinder hazard that the lithos C++
7
+ * engine had to architect around.
8
+ *
9
+ * It implements no cryptography of its own. Every primitive is a thin marshal
10
+ * of Ruby String bytes into a CNG / DPAPI call and back. Algorithm provider
11
+ * handles are opened once at load and cached for the life of the process
12
+ * (CNG algorithm handles are documented thread-safe); they are never closed.
13
+ */
14
+
15
+ #include <ruby.h>
16
+ #include <ruby/thread.h>
17
+ #include <ruby/encoding.h>
18
+ #include <limits.h>
19
+
20
+ /* ruby.h already pulls in <windows.h>/<winnt.h>, which define NTSTATUS,
21
+ * STATUS_SUCCESS and NT_SUCCESS. We only need the CNG/DPAPI declarations on top
22
+ * of that, plus STATUS_AUTH_TAG_MISMATCH (which lives in ntstatus.h and is
23
+ * defined locally below to avoid the winnt.h/ntstatus.h redefinition warnings). */
24
+ #include <windows.h>
25
+ #include <wincrypt.h> /* DATA_BLOB */
26
+ #include <bcrypt.h> /* CNG primitive provider */
27
+ #include <dpapi.h> /* CryptProtectData / CryptUnprotectData */
28
+
29
+ #ifndef STATUS_SUCCESS
30
+ # define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
31
+ #endif
32
+ #ifndef STATUS_AUTH_TAG_MISMATCH
33
+ # define STATUS_AUTH_TAG_MISMATCH ((NTSTATUS)0xC000A002L)
34
+ #endif
35
+ #ifndef NT_SUCCESS
36
+ # define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
37
+ #endif
38
+
39
+ /* ------------------------------------------------------------------ globals */
40
+
41
+ static VALUE mPhylax;
42
+ static VALUE eError; /* Phylax::Error < StandardError */
43
+ static VALUE eAuthError; /* Phylax::AuthenticationError < Phylax::Error */
44
+ static VALUE eOSError; /* Phylax::OSError < Phylax::Error */
45
+ static VALUE cDigest; /* Phylax::Digest */
46
+ static VALUE cHMAC; /* Phylax::HMAC */
47
+ static VALUE cSecretBox; /* Phylax::SecretBox */
48
+
49
+ /* Three SHA-2 variants, indexed 0/1/2 throughout (sha256/sha384/sha512). */
50
+ #define N_ALG 3
51
+ static BCRYPT_ALG_HANDLE g_hash[N_ALG]; /* plain hash providers */
52
+ static BCRYPT_ALG_HANDLE g_hmac[N_ALG]; /* HMAC-promoted providers */
53
+ static BCRYPT_ALG_HANDLE g_aes_gcm; /* AES provider switched to GCM mode */
54
+ static ULONG g_dlen[N_ALG]; /* digest length (32/48/64) */
55
+ static ULONG g_objlen_hash[N_ALG]; /* BCRYPT_OBJECT_LENGTH, hash */
56
+ static ULONG g_objlen_hmac[N_ALG]; /* BCRYPT_OBJECT_LENGTH, hmac */
57
+
58
+ #define SECRETBOX_KEY_BYTES 32u
59
+ #define SECRETBOX_NONCE_BYTES 12u
60
+ #define SECRETBOX_TAG_BYTES 16u
61
+
62
+ /* ------------------------------------------------------------- error raising */
63
+
64
+ /* Build (but do not raise) a Phylax error carrying @api and @code for triage. */
65
+ static VALUE
66
+ make_exc(VALUE klass, const char *api, unsigned long code, const char *what)
67
+ {
68
+ VALUE msg = rb_sprintf("%s: %s (0x%08lX)", api, what, code);
69
+ VALUE exc = rb_exc_new_str(klass, msg);
70
+ rb_iv_set(exc, "@api", rb_str_new_cstr(api));
71
+ rb_iv_set(exc, "@code", ULONG2NUM(code));
72
+ return exc;
73
+ }
74
+
75
+ /* Map a failing NTSTATUS to an exception. A GCM tag mismatch is an
76
+ * authentication failure; everything else is an OS error. Caller must have
77
+ * already released any native resources in this frame (longjmp follows). */
78
+ static void
79
+ raise_nt(const char *api, NTSTATUS st)
80
+ {
81
+ if (st == STATUS_AUTH_TAG_MISMATCH) {
82
+ rb_exc_raise(make_exc(eAuthError, api,
83
+ (unsigned long)(ULONG)st,
84
+ "ciphertext failed authentication"));
85
+ }
86
+ rb_exc_raise(make_exc(eOSError, api, (unsigned long)(ULONG)st, "NTSTATUS failure"));
87
+ }
88
+
89
+ /* Map a Win32 GetLastError() to an exception. For DPAPI unprotect, integrity /
90
+ * credential failures are authentication errors; the rest are OS errors. */
91
+ static void
92
+ raise_gle(const char *api, DWORD code, int integrity_is_auth)
93
+ {
94
+ if (integrity_is_auth &&
95
+ (code == ERROR_INVALID_DATA || /* 13 tamper / wrong entropy */
96
+ code == ERROR_INVALID_PARAMETER || /* 87 corrupted blob */
97
+ code == (DWORD)NTE_BAD_DATA || /* 0x80090005 */
98
+ code == (DWORD)NTE_BAD_KEY)) { /* 0x80090003 */
99
+ rb_exc_raise(make_exc(eAuthError, api, code,
100
+ "data could not be authenticated / decrypted"));
101
+ }
102
+ rb_exc_raise(make_exc(eOSError, api, code, "Windows API failure"));
103
+ }
104
+
105
+ /* StringValue + a guard that the byte length fits the ULONG-typed CNG params.
106
+ * Returns the byte length; writes the data pointer through *pp. Use only where
107
+ * there is NO Ruby allocation between this call and the native call that
108
+ * consumes the pointer (otherwise a compacting GC could move an embedded
109
+ * string and dangle *pp — see ulen()/RSTRING_PTR-late idiom below). */
110
+ static ULONG
111
+ str_bytes(VALUE *v, PUCHAR *pp)
112
+ {
113
+ long n;
114
+ StringValue(*v);
115
+ n = RSTRING_LEN(*v);
116
+ if ((unsigned long long)n > 0xFFFFFFFFull)
117
+ rb_raise(rb_eArgError, "phylax: input too large (> 4 GiB)");
118
+ *pp = (PUCHAR)RSTRING_PTR(*v);
119
+ return (ULONG)n;
120
+ }
121
+
122
+ /* Coerce-and-measure without taking the data pointer. RSTRING_LEN is stable
123
+ * across GC (compaction never changes a string's length), so length may be
124
+ * read early; RSTRING_PTR must be fetched only after the last Ruby allocation,
125
+ * immediately before the native call. */
126
+ static ULONG
127
+ ulen(VALUE v)
128
+ {
129
+ long n;
130
+ Check_Type(v, T_STRING);
131
+ n = RSTRING_LEN(v);
132
+ if ((unsigned long long)n > 0xFFFFFFFFull)
133
+ rb_raise(rb_eArgError, "phylax: input too large (> 4 GiB)");
134
+ return (ULONG)n;
135
+ }
136
+
137
+ static int
138
+ alg_index(VALUE idx)
139
+ {
140
+ int i = NUM2INT(idx);
141
+ if (i < 0 || i >= N_ALG)
142
+ rb_raise(rb_eArgError, "phylax: invalid algorithm index %d", i);
143
+ return i;
144
+ }
145
+
146
+ /* rb_protect body: copy a native buffer into a fresh String. Used so the
147
+ * native source buffer can be wiped/freed unconditionally even if the String
148
+ * allocation raises (NoMemoryError) — see strcopy_or_cleanup callers. */
149
+ struct strcopy { const char *ptr; long len; };
150
+
151
+ static VALUE
152
+ strcopy_body(VALUE a)
153
+ {
154
+ struct strcopy *s = (struct strcopy *)a;
155
+ return rb_str_new(s->ptr, s->len);
156
+ }
157
+
158
+ /* --------------------------------------------------------------- random ---- */
159
+
160
+ /* Phylax.random_bytes(n) -> String (BINARY, length n) */
161
+ static VALUE
162
+ phylax_random_bytes(VALUE self, VALUE rn)
163
+ {
164
+ long n = NUM2LONG(rn);
165
+ VALUE out;
166
+ NTSTATUS st;
167
+
168
+ if (n < 0)
169
+ rb_raise(rb_eArgError, "phylax: negative length");
170
+ if ((unsigned long long)n > 0xFFFFFFFFull)
171
+ rb_raise(rb_eArgError, "phylax: length too large (> 4 GiB)");
172
+
173
+ out = rb_str_new(NULL, n);
174
+ rb_enc_associate(out, rb_ascii8bit_encoding());
175
+ if (n == 0)
176
+ return out;
177
+
178
+ st = BCryptGenRandom(NULL, (PUCHAR)RSTRING_PTR(out), (ULONG)n,
179
+ BCRYPT_USE_SYSTEM_PREFERRED_RNG);
180
+ if (!NT_SUCCESS(st))
181
+ raise_nt("BCryptGenRandom", st); /* never returns the zeroed buffer */
182
+
183
+ return out;
184
+ }
185
+
186
+ /* --------------------------------------------------------------- hashing --- */
187
+
188
+ /* Phylax.__hash(idx, data) -> raw digest String (one-shot). */
189
+ static VALUE
190
+ phylax_hash(VALUE self, VALUE vidx, VALUE data)
191
+ {
192
+ int i = alg_index(vidx);
193
+ PUCHAR pin, pout;
194
+ ULONG nin;
195
+ BCRYPT_HASH_HANDLE h = NULL;
196
+ NTSTATUS st;
197
+ VALUE out;
198
+
199
+ StringValue(data);
200
+ nin = ulen(data);
201
+ out = rb_str_new(NULL, g_dlen[i]);
202
+ rb_enc_associate(out, rb_ascii8bit_encoding());
203
+ pin = (PUCHAR)RSTRING_PTR(data); /* pointers fetched after the last alloc */
204
+ pout = (PUCHAR)RSTRING_PTR(out);
205
+
206
+ st = BCryptCreateHash(g_hash[i], &h, NULL, 0, NULL, 0, 0);
207
+ if (NT_SUCCESS(st)) {
208
+ st = BCryptHashData(h, pin, nin, 0);
209
+ if (NT_SUCCESS(st))
210
+ st = BCryptFinishHash(h, pout, g_dlen[i], 0);
211
+ }
212
+ if (h) BCryptDestroyHash(h);
213
+ if (!NT_SUCCESS(st))
214
+ raise_nt("BCryptHash", st);
215
+
216
+ return out;
217
+ }
218
+
219
+ /* Phylax.__hmac(idx, key, data) -> raw MAC String (one-shot). */
220
+ static VALUE
221
+ phylax_hmac(VALUE self, VALUE vidx, VALUE key, VALUE data)
222
+ {
223
+ int i = alg_index(vidx);
224
+ PUCHAR pkey, pin, pout;
225
+ ULONG nkey, nin;
226
+ BCRYPT_HASH_HANDLE h = NULL;
227
+ NTSTATUS st;
228
+ VALUE out;
229
+
230
+ StringValue(key);
231
+ StringValue(data);
232
+ nkey = ulen(key);
233
+ nin = ulen(data);
234
+ out = rb_str_new(NULL, g_dlen[i]);
235
+ rb_enc_associate(out, rb_ascii8bit_encoding());
236
+ pkey = (PUCHAR)RSTRING_PTR(key); /* pointers fetched after the last alloc */
237
+ pin = (PUCHAR)RSTRING_PTR(data);
238
+ pout = (PUCHAR)RSTRING_PTR(out);
239
+
240
+ st = BCryptCreateHash(g_hmac[i], &h, NULL, 0, pkey, nkey, 0);
241
+ if (NT_SUCCESS(st)) {
242
+ st = BCryptHashData(h, pin, nin, 0);
243
+ if (NT_SUCCESS(st))
244
+ st = BCryptFinishHash(h, pout, g_dlen[i], 0);
245
+ }
246
+ if (h) BCryptDestroyHash(h);
247
+ if (!NT_SUCCESS(st))
248
+ raise_nt("BCryptHash(HMAC)", st);
249
+
250
+ return out;
251
+ }
252
+
253
+ /* ---------------------------------------------------------------- PBKDF2 --- */
254
+
255
+ struct pbkdf2_job {
256
+ BCRYPT_ALG_HANDLE hPrf;
257
+ PUCHAR pw; ULONG pwlen;
258
+ PUCHAR salt; ULONG saltlen;
259
+ ULONGLONG iters;
260
+ PUCHAR out; ULONG outlen;
261
+ NTSTATUS st;
262
+ };
263
+
264
+ static void *
265
+ pbkdf2_nogvl(void *p)
266
+ {
267
+ struct pbkdf2_job *j = (struct pbkdf2_job *)p;
268
+ j->st = BCryptDeriveKeyPBKDF2(j->hPrf, j->pw, j->pwlen, j->salt, j->saltlen,
269
+ j->iters, j->out, j->outlen, 0);
270
+ return NULL;
271
+ }
272
+
273
+ /* Phylax.__pbkdf2(idx, password, salt, iterations, length) -> derived key.
274
+ * Password/salt/output are copied into private heap buffers so the GVL can be
275
+ * released for the (potentially long) derivation without exposing the Ruby
276
+ * strings to GC compaction. Secret buffers are wiped before free. */
277
+ static VALUE
278
+ phylax_pbkdf2(VALUE self, VALUE vidx, VALUE password, VALUE salt,
279
+ VALUE viters, VALUE vlen)
280
+ {
281
+ int i = alg_index(vidx);
282
+ PUCHAR ppw, psalt;
283
+ ULONG npw, nsalt;
284
+ long len = NUM2LONG(vlen);
285
+ long long signed_iters = NUM2LL(viters); /* RangeError if it overflows int64 */
286
+ unsigned long long iters;
287
+ struct pbkdf2_job job;
288
+ VALUE out;
289
+
290
+ /* Validate in C too: __pbkdf2 is reachable directly, bypassing the Ruby
291
+ * guards, and a negative count would wrap (via the old NUM2ULL) into a
292
+ * ~2^64 iteration run that spins uninterruptibly under the released GVL. */
293
+ if (signed_iters < 1)
294
+ rb_raise(rb_eArgError, "phylax: iterations must be >= 1");
295
+ iters = (unsigned long long)signed_iters;
296
+
297
+ if (len < 1)
298
+ rb_raise(rb_eArgError, "phylax: length must be >= 1");
299
+ if ((unsigned long long)len > 0xFFFFFFFFull)
300
+ rb_raise(rb_eArgError, "phylax: length too large (> 4 GiB)");
301
+
302
+ StringValue(password);
303
+ StringValue(salt);
304
+ npw = ulen(password);
305
+ nsalt = ulen(salt);
306
+ ppw = (PUCHAR)RSTRING_PTR(password); /* copied into heap below, before any */
307
+ psalt = (PUCHAR)RSTRING_PTR(salt); /* further Ruby allocation */
308
+
309
+ job.hPrf = g_hmac[i];
310
+ job.pwlen = npw;
311
+ job.saltlen = nsalt;
312
+ job.iters = iters;
313
+ job.outlen = (ULONG)len;
314
+ job.pw = (PUCHAR)malloc(npw ? npw : 1);
315
+ job.salt = (PUCHAR)malloc(nsalt ? nsalt : 1);
316
+ job.out = (PUCHAR)malloc((size_t)len);
317
+ if (!job.pw || !job.salt || !job.out) {
318
+ free(job.pw); free(job.salt); free(job.out);
319
+ rb_raise(rb_eNoMemError, "phylax: out of memory in pbkdf2");
320
+ }
321
+ memcpy(job.pw, ppw, npw);
322
+ memcpy(job.salt, psalt, nsalt);
323
+
324
+ /* NULL ubf: the derivation is bounded CPU work that cannot be unblocked
325
+ * mid-call, so it runs to completion rather than pretending to interrupt. */
326
+ rb_thread_call_without_gvl(pbkdf2_nogvl, &job, NULL, NULL);
327
+
328
+ SecureZeroMemory(job.pw, npw);
329
+ free(job.pw);
330
+ free(job.salt);
331
+
332
+ if (!NT_SUCCESS(job.st)) {
333
+ SecureZeroMemory(job.out, (size_t)len);
334
+ free(job.out);
335
+ raise_nt("BCryptDeriveKeyPBKDF2", job.st);
336
+ }
337
+
338
+ /* Wipe and free the derived key even if the String allocation raises (OOM). */
339
+ {
340
+ struct strcopy sc = { (const char *)job.out, len };
341
+ int state = 0;
342
+ out = rb_protect(strcopy_body, (VALUE)&sc, &state);
343
+ SecureZeroMemory(job.out, (size_t)len);
344
+ free(job.out);
345
+ if (state) rb_jump_tag(state);
346
+ }
347
+ rb_enc_associate(out, rb_ascii8bit_encoding());
348
+ return out;
349
+ }
350
+
351
+ /* -------------------------------------------------------- secure_compare --- */
352
+
353
+ /* Phylax.secure_compare(a, b) -> true/false. Constant time w.r.t. content for
354
+ * equal-length inputs; short-circuits false on differing length (length is not
355
+ * secret for tag comparison — documented). */
356
+ static VALUE
357
+ phylax_secure_compare(VALUE self, VALUE a, VALUE b)
358
+ {
359
+ long la, lb, i;
360
+ const volatile unsigned char *pa, *pb;
361
+ volatile unsigned char acc = 0;
362
+
363
+ StringValue(a);
364
+ StringValue(b);
365
+ la = RSTRING_LEN(a);
366
+ lb = RSTRING_LEN(b);
367
+ if (la != lb)
368
+ return Qfalse;
369
+
370
+ pa = (const volatile unsigned char *)RSTRING_PTR(a);
371
+ pb = (const volatile unsigned char *)RSTRING_PTR(b);
372
+ for (i = 0; i < la; i++)
373
+ acc |= (unsigned char)(pa[i] ^ pb[i]);
374
+
375
+ return acc == 0 ? Qtrue : Qfalse;
376
+ }
377
+
378
+ /* ===================================================================
379
+ * Streaming Digest — Phylax::Digest
380
+ * =================================================================== */
381
+
382
+ typedef struct {
383
+ BCRYPT_HASH_HANDLE h;
384
+ int idx;
385
+ } digest_t;
386
+
387
+ static void
388
+ digest_free(void *p)
389
+ {
390
+ digest_t *d = (digest_t *)p;
391
+ if (d->h) BCryptDestroyHash(d->h);
392
+ xfree(d);
393
+ }
394
+
395
+ static size_t
396
+ digest_memsize(const void *p)
397
+ {
398
+ (void)p;
399
+ return sizeof(digest_t);
400
+ }
401
+
402
+ static const rb_data_type_t digest_type = {
403
+ "Phylax::Digest",
404
+ { 0, digest_free, digest_memsize, },
405
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
406
+ };
407
+
408
+ static VALUE
409
+ digest_alloc(VALUE klass)
410
+ {
411
+ digest_t *d;
412
+ VALUE obj = TypedData_Make_Struct(klass, digest_t, &digest_type, d);
413
+ d->h = NULL;
414
+ d->idx = -1;
415
+ return obj;
416
+ }
417
+
418
+ static digest_t *
419
+ digest_get(VALUE self)
420
+ {
421
+ digest_t *d;
422
+ TypedData_Get_Struct(self, digest_t, &digest_type, d);
423
+ return d;
424
+ }
425
+
426
+ /* Phylax::Digest#_init(idx) — (re)create the underlying hash object. */
427
+ static VALUE
428
+ digest_init(VALUE self, VALUE vidx)
429
+ {
430
+ digest_t *d = digest_get(self);
431
+ int i = alg_index(vidx);
432
+ NTSTATUS st;
433
+
434
+ if (d->h) { BCryptDestroyHash(d->h); d->h = NULL; }
435
+ st = BCryptCreateHash(g_hash[i], &d->h, NULL, 0, NULL, 0, 0);
436
+ if (!NT_SUCCESS(st))
437
+ raise_nt("BCryptCreateHash", st);
438
+ d->idx = i;
439
+ return self;
440
+ }
441
+
442
+ static VALUE
443
+ digest_update(VALUE self, VALUE data)
444
+ {
445
+ digest_t *d = digest_get(self);
446
+ PUCHAR pin;
447
+ ULONG nin;
448
+ NTSTATUS st;
449
+
450
+ if (!d->h) rb_raise(eError, "phylax: digest not initialized");
451
+ nin = str_bytes(&data, &pin);
452
+ st = BCryptHashData(d->h, pin, nin, 0);
453
+ if (!NT_SUCCESS(st))
454
+ raise_nt("BCryptHashData", st);
455
+ return self;
456
+ }
457
+
458
+ /* Non-destructive: duplicate the running state and finish the copy, leaving the
459
+ * original open for further #update (matches Ruby stdlib Digest semantics). */
460
+ static VALUE
461
+ digest_digest(VALUE self)
462
+ {
463
+ digest_t *d = digest_get(self);
464
+ BCRYPT_HASH_HANDLE dup = NULL;
465
+ PUCHAR obj;
466
+ ULONG objlen;
467
+ NTSTATUS st;
468
+ VALUE out;
469
+
470
+ if (!d->h) rb_raise(eError, "phylax: digest not initialized");
471
+
472
+ /* Allocate the Ruby output first so the malloc below is the last resource
473
+ * acquired; an rb_str_new (OOM) raise then leaks nothing. */
474
+ out = rb_str_new(NULL, g_dlen[d->idx]);
475
+ rb_enc_associate(out, rb_ascii8bit_encoding());
476
+
477
+ objlen = g_objlen_hash[d->idx];
478
+ obj = (PUCHAR)malloc(objlen ? objlen : 1);
479
+ if (!obj) rb_raise(rb_eNoMemError, "phylax: out of memory");
480
+
481
+ st = BCryptDuplicateHash(d->h, &dup, obj, objlen, 0);
482
+ if (NT_SUCCESS(st))
483
+ st = BCryptFinishHash(dup, (PUCHAR)RSTRING_PTR(out), g_dlen[d->idx], 0);
484
+ if (dup) BCryptDestroyHash(dup);
485
+ free(obj);
486
+ if (!NT_SUCCESS(st))
487
+ raise_nt("BCryptDuplicateHash", st);
488
+
489
+ return out;
490
+ }
491
+
492
+ static VALUE
493
+ digest_size(VALUE self)
494
+ {
495
+ digest_t *d = digest_get(self);
496
+ if (d->idx < 0) rb_raise(eError, "phylax: digest not initialized");
497
+ return UINT2NUM(g_dlen[d->idx]);
498
+ }
499
+
500
+ /* ===================================================================
501
+ * Streaming HMAC — Phylax::HMAC (retains the key for #reset; wiped on free)
502
+ * =================================================================== */
503
+
504
+ typedef struct {
505
+ BCRYPT_HASH_HANDLE h;
506
+ int idx;
507
+ unsigned char *key;
508
+ ULONG keylen;
509
+ } hmac_t;
510
+
511
+ static void
512
+ hmac_free(void *p)
513
+ {
514
+ hmac_t *m = (hmac_t *)p;
515
+ if (m->h) BCryptDestroyHash(m->h);
516
+ if (m->key) { SecureZeroMemory(m->key, m->keylen); free(m->key); }
517
+ xfree(m);
518
+ }
519
+
520
+ static size_t
521
+ hmac_memsize(const void *p)
522
+ {
523
+ const hmac_t *m = (const hmac_t *)p;
524
+ return sizeof(hmac_t) + (m->key ? m->keylen : 0);
525
+ }
526
+
527
+ static const rb_data_type_t hmac_type = {
528
+ "Phylax::HMAC",
529
+ { 0, hmac_free, hmac_memsize, },
530
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
531
+ };
532
+
533
+ static VALUE
534
+ hmac_alloc(VALUE klass)
535
+ {
536
+ hmac_t *m;
537
+ VALUE obj = TypedData_Make_Struct(klass, hmac_t, &hmac_type, m);
538
+ m->h = NULL;
539
+ m->idx = -1;
540
+ m->key = NULL;
541
+ m->keylen = 0;
542
+ return obj;
543
+ }
544
+
545
+ static hmac_t *
546
+ hmac_get(VALUE self)
547
+ {
548
+ hmac_t *m;
549
+ TypedData_Get_Struct(self, hmac_t, &hmac_type, m);
550
+ return m;
551
+ }
552
+
553
+ static VALUE
554
+ hmac_init(VALUE self, VALUE vidx, VALUE key)
555
+ {
556
+ hmac_t *m = hmac_get(self);
557
+ int i = alg_index(vidx);
558
+ PUCHAR pkey;
559
+ ULONG nkey;
560
+ NTSTATUS st;
561
+
562
+ nkey = str_bytes(&key, &pkey);
563
+
564
+ if (m->h) { BCryptDestroyHash(m->h); m->h = NULL; }
565
+ if (m->key) { SecureZeroMemory(m->key, m->keylen); free(m->key); m->key = NULL; m->keylen = 0; }
566
+
567
+ /* Retain a private copy of the key so #reset can re-key without the caller. */
568
+ m->key = (unsigned char *)malloc(nkey ? nkey : 1);
569
+ if (!m->key) rb_raise(rb_eNoMemError, "phylax: out of memory");
570
+ memcpy(m->key, pkey, nkey);
571
+ m->keylen = nkey;
572
+
573
+ st = BCryptCreateHash(g_hmac[i], &m->h, NULL, 0, m->key, nkey, 0);
574
+ if (!NT_SUCCESS(st)) {
575
+ SecureZeroMemory(m->key, m->keylen); free(m->key); m->key = NULL; m->keylen = 0;
576
+ raise_nt("BCryptCreateHash(HMAC)", st);
577
+ }
578
+ m->idx = i;
579
+ return self;
580
+ }
581
+
582
+ static VALUE
583
+ hmac_update(VALUE self, VALUE data)
584
+ {
585
+ hmac_t *m = hmac_get(self);
586
+ PUCHAR pin;
587
+ ULONG nin;
588
+ NTSTATUS st;
589
+
590
+ if (!m->h) rb_raise(eError, "phylax: hmac not initialized");
591
+ nin = str_bytes(&data, &pin);
592
+ st = BCryptHashData(m->h, pin, nin, 0);
593
+ if (!NT_SUCCESS(st))
594
+ raise_nt("BCryptHashData", st);
595
+ return self;
596
+ }
597
+
598
+ static VALUE
599
+ hmac_digest(VALUE self)
600
+ {
601
+ hmac_t *m = hmac_get(self);
602
+ BCRYPT_HASH_HANDLE dup = NULL;
603
+ PUCHAR obj;
604
+ ULONG objlen;
605
+ NTSTATUS st;
606
+ VALUE out;
607
+
608
+ if (!m->h) rb_raise(eError, "phylax: hmac not initialized");
609
+
610
+ /* Allocate the Ruby output first so the malloc below is the last resource
611
+ * acquired; an rb_str_new (OOM) raise then leaks nothing. */
612
+ out = rb_str_new(NULL, g_dlen[m->idx]);
613
+ rb_enc_associate(out, rb_ascii8bit_encoding());
614
+
615
+ objlen = g_objlen_hmac[m->idx];
616
+ obj = (PUCHAR)malloc(objlen ? objlen : 1);
617
+ if (!obj) rb_raise(rb_eNoMemError, "phylax: out of memory");
618
+
619
+ st = BCryptDuplicateHash(m->h, &dup, obj, objlen, 0);
620
+ if (NT_SUCCESS(st))
621
+ st = BCryptFinishHash(dup, (PUCHAR)RSTRING_PTR(out), g_dlen[m->idx], 0);
622
+ if (dup) BCryptDestroyHash(dup);
623
+ free(obj);
624
+ if (!NT_SUCCESS(st))
625
+ raise_nt("BCryptDuplicateHash(HMAC)", st);
626
+
627
+ return out;
628
+ }
629
+
630
+ /* #reset — re-create the MAC object with the retained key. */
631
+ static VALUE
632
+ hmac_reset(VALUE self)
633
+ {
634
+ hmac_t *m = hmac_get(self);
635
+ BCRYPT_HASH_HANDLE nh = NULL;
636
+ NTSTATUS st;
637
+
638
+ if (m->idx < 0) rb_raise(eError, "phylax: hmac not initialized");
639
+ st = BCryptCreateHash(g_hmac[m->idx], &nh, NULL, 0, m->key, m->keylen, 0);
640
+ if (!NT_SUCCESS(st))
641
+ raise_nt("BCryptCreateHash(HMAC)", st);
642
+ if (m->h) BCryptDestroyHash(m->h);
643
+ m->h = nh;
644
+ return self;
645
+ }
646
+
647
+ static VALUE
648
+ hmac_size(VALUE self)
649
+ {
650
+ hmac_t *m = hmac_get(self);
651
+ if (m->idx < 0) rb_raise(eError, "phylax: hmac not initialized");
652
+ return UINT2NUM(g_dlen[m->idx]);
653
+ }
654
+
655
+ /* ===================================================================
656
+ * SecretBox — AES-256-GCM AEAD. Phylax::SecretBox
657
+ * Holds only the CNG key handle; no raw key bytes are retained.
658
+ * =================================================================== */
659
+
660
+ typedef struct {
661
+ BCRYPT_KEY_HANDLE key;
662
+ } box_t;
663
+
664
+ static void
665
+ box_free(void *p)
666
+ {
667
+ box_t *b = (box_t *)p;
668
+ if (b->key) BCryptDestroyKey(b->key);
669
+ xfree(b);
670
+ }
671
+
672
+ static size_t
673
+ box_memsize(const void *p)
674
+ {
675
+ (void)p;
676
+ return sizeof(box_t);
677
+ }
678
+
679
+ static const rb_data_type_t box_type = {
680
+ "Phylax::SecretBox",
681
+ { 0, box_free, box_memsize, },
682
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
683
+ };
684
+
685
+ static VALUE
686
+ box_alloc(VALUE klass)
687
+ {
688
+ box_t *b;
689
+ VALUE obj = TypedData_Make_Struct(klass, box_t, &box_type, b);
690
+ b->key = NULL;
691
+ return obj;
692
+ }
693
+
694
+ static box_t *
695
+ box_get(VALUE self)
696
+ {
697
+ box_t *b;
698
+ TypedData_Get_Struct(self, box_t, &box_type, b);
699
+ return b;
700
+ }
701
+
702
+ /* Phylax::SecretBox#_init(key) — key must be exactly 32 bytes (checked in Ruby
703
+ * too; re-checked here so the C contract is self-standing). */
704
+ static VALUE
705
+ box_init(VALUE self, VALUE key)
706
+ {
707
+ box_t *b = box_get(self);
708
+ PUCHAR pkey;
709
+ ULONG nkey;
710
+ NTSTATUS st;
711
+
712
+ nkey = str_bytes(&key, &pkey);
713
+ if (nkey != SECRETBOX_KEY_BYTES)
714
+ rb_raise(rb_eArgError, "phylax: key must be exactly %u bytes, got %lu",
715
+ SECRETBOX_KEY_BYTES, (unsigned long)nkey);
716
+
717
+ if (b->key) { BCryptDestroyKey(b->key); b->key = NULL; }
718
+ st = BCryptGenerateSymmetricKey(g_aes_gcm, &b->key, NULL, 0, pkey, nkey, 0);
719
+ if (!NT_SUCCESS(st))
720
+ raise_nt("BCryptGenerateSymmetricKey", st);
721
+ return self;
722
+ }
723
+
724
+ /* Phylax::SecretBox#_seal(plaintext, aad) -> nonce(12) || ciphertext || tag(16).
725
+ * A fresh random nonce is generated here on every call — the caller cannot
726
+ * supply or reuse one. aad is nil or a String (authenticated, not encrypted). */
727
+ static VALUE
728
+ box_seal(VALUE self, VALUE plaintext, VALUE aad)
729
+ {
730
+ box_t *b = box_get(self);
731
+ PUCHAR ppt, paad = NULL;
732
+ ULONG npt, naad = 0;
733
+ BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO info;
734
+ NTSTATUS st;
735
+ VALUE out;
736
+ PUCHAR base;
737
+ ULONG produced = 0;
738
+
739
+ if (!b->key) rb_raise(eError, "phylax: SecretBox is closed");
740
+ StringValue(plaintext);
741
+ npt = ulen(plaintext);
742
+ if (!NIL_P(aad)) { StringValue(aad); naad = ulen(aad); }
743
+
744
+ /* out = [ nonce(12) | ciphertext(npt) | tag(16) ]. Compute the total in a
745
+ * 64-bit unsigned type and bound it to LONG_MAX so the rb_str_new length
746
+ * (a 32-bit signed long on this LLP64 target) can never overflow. */
747
+ {
748
+ unsigned long long total = (unsigned long long)SECRETBOX_NONCE_BYTES +
749
+ (unsigned long long)npt +
750
+ (unsigned long long)SECRETBOX_TAG_BYTES;
751
+ if (total > (unsigned long long)LONG_MAX)
752
+ rb_raise(rb_eArgError, "phylax: plaintext too large to seal");
753
+ }
754
+ out = rb_str_new(NULL, (long)SECRETBOX_NONCE_BYTES + (long)npt + (long)SECRETBOX_TAG_BYTES);
755
+ rb_enc_associate(out, rb_ascii8bit_encoding());
756
+ /* pointers fetched after the last allocation (out) */
757
+ base = (PUCHAR)RSTRING_PTR(out);
758
+ ppt = (PUCHAR)RSTRING_PTR(plaintext);
759
+ if (!NIL_P(aad)) paad = (PUCHAR)RSTRING_PTR(aad);
760
+
761
+ st = BCryptGenRandom(NULL, base, SECRETBOX_NONCE_BYTES, BCRYPT_USE_SYSTEM_PREFERRED_RNG);
762
+ if (!NT_SUCCESS(st))
763
+ raise_nt("BCryptGenRandom", st);
764
+
765
+ BCRYPT_INIT_AUTH_MODE_INFO(info);
766
+ info.pbNonce = base;
767
+ info.cbNonce = SECRETBOX_NONCE_BYTES;
768
+ info.pbAuthData = paad;
769
+ info.cbAuthData = naad;
770
+ info.pbTag = base + SECRETBOX_NONCE_BYTES + npt;
771
+ info.cbTag = SECRETBOX_TAG_BYTES;
772
+
773
+ st = BCryptEncrypt(b->key, ppt, npt, &info,
774
+ NULL, 0, /* pbIV unused in GCM */
775
+ base + SECRETBOX_NONCE_BYTES, npt, &produced, 0);
776
+ if (!NT_SUCCESS(st))
777
+ raise_nt("BCryptEncrypt", st);
778
+
779
+ return out;
780
+ }
781
+
782
+ /* Phylax::SecretBox#_open(sealed, aad) -> plaintext, or raise AuthenticationError. */
783
+ static VALUE
784
+ box_open(VALUE self, VALUE sealed, VALUE aad)
785
+ {
786
+ box_t *b = box_get(self);
787
+ PUCHAR psealed, paad = NULL, pout;
788
+ ULONG nsealed, naad = 0, nct, produced = 0;
789
+ BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO info;
790
+ NTSTATUS st;
791
+ VALUE out;
792
+
793
+ if (!b->key) rb_raise(eError, "phylax: SecretBox is closed");
794
+ StringValue(sealed);
795
+ nsealed = ulen(sealed);
796
+ if (!NIL_P(aad)) { StringValue(aad); naad = ulen(aad); }
797
+
798
+ /* A blob shorter than nonce+tag can't be authentic — treat as tamper, not
799
+ * an ArgumentError, so probing truncated blobs yields no distinct signal. */
800
+ if (nsealed < SECRETBOX_NONCE_BYTES + SECRETBOX_TAG_BYTES)
801
+ rb_exc_raise(make_exc(eAuthError, "SecretBox#open", 0,
802
+ "sealed message is too short to be authentic"));
803
+
804
+ nct = nsealed - SECRETBOX_NONCE_BYTES - SECRETBOX_TAG_BYTES;
805
+ /* Bound the plaintext length to LONG_MAX so the rb_str_new length (32-bit
806
+ * signed long on this LLP64 target) can never wrap negative — mirrors the
807
+ * guard in box_seal. */
808
+ if ((unsigned long long)nct > (unsigned long long)LONG_MAX)
809
+ rb_raise(rb_eArgError, "phylax: sealed message too large");
810
+ out = rb_str_new(NULL, (long)nct);
811
+ rb_enc_associate(out, rb_ascii8bit_encoding());
812
+ /* pointers fetched after the last allocation (out) */
813
+ psealed = (PUCHAR)RSTRING_PTR(sealed);
814
+ pout = (PUCHAR)RSTRING_PTR(out);
815
+ if (!NIL_P(aad)) paad = (PUCHAR)RSTRING_PTR(aad);
816
+
817
+ BCRYPT_INIT_AUTH_MODE_INFO(info);
818
+ info.pbNonce = psealed;
819
+ info.cbNonce = SECRETBOX_NONCE_BYTES;
820
+ info.pbAuthData = paad;
821
+ info.cbAuthData = naad;
822
+ info.pbTag = psealed + SECRETBOX_NONCE_BYTES + nct;
823
+ info.cbTag = SECRETBOX_TAG_BYTES;
824
+
825
+ st = BCryptDecrypt(b->key, psealed + SECRETBOX_NONCE_BYTES, nct, &info,
826
+ NULL, 0,
827
+ pout, nct, &produced, 0);
828
+ if (!NT_SUCCESS(st)) {
829
+ /* GCM decrypts into pout BEFORE checking the tag, so on a mismatch the
830
+ * unauthenticated plaintext is already there. Wipe it before the raise
831
+ * abandons the buffer to GC — never let chosen-ciphertext output linger. */
832
+ SecureZeroMemory(pout, nct);
833
+ raise_nt("BCryptDecrypt", st); /* tag mismatch -> AuthenticationError */
834
+ }
835
+
836
+ return out;
837
+ }
838
+
839
+ /* Phylax::SecretBox#close — destroy the key handle now; idempotent. */
840
+ static VALUE
841
+ box_close(VALUE self)
842
+ {
843
+ box_t *b = box_get(self);
844
+ if (b->key) { BCryptDestroyKey(b->key); b->key = NULL; }
845
+ return Qnil;
846
+ }
847
+
848
+ /* --------------------------------------------------------------- DPAPI ----- */
849
+
850
+ /* Phylax.__protect(data, machine_scope_bool, entropy_or_nil) -> blob */
851
+ static VALUE
852
+ phylax_protect(VALUE self, VALUE data, VALUE machine, VALUE entropy)
853
+ {
854
+ DATA_BLOB in, ent, out;
855
+ DWORD flags = CRYPTPROTECT_UI_FORBIDDEN;
856
+ PUCHAR pin, pent = NULL;
857
+ ULONG nin, nent = 0;
858
+ VALUE result;
859
+
860
+ StringValue(data);
861
+ nin = ulen(data);
862
+ if (!NIL_P(entropy)) { StringValue(entropy); nent = ulen(entropy); }
863
+ pin = (PUCHAR)RSTRING_PTR(data);
864
+ if (!NIL_P(entropy)) pent = (PUCHAR)RSTRING_PTR(entropy);
865
+ if (RTEST(machine)) flags |= CRYPTPROTECT_LOCAL_MACHINE;
866
+
867
+ in.cbData = nin; in.pbData = pin;
868
+ ent.cbData = nent; ent.pbData = pent;
869
+ out.cbData = 0; out.pbData = NULL;
870
+
871
+ if (!CryptProtectData(&in, NULL, pent ? &ent : NULL, NULL, NULL, flags, &out)) {
872
+ raise_gle("CryptProtectData", GetLastError(), 0);
873
+ }
874
+
875
+ if ((unsigned long long)out.cbData > (unsigned long long)LONG_MAX) {
876
+ LocalFree(out.pbData);
877
+ rb_raise(rb_eArgError, "phylax: DPAPI blob too large");
878
+ }
879
+
880
+ /* LocalFree the OS blob even if the String allocation raises (OOM). */
881
+ {
882
+ struct strcopy sc = { (const char *)out.pbData, (long)out.cbData };
883
+ int state = 0;
884
+ result = rb_protect(strcopy_body, (VALUE)&sc, &state);
885
+ LocalFree(out.pbData);
886
+ if (state) rb_jump_tag(state);
887
+ }
888
+ rb_enc_associate(result, rb_ascii8bit_encoding());
889
+ return result;
890
+ }
891
+
892
+ /* Phylax.__unprotect(data, entropy_or_nil) -> plaintext */
893
+ static VALUE
894
+ phylax_unprotect(VALUE self, VALUE data, VALUE entropy)
895
+ {
896
+ DATA_BLOB in, ent, out;
897
+ PUCHAR pin, pent = NULL;
898
+ ULONG nin, nent = 0;
899
+ VALUE result;
900
+
901
+ StringValue(data);
902
+ nin = ulen(data);
903
+ if (!NIL_P(entropy)) { StringValue(entropy); nent = ulen(entropy); }
904
+ pin = (PUCHAR)RSTRING_PTR(data);
905
+ if (!NIL_P(entropy)) pent = (PUCHAR)RSTRING_PTR(entropy);
906
+
907
+ in.cbData = nin; in.pbData = pin;
908
+ ent.cbData = nent; ent.pbData = pent;
909
+ out.cbData = 0; out.pbData = NULL;
910
+
911
+ if (!CryptUnprotectData(&in, NULL, pent ? &ent : NULL, NULL, NULL,
912
+ CRYPTPROTECT_UI_FORBIDDEN, &out)) {
913
+ raise_gle("CryptUnprotectData", GetLastError(), 1);
914
+ }
915
+
916
+ if ((unsigned long long)out.cbData > (unsigned long long)LONG_MAX) {
917
+ SecureZeroMemory(out.pbData, out.cbData);
918
+ LocalFree(out.pbData);
919
+ rb_raise(rb_eArgError, "phylax: DPAPI blob too large");
920
+ }
921
+
922
+ /* Wipe the recovered plaintext and free the OS blob even if the String
923
+ * allocation raises (OOM). */
924
+ {
925
+ struct strcopy sc = { (const char *)out.pbData, (long)out.cbData };
926
+ int state = 0;
927
+ result = rb_protect(strcopy_body, (VALUE)&sc, &state);
928
+ SecureZeroMemory(out.pbData, out.cbData);
929
+ LocalFree(out.pbData);
930
+ if (state) rb_jump_tag(state);
931
+ }
932
+ rb_enc_associate(result, rb_ascii8bit_encoding());
933
+ return result;
934
+ }
935
+
936
+ /* ----------------------------------------------------------------- Init ---- */
937
+
938
+ static void
939
+ open_provider(BCRYPT_ALG_HANDLE *ph, LPCWSTR alg, ULONG flags, const char *what)
940
+ {
941
+ NTSTATUS st = BCryptOpenAlgorithmProvider(ph, alg, NULL, flags);
942
+ if (!NT_SUCCESS(st))
943
+ rb_raise(rb_eLoadError, "phylax: BCryptOpenAlgorithmProvider(%s) failed: 0x%08lX",
944
+ what, (unsigned long)(ULONG)st);
945
+ }
946
+
947
+ static ULONG
948
+ provider_prop(BCRYPT_ALG_HANDLE h, LPCWSTR prop, const char *what)
949
+ {
950
+ DWORD v = 0, got = 0;
951
+ NTSTATUS st = BCryptGetProperty(h, prop, (PUCHAR)&v, sizeof(v), &got, 0);
952
+ if (!NT_SUCCESS(st))
953
+ rb_raise(rb_eLoadError, "phylax: BCryptGetProperty(%s) failed: 0x%08lX",
954
+ what, (unsigned long)(ULONG)st);
955
+ return v;
956
+ }
957
+
958
+ void
959
+ Init_phylax(void)
960
+ {
961
+ LPCWSTR algids[N_ALG];
962
+ int i;
963
+ NTSTATUS st;
964
+
965
+ algids[0] = BCRYPT_SHA256_ALGORITHM;
966
+ algids[1] = BCRYPT_SHA384_ALGORITHM;
967
+ algids[2] = BCRYPT_SHA512_ALGORITHM;
968
+
969
+ mPhylax = rb_define_module("Phylax");
970
+ eError = rb_define_class_under(mPhylax, "Error", rb_eStandardError);
971
+ eAuthError = rb_define_class_under(mPhylax, "AuthenticationError", eError);
972
+ eOSError = rb_define_class_under(mPhylax, "OSError", eError);
973
+
974
+ /* Open and cache provider handles for the life of the process. */
975
+ for (i = 0; i < N_ALG; i++) {
976
+ open_provider(&g_hash[i], algids[i], 0, "hash");
977
+ open_provider(&g_hmac[i], algids[i], BCRYPT_ALG_HANDLE_HMAC_FLAG, "hmac");
978
+ g_dlen[i] = provider_prop(g_hash[i], BCRYPT_HASH_LENGTH, "HashLength");
979
+ g_objlen_hash[i] = provider_prop(g_hash[i], BCRYPT_OBJECT_LENGTH, "ObjectLength(hash)");
980
+ g_objlen_hmac[i] = provider_prop(g_hmac[i], BCRYPT_OBJECT_LENGTH, "ObjectLength(hmac)");
981
+ }
982
+
983
+ open_provider(&g_aes_gcm, BCRYPT_AES_ALGORITHM, 0, "aes");
984
+ st = BCryptSetProperty(g_aes_gcm, BCRYPT_CHAINING_MODE,
985
+ (PUCHAR)BCRYPT_CHAIN_MODE_GCM,
986
+ sizeof(BCRYPT_CHAIN_MODE_GCM), 0);
987
+ if (!NT_SUCCESS(st))
988
+ rb_raise(rb_eLoadError, "phylax: set GCM chaining mode failed: 0x%08lX",
989
+ (unsigned long)(ULONG)st);
990
+
991
+ /* module-level primitives */
992
+ rb_define_singleton_method(mPhylax, "random_bytes", phylax_random_bytes, 1);
993
+ rb_define_singleton_method(mPhylax, "secure_compare", phylax_secure_compare, 2);
994
+ rb_define_singleton_method(mPhylax, "__hash", phylax_hash, 2);
995
+ rb_define_singleton_method(mPhylax, "__hmac", phylax_hmac, 3);
996
+ rb_define_singleton_method(mPhylax, "__pbkdf2", phylax_pbkdf2, 5);
997
+ rb_define_singleton_method(mPhylax, "__protect", phylax_protect, 3);
998
+ rb_define_singleton_method(mPhylax, "__unprotect", phylax_unprotect, 2);
999
+
1000
+ /* Phylax::Digest */
1001
+ cDigest = rb_define_class_under(mPhylax, "Digest", rb_cObject);
1002
+ rb_define_alloc_func(cDigest, digest_alloc);
1003
+ rb_define_method(cDigest, "_init", digest_init, 1);
1004
+ rb_define_method(cDigest, "update", digest_update, 1);
1005
+ rb_define_method(cDigest, "digest", digest_digest, 0);
1006
+ rb_define_method(cDigest, "digest_length", digest_size, 0);
1007
+
1008
+ /* Phylax::HMAC */
1009
+ cHMAC = rb_define_class_under(mPhylax, "HMAC", rb_cObject);
1010
+ rb_define_alloc_func(cHMAC, hmac_alloc);
1011
+ rb_define_method(cHMAC, "_init", hmac_init, 2);
1012
+ rb_define_method(cHMAC, "update", hmac_update, 1);
1013
+ rb_define_method(cHMAC, "digest", hmac_digest, 0);
1014
+ rb_define_method(cHMAC, "reset", hmac_reset, 0);
1015
+ rb_define_method(cHMAC, "digest_length", hmac_size, 0);
1016
+
1017
+ /* Phylax::SecretBox */
1018
+ cSecretBox = rb_define_class_under(mPhylax, "SecretBox", rb_cObject);
1019
+ rb_define_alloc_func(cSecretBox, box_alloc);
1020
+ rb_define_method(cSecretBox, "_init", box_init, 1);
1021
+ rb_define_method(cSecretBox, "_seal", box_seal, 2);
1022
+ rb_define_method(cSecretBox, "_open", box_open, 2);
1023
+ rb_define_method(cSecretBox, "close", box_close, 0);
1024
+ }