Class: Phylax::SecretBox
- Inherits:
-
Object
- Object
- Phylax::SecretBox
- Defined in:
- lib/phylax.rb,
ext/phylax/phylax.c
Overview
Authenticated symmetric encryption (AES-256-GCM) with a misuse-resistant API: a fresh 96-bit nonce is generated on every #seal and framed into the output, so nonce reuse is impossible by construction, and #open refuses to return anything that fails authentication.
Constant Summary collapse
- KEY_BYTES =
32- NONCE_BYTES =
12- TAG_BYTES =
16- OVERHEAD =
bytes #seal adds over the plaintext
NONCE_BYTES + TAG_BYTES
Class Method Summary collapse
-
.from_password(password, salt:, iterations: 600_000) ⇒ Object
Derive a box’s key from a password via PBKDF2-HMAC-SHA256.
-
.generate_key ⇒ Object
A new random 32-byte key.
Instance Method Summary collapse
-
#_init(key) ⇒ Object
Phylax::SecretBox#_init(key) — key must be exactly 32 bytes (checked in Ruby too; re-checked here so the C contract is self-standing).
-
#_open(sealed, aad) ⇒ Object
Phylax::SecretBox#_open(sealed, aad) -> plaintext, or raise AuthenticationError.
-
#_seal(plaintext, aad) ⇒ Object
Phylax::SecretBox#_seal(plaintext, aad) -> nonce(12) || ciphertext || tag(16).
-
#close ⇒ Object
Phylax::SecretBox#close — destroy the key handle now; idempotent.
-
#initialize(key) ⇒ SecretBox
constructor
keymust be exactly 32 bytes (e.g. from .generate_key or .from_password). - #inspect ⇒ Object
-
#open(sealed, aad: nil) ⇒ Object
Verify and decrypt a blob from #seal.
-
#seal(plaintext, aad: nil) ⇒ Object
Encrypt and authenticate
plaintext.
Constructor Details
#initialize(key) ⇒ SecretBox
key must be exactly 32 bytes (e.g. from .generate_key or .from_password).
224 225 226 227 228 229 230 |
# File 'lib/phylax.rb', line 224 def initialize(key) key = String(key) unless key.bytesize == KEY_BYTES raise ArgumentError, "key must be exactly #{KEY_BYTES} bytes, got #{key.bytesize}" end _init(key) end |
Class Method Details
.from_password(password, salt:, iterations: 600_000) ⇒ Object
Derive a box’s key from a password via PBKDF2-HMAC-SHA256. salt is required and should be random and stored alongside the ciphertext.
217 218 219 220 221 |
# File 'lib/phylax.rb', line 217 def self.from_password(password, salt:, iterations: 600_000) key = Phylax.pbkdf2(password: password, salt: salt, iterations: iterations, length: KEY_BYTES, hash: :sha256) new(key) end |
.generate_key ⇒ Object
A new random 32-byte key.
211 212 213 |
# File 'lib/phylax.rb', line 211 def self.generate_key Phylax.random_bytes(KEY_BYTES) end |
Instance Method Details
#_init(key) ⇒ Object
Phylax::SecretBox#_init(key) — key must be exactly 32 bytes (checked in Ruby too; re-checked here so the C contract is self-standing).
704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 |
# File 'ext/phylax/phylax.c', line 704
static VALUE
box_init(VALUE self, VALUE key)
{
box_t *b = box_get(self);
PUCHAR pkey;
ULONG nkey;
NTSTATUS st;
nkey = str_bytes(&key, &pkey);
if (nkey != SECRETBOX_KEY_BYTES)
rb_raise(rb_eArgError, "phylax: key must be exactly %u bytes, got %lu",
SECRETBOX_KEY_BYTES, (unsigned long)nkey);
if (b->key) { BCryptDestroyKey(b->key); b->key = NULL; }
st = BCryptGenerateSymmetricKey(g_aes_gcm, &b->key, NULL, 0, pkey, nkey, 0);
if (!NT_SUCCESS(st))
raise_nt("BCryptGenerateSymmetricKey", st);
return self;
}
|
#_open(sealed, aad) ⇒ Object
Phylax::SecretBox#_open(sealed, aad) -> plaintext, or raise AuthenticationError.
783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 |
# File 'ext/phylax/phylax.c', line 783
static VALUE
box_open(VALUE self, VALUE sealed, VALUE aad)
{
box_t *b = box_get(self);
PUCHAR psealed, paad = NULL, pout;
ULONG nsealed, naad = 0, nct, produced = 0;
BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO info;
NTSTATUS st;
VALUE out;
if (!b->key) rb_raise(eError, "phylax: SecretBox is closed");
StringValue(sealed);
nsealed = ulen(sealed);
if (!NIL_P(aad)) { StringValue(aad); naad = ulen(aad); }
/* A blob shorter than nonce+tag can't be authentic — treat as tamper, not
* an ArgumentError, so probing truncated blobs yields no distinct signal. */
if (nsealed < SECRETBOX_NONCE_BYTES + SECRETBOX_TAG_BYTES)
rb_exc_raise(make_exc(eAuthError, "SecretBox#open", 0,
"sealed message is too short to be authentic"));
nct = nsealed - SECRETBOX_NONCE_BYTES - SECRETBOX_TAG_BYTES;
/* Bound the plaintext length to LONG_MAX so the rb_str_new length (32-bit
* signed long on this LLP64 target) can never wrap negative — mirrors the
* guard in box_seal. */
if ((unsigned long long)nct > (unsigned long long)LONG_MAX)
rb_raise(rb_eArgError, "phylax: sealed message too large");
out = rb_str_new(NULL, (long)nct);
rb_enc_associate(out, rb_ascii8bit_encoding());
/* pointers fetched after the last allocation (out) */
psealed = (PUCHAR)RSTRING_PTR(sealed);
pout = (PUCHAR)RSTRING_PTR(out);
if (!NIL_P(aad)) paad = (PUCHAR)RSTRING_PTR(aad);
BCRYPT_INIT_AUTH_MODE_INFO(info);
info.pbNonce = psealed;
info.cbNonce = SECRETBOX_NONCE_BYTES;
info.pbAuthData = paad;
info.cbAuthData = naad;
info.pbTag = psealed + SECRETBOX_NONCE_BYTES + nct;
info.cbTag = SECRETBOX_TAG_BYTES;
st = BCryptDecrypt(b->key, psealed + SECRETBOX_NONCE_BYTES, nct, &info,
NULL, 0,
pout, nct, &produced, 0);
if (!NT_SUCCESS(st)) {
/* GCM decrypts into pout BEFORE checking the tag, so on a mismatch the
* unauthenticated plaintext is already there. Wipe it before the raise
* abandons the buffer to GC — never let chosen-ciphertext output linger. */
SecureZeroMemory(pout, nct);
raise_nt("BCryptDecrypt", st); /* tag mismatch -> AuthenticationError */
}
return out;
}
|
#_seal(plaintext, aad) ⇒ Object
Phylax::SecretBox#_seal(plaintext, aad) -> nonce(12) || ciphertext || tag(16). A fresh random nonce is generated here on every call — the caller cannot supply or reuse one. aad is nil or a String (authenticated, not encrypted).
727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 |
# File 'ext/phylax/phylax.c', line 727
static VALUE
box_seal(VALUE self, VALUE plaintext, VALUE aad)
{
box_t *b = box_get(self);
PUCHAR ppt, paad = NULL;
ULONG npt, naad = 0;
BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO info;
NTSTATUS st;
VALUE out;
PUCHAR base;
ULONG produced = 0;
if (!b->key) rb_raise(eError, "phylax: SecretBox is closed");
StringValue(plaintext);
npt = ulen(plaintext);
if (!NIL_P(aad)) { StringValue(aad); naad = ulen(aad); }
/* out = [ nonce(12) | ciphertext(npt) | tag(16) ]. Compute the total in a
* 64-bit unsigned type and bound it to LONG_MAX so the rb_str_new length
* (a 32-bit signed long on this LLP64 target) can never overflow. */
{
unsigned long long total = (unsigned long long)SECRETBOX_NONCE_BYTES +
(unsigned long long)npt +
(unsigned long long)SECRETBOX_TAG_BYTES;
if (total > (unsigned long long)LONG_MAX)
rb_raise(rb_eArgError, "phylax: plaintext too large to seal");
}
out = rb_str_new(NULL, (long)SECRETBOX_NONCE_BYTES + (long)npt + (long)SECRETBOX_TAG_BYTES);
rb_enc_associate(out, rb_ascii8bit_encoding());
/* pointers fetched after the last allocation (out) */
base = (PUCHAR)RSTRING_PTR(out);
ppt = (PUCHAR)RSTRING_PTR(plaintext);
if (!NIL_P(aad)) paad = (PUCHAR)RSTRING_PTR(aad);
st = BCryptGenRandom(NULL, base, SECRETBOX_NONCE_BYTES, BCRYPT_USE_SYSTEM_PREFERRED_RNG);
if (!NT_SUCCESS(st))
raise_nt("BCryptGenRandom", st);
BCRYPT_INIT_AUTH_MODE_INFO(info);
info.pbNonce = base;
info.cbNonce = SECRETBOX_NONCE_BYTES;
info.pbAuthData = paad;
info.cbAuthData = naad;
info.pbTag = base + SECRETBOX_NONCE_BYTES + npt;
info.cbTag = SECRETBOX_TAG_BYTES;
st = BCryptEncrypt(b->key, ppt, npt, &info,
NULL, 0, /* pbIV unused in GCM */
base + SECRETBOX_NONCE_BYTES, npt, &produced, 0);
if (!NT_SUCCESS(st))
raise_nt("BCryptEncrypt", st);
return out;
}
|
#close ⇒ Object
Phylax::SecretBox#close — destroy the key handle now; idempotent.
840 841 842 843 844 845 846 |
# File 'ext/phylax/phylax.c', line 840
static VALUE
box_close(VALUE self)
{
box_t *b = box_get(self);
if (b->key) { BCryptDestroyKey(b->key); b->key = NULL; }
return Qnil;
}
|
#inspect ⇒ Object
246 247 248 |
# File 'lib/phylax.rb', line 246 def inspect "#<Phylax::SecretBox>" # never expose key material end |
#open(sealed, aad: nil) ⇒ Object
Verify and decrypt a blob from #seal. Raises Phylax::AuthenticationError if the ciphertext, tag, nonce, or aad don’t match (or the blob is too short to be authentic).
242 243 244 |
# File 'lib/phylax.rb', line 242 def open(sealed, aad: nil) _open(sealed, aad) end |
#seal(plaintext, aad: nil) ⇒ Object
Encrypt and authenticate plaintext. Optional aad (additional authenticated data) is authenticated but not encrypted, and must be supplied identically to #open. Returns nonce ‖ ciphertext ‖ tag.
235 236 237 |
# File 'lib/phylax.rb', line 235 def seal(plaintext, aad: nil) _seal(plaintext, aad) end |