Module: Phylax
- Defined in:
- lib/phylax.rb,
lib/phylax/version.rb,
ext/phylax/phylax.c
Overview
phylax — misuse-resistant cryptography for Ruby, backed by the Windows CNG provider (bcrypt.dll) and DPAPI (crypt32.dll).
It implements no cryptography itself; it is a thin, hard-to-misuse binding to the operating system’s own validated primitives.
key = Phylax::SecretBox.generate_key # 32 secure random bytes
box = Phylax::SecretBox.new(key)
sealed = box.seal("attack at dawn") # fresh nonce per call, authenticated
box.open(sealed) # => "attack at dawn"
Phylax.sha256("data") # raw 32-byte digest
Phylax.hmac_sha256(key, "msg") # keyed MAC
Phylax.pbkdf2(password: pw, salt: s, iterations: 600_000, length: 32)
Phylax.random_bytes(16)
Phylax.protect(secret) # DPAPI, bound to this user
Defined Under Namespace
Classes: AuthenticationError, Digest, Error, HMAC, OSError, SecretBox
Constant Summary collapse
- HASHES =
The only hash algorithms the gem accepts, mapped to their C-side index.
{ sha256: 0, sha384: 1, sha512: 2 }.freeze
- VERSION =
"0.1.0"
Class Method Summary collapse
-
.__hash(vidx, data) ⇒ Object
Phylax.__hash(idx, data) -> raw digest String (one-shot).
-
.__hmac(vidx, key, data) ⇒ Object
Phylax.__hmac(idx, key, data) -> raw MAC String (one-shot).
-
.__pbkdf2(vidx, password, salt, viters, vlen) ⇒ Object
Phylax.__pbkdf2(idx, password, salt, iterations, length) -> derived key.
-
.__protect(data, machine, entropy) ⇒ Object
Phylax.__protect(data, machine_scope_bool, entropy_or_nil) -> blob.
-
.__unprotect(data, entropy) ⇒ Object
Phylax.__unprotect(data, entropy_or_nil) -> plaintext.
-
.hmac_sha256(key, data) ⇒ Object
— one-shot HMAC ——————————————————-.
- .hmac_sha384(key, data) ⇒ Object
- .hmac_sha512(key, data) ⇒ Object
-
.pbkdf2(password:, salt:, iterations:, length:, hash: :sha256) ⇒ Object
Derive a key from a password and salt.
-
.protect(data, scope: :user, entropy: nil) ⇒ Object
Encrypt
datawith a key the OS derives from the current user (default) or the machine. -
.random_bytes(rn) ⇒ Object
Phylax.random_bytes(n) -> String (BINARY, length n).
-
.secure_compare(a, b) ⇒ Object
Phylax.secure_compare(a, b) -> true/false.
-
.sha256(data) ⇒ Object
— one-shot hashing —————————————————-.
- .sha384(data) ⇒ Object
- .sha512(data) ⇒ Object
-
.unprotect(data, scope: :user, entropy: nil) ⇒ Object
Reverse of #protect.
Class Method Details
.__hash(vidx, data) ⇒ Object
Phylax.__hash(idx, data) -> raw digest String (one-shot).
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 |
# File 'ext/phylax/phylax.c', line 189
static VALUE
phylax_hash(VALUE self, VALUE vidx, VALUE data)
{
int i = alg_index(vidx);
PUCHAR pin, pout;
ULONG nin;
BCRYPT_HASH_HANDLE h = NULL;
NTSTATUS st;
VALUE out;
StringValue(data);
nin = ulen(data);
out = rb_str_new(NULL, g_dlen[i]);
rb_enc_associate(out, rb_ascii8bit_encoding());
pin = (PUCHAR)RSTRING_PTR(data); /* pointers fetched after the last alloc */
pout = (PUCHAR)RSTRING_PTR(out);
st = BCryptCreateHash(g_hash[i], &h, NULL, 0, NULL, 0, 0);
if (NT_SUCCESS(st)) {
st = BCryptHashData(h, pin, nin, 0);
if (NT_SUCCESS(st))
st = BCryptFinishHash(h, pout, g_dlen[i], 0);
}
if (h) BCryptDestroyHash(h);
if (!NT_SUCCESS(st))
raise_nt("BCryptHash", st);
return out;
}
|
.__hmac(vidx, key, data) ⇒ Object
Phylax.__hmac(idx, key, data) -> raw MAC String (one-shot).
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 |
# File 'ext/phylax/phylax.c', line 220
static VALUE
phylax_hmac(VALUE self, VALUE vidx, VALUE key, VALUE data)
{
int i = alg_index(vidx);
PUCHAR pkey, pin, pout;
ULONG nkey, nin;
BCRYPT_HASH_HANDLE h = NULL;
NTSTATUS st;
VALUE out;
StringValue(key);
StringValue(data);
nkey = ulen(key);
nin = ulen(data);
out = rb_str_new(NULL, g_dlen[i]);
rb_enc_associate(out, rb_ascii8bit_encoding());
pkey = (PUCHAR)RSTRING_PTR(key); /* pointers fetched after the last alloc */
pin = (PUCHAR)RSTRING_PTR(data);
pout = (PUCHAR)RSTRING_PTR(out);
st = BCryptCreateHash(g_hmac[i], &h, NULL, 0, pkey, nkey, 0);
if (NT_SUCCESS(st)) {
st = BCryptHashData(h, pin, nin, 0);
if (NT_SUCCESS(st))
st = BCryptFinishHash(h, pout, g_dlen[i], 0);
}
if (h) BCryptDestroyHash(h);
if (!NT_SUCCESS(st))
raise_nt("BCryptHash(HMAC)", st);
return out;
}
|
.__pbkdf2(vidx, password, salt, viters, vlen) ⇒ Object
Phylax.__pbkdf2(idx, password, salt, iterations, length) -> derived key. Password/salt/output are copied into private heap buffers so the GVL can be released for the (potentially long) derivation without exposing the Ruby strings to GC compaction. Secret buffers are wiped before free.
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 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 |
# File 'ext/phylax/phylax.c', line 277
static VALUE
phylax_pbkdf2(VALUE self, VALUE vidx, VALUE password, VALUE salt,
VALUE viters, VALUE vlen)
{
int i = alg_index(vidx);
PUCHAR ppw, psalt;
ULONG npw, nsalt;
long len = NUM2LONG(vlen);
long long signed_iters = NUM2LL(viters); /* RangeError if it overflows int64 */
unsigned long long iters;
struct pbkdf2_job job;
VALUE out;
/* Validate in C too: __pbkdf2 is reachable directly, bypassing the Ruby
* guards, and a negative count would wrap (via the old NUM2ULL) into a
* ~2^64 iteration run that spins uninterruptibly under the released GVL. */
if (signed_iters < 1)
rb_raise(rb_eArgError, "phylax: iterations must be >= 1");
iters = (unsigned long long)signed_iters;
if (len < 1)
rb_raise(rb_eArgError, "phylax: length must be >= 1");
if ((unsigned long long)len > 0xFFFFFFFFull)
rb_raise(rb_eArgError, "phylax: length too large (> 4 GiB)");
StringValue(password);
StringValue(salt);
npw = ulen(password);
nsalt = ulen(salt);
ppw = (PUCHAR)RSTRING_PTR(password); /* copied into heap below, before any */
psalt = (PUCHAR)RSTRING_PTR(salt); /* further Ruby allocation */
job.hPrf = g_hmac[i];
job.pwlen = npw;
job.saltlen = nsalt;
job.iters = iters;
job.outlen = (ULONG)len;
job.pw = (PUCHAR)malloc(npw ? npw : 1);
job.salt = (PUCHAR)malloc(nsalt ? nsalt : 1);
job.out = (PUCHAR)malloc((size_t)len);
if (!job.pw || !job.salt || !job.out) {
free(job.pw); free(job.salt); free(job.out);
rb_raise(rb_eNoMemError, "phylax: out of memory in pbkdf2");
}
memcpy(job.pw, ppw, npw);
memcpy(job.salt, psalt, nsalt);
/* NULL ubf: the derivation is bounded CPU work that cannot be unblocked
* mid-call, so it runs to completion rather than pretending to interrupt. */
rb_thread_call_without_gvl(pbkdf2_nogvl, &job, NULL, NULL);
SecureZeroMemory(job.pw, npw);
free(job.pw);
free(job.salt);
if (!NT_SUCCESS(job.st)) {
SecureZeroMemory(job.out, (size_t)len);
free(job.out);
raise_nt("BCryptDeriveKeyPBKDF2", job.st);
}
/* Wipe and free the derived key even if the String allocation raises (OOM). */
{
struct strcopy sc = { (const char *)job.out, len };
int state = 0;
out = rb_protect(strcopy_body, (VALUE)&sc, &state);
SecureZeroMemory(job.out, (size_t)len);
free(job.out);
if (state) rb_jump_tag(state);
}
rb_enc_associate(out, rb_ascii8bit_encoding());
return out;
}
|
.__protect(data, machine, entropy) ⇒ Object
Phylax.__protect(data, machine_scope_bool, entropy_or_nil) -> blob
851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 |
# File 'ext/phylax/phylax.c', line 851
static VALUE
phylax_protect(VALUE self, VALUE data, VALUE machine, VALUE entropy)
{
DATA_BLOB in, ent, out;
DWORD flags = CRYPTPROTECT_UI_FORBIDDEN;
PUCHAR pin, pent = NULL;
ULONG nin, nent = 0;
VALUE result;
StringValue(data);
nin = ulen(data);
if (!NIL_P(entropy)) { StringValue(entropy); nent = ulen(entropy); }
pin = (PUCHAR)RSTRING_PTR(data);
if (!NIL_P(entropy)) pent = (PUCHAR)RSTRING_PTR(entropy);
if (RTEST(machine)) flags |= CRYPTPROTECT_LOCAL_MACHINE;
in.cbData = nin; in.pbData = pin;
ent.cbData = nent; ent.pbData = pent;
out.cbData = 0; out.pbData = NULL;
if (!CryptProtectData(&in, NULL, pent ? &ent : NULL, NULL, NULL, flags, &out)) {
raise_gle("CryptProtectData", GetLastError(), 0);
}
if ((unsigned long long)out.cbData > (unsigned long long)LONG_MAX) {
LocalFree(out.pbData);
rb_raise(rb_eArgError, "phylax: DPAPI blob too large");
}
/* LocalFree the OS blob even if the String allocation raises (OOM). */
{
struct strcopy sc = { (const char *)out.pbData, (long)out.cbData };
int state = 0;
result = rb_protect(strcopy_body, (VALUE)&sc, &state);
LocalFree(out.pbData);
if (state) rb_jump_tag(state);
}
rb_enc_associate(result, rb_ascii8bit_encoding());
return result;
}
|
.__unprotect(data, entropy) ⇒ Object
Phylax.__unprotect(data, entropy_or_nil) -> plaintext
893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 |
# File 'ext/phylax/phylax.c', line 893
static VALUE
phylax_unprotect(VALUE self, VALUE data, VALUE entropy)
{
DATA_BLOB in, ent, out;
PUCHAR pin, pent = NULL;
ULONG nin, nent = 0;
VALUE result;
StringValue(data);
nin = ulen(data);
if (!NIL_P(entropy)) { StringValue(entropy); nent = ulen(entropy); }
pin = (PUCHAR)RSTRING_PTR(data);
if (!NIL_P(entropy)) pent = (PUCHAR)RSTRING_PTR(entropy);
in.cbData = nin; in.pbData = pin;
ent.cbData = nent; ent.pbData = pent;
out.cbData = 0; out.pbData = NULL;
if (!CryptUnprotectData(&in, NULL, pent ? &ent : NULL, NULL, NULL,
CRYPTPROTECT_UI_FORBIDDEN, &out)) {
raise_gle("CryptUnprotectData", GetLastError(), 1);
}
if ((unsigned long long)out.cbData > (unsigned long long)LONG_MAX) {
SecureZeroMemory(out.pbData, out.cbData);
LocalFree(out.pbData);
rb_raise(rb_eArgError, "phylax: DPAPI blob too large");
}
/* Wipe the recovered plaintext and free the OS blob even if the String
* allocation raises (OOM). */
{
struct strcopy sc = { (const char *)out.pbData, (long)out.cbData };
int state = 0;
result = rb_protect(strcopy_body, (VALUE)&sc, &state);
SecureZeroMemory(out.pbData, out.cbData);
LocalFree(out.pbData);
if (state) rb_jump_tag(state);
}
rb_enc_associate(result, rb_ascii8bit_encoding());
return result;
}
|
.hmac_sha256(key, data) ⇒ Object
— one-shot HMAC ——————————————————-
41 |
# File 'lib/phylax.rb', line 41 def hmac_sha256(key, data) = __hmac(0, key, data) |
.hmac_sha384(key, data) ⇒ Object
42 |
# File 'lib/phylax.rb', line 42 def hmac_sha384(key, data) = __hmac(1, key, data) |
.hmac_sha512(key, data) ⇒ Object
43 |
# File 'lib/phylax.rb', line 43 def hmac_sha512(key, data) = __hmac(2, key, data) |
.pbkdf2(password:, salt:, iterations:, length:, hash: :sha256) ⇒ Object
Derive a key from a password and salt. Defaults to HMAC-SHA256.
password: secret bytes (String)
salt: non-secret bytes (String); must not be empty, >= 16 advised
iterations: PRF iteration count (Integer >= 1)
length: desired key length in bytes (Integer >= 1)
hash: :sha256 / :sha384 / :sha512
53 54 55 56 57 58 59 60 |
# File 'lib/phylax.rb', line 53 def pbkdf2(password:, salt:, iterations:, length:, hash: :sha256) idx = HASHES.fetch(hash) { raise ArgumentError, "unknown hash #{hash.inspect}" } raise ArgumentError, "iterations must be >= 1" if iterations < 1 raise ArgumentError, "length must be >= 1" if length < 1 raise ArgumentError, "salt must not be empty" if String(salt).empty? __pbkdf2(idx, password, salt, iterations, length) end |
.protect(data, scope: :user, entropy: nil) ⇒ Object
Encrypt data with a key the OS derives from the current user (default) or the machine. The returned opaque blob can only be unprotected on the same machine (and, for :user scope, by the same user). An optional entropy string acts as a second secret that must be supplied identically to #unprotect.
69 70 71 |
# File 'lib/phylax.rb', line 69 def protect(data, scope: :user, entropy: nil) __protect(data, machine_scope!(scope), entropy) end |
.random_bytes(rn) ⇒ Object
Phylax.random_bytes(n) -> String (BINARY, length n)
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'ext/phylax/phylax.c', line 161
static VALUE
phylax_random_bytes(VALUE self, VALUE rn)
{
long n = NUM2LONG(rn);
VALUE out;
NTSTATUS st;
if (n < 0)
rb_raise(rb_eArgError, "phylax: negative length");
if ((unsigned long long)n > 0xFFFFFFFFull)
rb_raise(rb_eArgError, "phylax: length too large (> 4 GiB)");
out = rb_str_new(NULL, n);
rb_enc_associate(out, rb_ascii8bit_encoding());
if (n == 0)
return out;
st = BCryptGenRandom(NULL, (PUCHAR)RSTRING_PTR(out), (ULONG)n,
BCRYPT_USE_SYSTEM_PREFERRED_RNG);
if (!NT_SUCCESS(st))
raise_nt("BCryptGenRandom", st); /* never returns the zeroed buffer */
return out;
}
|
.secure_compare(a, b) ⇒ Object
Phylax.secure_compare(a, b) -> true/false. Constant time w.r.t. content for equal-length inputs; short-circuits false on differing length (length is not secret for tag comparison — documented).
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 |
# File 'ext/phylax/phylax.c', line 356
static VALUE
phylax_secure_compare(VALUE self, VALUE a, VALUE b)
{
long la, lb, i;
const volatile unsigned char *pa, *pb;
volatile unsigned char acc = 0;
StringValue(a);
StringValue(b);
la = RSTRING_LEN(a);
lb = RSTRING_LEN(b);
if (la != lb)
return Qfalse;
pa = (const volatile unsigned char *)RSTRING_PTR(a);
pb = (const volatile unsigned char *)RSTRING_PTR(b);
for (i = 0; i < la; i++)
acc |= (unsigned char)(pa[i] ^ pb[i]);
return acc == 0 ? Qtrue : Qfalse;
}
|
.sha256(data) ⇒ Object
— one-shot hashing —————————————————-
35 |
# File 'lib/phylax.rb', line 35 def sha256(data) = __hash(0, data) |
.sha384(data) ⇒ Object
36 |
# File 'lib/phylax.rb', line 36 def sha384(data) = __hash(1, data) |
.sha512(data) ⇒ Object
37 |
# File 'lib/phylax.rb', line 37 def sha512(data) = __hash(2, data) |
.unprotect(data, scope: :user, entropy: nil) ⇒ Object
Reverse of #protect. Raises Phylax::AuthenticationError if the blob was tampered with, the wrong entropy was given, or it belongs to another user/machine. (DPAPI infers the scope from the blob; scope is validated for symmetry but does not change the operation.)
77 78 79 80 |
# File 'lib/phylax.rb', line 77 def unprotect(data, scope: :user, entropy: nil) machine_scope!(scope) __unprotect(data, entropy) end |