Class: Phylax::SecretBox

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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_keyObject

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;
}

#closeObject

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;
}

#inspectObject



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