phylax

Misuse-resistant cryptography for Ruby, backed by the Windows CNG provider.

phylax (Greek φύλαξ, "guardian") is a thin, safe-by-default binding to the cryptography Windows already ships and validates: the Cryptography API: Next Generation (CNG / bcrypt.dll) and DPAPI (crypt32.dll). It implements no cryptography of its own — it exposes the operating system's own primitives through an ergonomic, hard-to-misuse API.

The design goal is that the easy way is the safe way:

  • Authenticated encryption that can't be nonce-reused. SecretBox (AES-256-GCM) generates a fresh 96-bit nonce on every seal and frames it into the output, so the catastrophic GCM failure mode — reusing a (key, nonce) pair — is impossible through the public API. There is no nonce parameter to get wrong.
  • Decryption is all-or-nothing. open verifies the authentication tag inside the OS call and raises Phylax::AuthenticationError rather than ever returning unauthenticated plaintext.
  • No silent weakening. A key that isn't exactly 32 bytes is a loud error, not a silently-truncated AES-128 key. Only SHA-2 is accepted — :md5/:sha1 raise.
  • Constant-time comparison is provided so you don't verify MACs with ==.
What API
Secure random Phylax.random_bytes(n)
Hashing (one-shot) Phylax.sha256(data), .sha384, .sha512
Hashing (streaming) Phylax::Digest.new(:sha256)
HMAC (one-shot) Phylax.hmac_sha256(key, data), .hmac_sha384, .hmac_sha512
HMAC (streaming) Phylax::HMAC.new(:sha256, key), Phylax::HMAC.verify(...)
Key derivation Phylax.pbkdf2(password:, salt:, iterations:, length:, hash:)
Authenticated encryption Phylax::SecretBox (AES-256-GCM)
Constant-time compare Phylax.secure_compare(a, b)
Secrets at rest Phylax.protect(data, scope:, entropy:) / Phylax.unprotect

Requirements

  • Windows with a native MSVC (mswin) Ruby. Not supported on MinGW/UCRT.
  • Visual Studio 2017+ / Build Tools with the Desktop development with C++ workload.

Install

gem install phylax

Authenticated encryption — SecretBox

require "phylax"

key = Phylax::SecretBox.generate_key      # 32 cryptographically random bytes
box = Phylax::SecretBox.new(key)

sealed = box.seal("attack at dawn")       # => nonce ‖ ciphertext ‖ tag (binary)
box.open(sealed)                          # => "attack at dawn"

# Additional authenticated data (authenticated, not encrypted) — must match.
sealed = box.seal(payload, aad: "v1:user-42")
box.open(sealed, aad: "v1:user-42")       # ok
box.open(sealed, aad: "v1:user-99")       # raises Phylax::AuthenticationError

# Any tampering with the nonce, ciphertext, tag, or aad fails closed:
box.open(sealed.tap { |s| s.setbyte(20, s.getbyte(20) ^ 1) })
# => raises Phylax::AuthenticationError (no plaintext is returned)

Derive the key from a password instead (PBKDF2-HMAC-SHA256):

salt = Phylax.random_bytes(16)            # store this alongside the ciphertext
box  = Phylax::SecretBox.from_password("correct horse battery staple",
                                       salt: salt, iterations: 600_000)

Wire format. seal returns nonce (12 bytes) ‖ ciphertext (= plaintext length) ‖ tag (16 bytes), so the overhead is a constant Phylax::SecretBox::OVERHEAD (28) bytes. This is standard AES-256-GCM: a blob from seal can be decrypted by any GCM implementation (e.g. OpenSSL) given the same key, and vice versa.

Hashing and HMAC

Phylax.sha256("data")                     # => 32 raw bytes (ASCII-8BIT)
Phylax.sha256("data").unpack1("H*")       # => hex

# Streaming — non-destructive #digest, so you can keep updating:
d = Phylax::Digest.new(:sha256)
d << "chunk one" << "chunk two"
d.hexdigest

Phylax.hmac_sha256(key, "message")        # keyed MAC, raw bytes

# Verify a MAC the safe way (constant-time, never raises on mismatch):
Phylax::HMAC.verify(:sha256, key, message, received_tag)   # => true / false

Key derivation, random, and comparison

Phylax.pbkdf2(password: pw, salt: salt, iterations: 600_000, length: 32)
Phylax.random_bytes(16)                   # system CSPRNG (CTR_DRBG)
Phylax.secure_compare(tag_a, tag_b)       # constant-time for equal-length inputs

Secrets at rest — DPAPI

protect encrypts data with a key the OS derives from the current user (default) or the machine, so the blob can only be read back on the same machine (and, for :user scope, by the same user). Great for storing a credential or a SecretBox key on disk without a master password.

blob = Phylax.protect("api-token-value")          # bound to this Windows user
Phylax.unprotect(blob)                             # => "api-token-value"

# Optional second secret ("entropy") that must be supplied to unprotect:
blob = Phylax.protect(secret, entropy: app_salt)
Phylax.unprotect(blob, entropy: app_salt)

# Machine scope: any account on this machine can unprotect.
Phylax.protect(secret, scope: :machine)

Errors

StandardError
└─ Phylax::Error                 # base for everything phylax raises
    ├─ Phylax::AuthenticationError  # GCM tag mismatch / tamper, or DPAPI integrity failure
    └─ Phylax::OSError              # other Windows API failure (#api, #code)

ArgumentError/TypeError are raised for bad sizes or types before any OS call (e.g. a key that isn't 32 bytes, an unknown hash, a non-String input).

Security notes

  • Algorithms. AES-256-GCM (128-bit tag), SHA-256/384/512, HMAC-SHA-2, PBKDF2-HMAC-SHA-2, BCryptGenRandom (SP 800-90A CTR_DRBG). All are the OS's validated implementations; phylax only marshals bytes.
  • Random-nonce limit. With random 96-bit nonces, keep a single SecretBox key under ~2³² messages (the GCM birthday bound). Rotate keys for very high volume.
  • secure_compare leaks length. It returns false immediately when lengths differ and is constant-time only across equal-length inputs — fine for comparing fixed-length MACs, which is its purpose.
  • DPAPI tamper detection is best-effort. Windows may return an error or, rarely, succeed with corrupted output on a mangled blob. If you need a hard integrity guarantee, wrap the data in a SecretBox (or add your own HMAC) first.
  • Windows/MSVC only. The extension links bcrypt.lib + crypt32.lib and is built with cl.exe against a native-MSVC Ruby.

License

MIT.