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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +155 -0
- data/ext/phylax/extconf.rb +20 -0
- data/ext/phylax/phylax.c +1024 -0
- data/lib/phylax/version.rb +5 -0
- data/lib/phylax.rb +252 -0
- metadata +121 -0
data/ext/phylax/phylax.c
ADDED
|
@@ -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
|
+
}
|