Class: PqcAsn1::SecureBuffer

Inherits:
Object
  • Object
show all
Defined in:
lib/pqc_asn1.rb,
lib/pqc_asn1.rb,
ext/pqc_asn1/secure_buffer.c

Overview

Secure memory buffer backed by mmap(2) with mprotect(2) and mlock(2).

SecureBuffer holds secret key material in memory that is:

  • mlock'd to prevent swapping to disk
  • mprotect'd to PROT_NONE when not in use (inaccessible)
  • securely zeroed on GC or explicit wipe!
  • guarded by canary bytes to detect buffer overflow

Use #use to temporarily unlock the memory for reading:

secure_buf.use { |bytes| do_something_with(bytes) }

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.from_string(rb_str) ⇒ Object



462
463
464
465
466
467
468
469
470
471
# File 'ext/pqc_asn1/secure_buffer.c', line 462

static VALUE
pqcsb_from_string(VALUE klass, VALUE rb_str)
{
    StringValue(rb_str);
    const uint8_t *ptr = (const uint8_t *)RSTRING_PTR(rb_str);
    size_t len = (size_t)RSTRING_LEN(rb_str);
    if (len == 0)
        rb_raise(rb_eArgError, "string must not be empty");
    return pqcsb_rb_create(klass, ptr, len);
}

.random(rb_n) ⇒ Object



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'ext/pqc_asn1/secure_buffer.c', line 379

static VALUE
pqcsb_random(VALUE klass, VALUE rb_n)
{
    long n_signed = NUM2LONG(rb_n);
    if (n_signed <= 0)
        rb_raise(rb_eArgError, "size must be > 0");
    size_t n = (size_t)n_signed;

    pqcsb_buf_t *buf = NULL;
    pqcsb_status_t rc = pqcsb_create_random(n, &buf);
    if (rc != PQCSB_OK)
        rb_raise(rb_eRuntimeError, "pqcsb_create_random failed: %s",
                 pqcsb_error_message(rc));

    VALUE obj = TypedData_Wrap_Struct(klass, &pqcsb_buf_type, buf);
    rb_obj_freeze(obj);
    return obj;
}

Instance Method Details

#==(other) ⇒ Object



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'ext/pqc_asn1/secure_buffer.c', line 293

static VALUE
pqcsb_eq(VALUE self, VALUE other)
{
    pqcsb_buf_t *buf = (pqcsb_buf_t *)RTYPEDDATA_DATA(self);
    pqcsb_check_wiped(buf);

    if (rb_obj_is_kind_of(other, s_cSecureBuffer)) {
        pqcsb_buf_t *other_buf = (pqcsb_buf_t *)RTYPEDDATA_DATA(other);
        pqcsb_check_wiped(other_buf);
        return pqcsb_ct_equal_bufs(buf, other_buf) ? Qtrue : Qfalse;
    }

    if (RB_TYPE_P(other, T_STRING)) {
        const uint8_t *other_ptr = (const uint8_t *)RSTRING_PTR(other);
        size_t other_len = (size_t)RSTRING_LEN(other);
        return pqcsb_ct_equal(buf, other_ptr, other_len) ? Qtrue : Qfalse;
    }

    return Qfalse;
}

#bytesize ⇒ Object




717
# File 'lib/pqc_asn1.rb', line 717

class SecureBuffer; end

#canary_ok? ⇒ Boolean

Returns true if the guard canary bytes are intact.

Returns:

  • (Boolean) —

    true if the guard canary bytes are intact



717
# File 'lib/pqc_asn1.rb', line 717

class SecureBuffer; end

#eql?(other) ⇒ Boolean

Returns:

  • (Boolean)


314
315
316
317
318
# File 'ext/pqc_asn1/secure_buffer.c', line 314

static VALUE
pqcsb_eql(VALUE self, VALUE other)
{
    return pqcsb_eq(self, other);
}

#hash ⇒ Object



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'ext/pqc_asn1/secure_buffer.c', line 338

static VALUE
pqcsb_hash(VALUE self)
{
    pqcsb_buf_t *buf = (pqcsb_buf_t *)RTYPEDDATA_DATA(self);
    pqcsb_check_wiped(buf);

    pqcsb_read_guard_t guard = pqcsb_begin_read(buf);
    if (guard.status != PQCSB_OK)
        rb_raise(rb_eRuntimeError, "pqcsb_begin_read failed: %s",
                 pqcsb_error_message(guard.status));

    st_index_t h = rb_memhash(guard.data, (long)guard.len);
    pqcsb_end_read(&guard);

    h ^= s_hash_salt;
    return ST2FIX(h);
}

#inspect ⇒ String

Returns redacted representation (never reveals contents).

Returns:

  • (String) —

    redacted representation (never reveals contents)



717
# File 'lib/pqc_asn1.rb', line 717

class SecureBuffer; end

#marshal_dump ⇒ Object



329
330
331
332
333
334
335
336
# File 'ext/pqc_asn1/secure_buffer.c', line 329

static VALUE
pqcsb_marshal_dump(VALUE self)
{
    (void)self;
    rb_raise(rb_eTypeError,
             "can't dump PqcAsn1::SecureBuffer (contains secret key material)");
    return Qnil;
}

#size ⇒ Object




717
# File 'lib/pqc_asn1.rb', line 717

class SecureBuffer; end

#slice(rb_offset, rb_length) ⇒ PqcAsn1::SecureBuffer

Extract a sub-range of the buffer as a new SecureBuffer. The sub-range is copied into a fresh mmap-protected region so the extracted material gets the same security guarantees as the source.

Parameters:

  • offset (Integer) —

    byte offset

  • length (Integer) —

    number of bytes

Returns:



717
# File 'lib/pqc_asn1.rb', line 717

class SecureBuffer; end

#to_pem(label = "PRIVATE KEY") ⇒ String

PEM-encode the DER contents of this SecureBuffer.

The returned PEM String is an ordinary Ruby String on the heap, NOT a SecureBuffer. Discard it as soon as possible to limit exposure of secret key material.

Parameters:

  • label (String) (defaults to: "PRIVATE KEY") —

    PEM label (default "PRIVATE KEY")

Returns:

  • (String) —

    frozen US-ASCII PEM string



1094
1095
1096
# File 'lib/pqc_asn1.rb', line 1094

def to_pem(label = "PRIVATE KEY")
  PqcAsn1::PEM.encode(self, label)
end

#to_pem_io(io, label = "PRIVATE KEY") ⇒ Integer

PEM-encode and write directly to an IO object. The PEM string is written and then discarded immediately.

Parameters:

  • io (IO) —

    writable IO

  • label (String) (defaults to: "PRIVATE KEY") —

    PEM label (default "PRIVATE KEY")

Returns:

  • (Integer) —

    number of bytes written



1114
1115
1116
1117
# File 'lib/pqc_asn1.rb', line 1114

def to_pem_io(io, label = "PRIVATE KEY")
  pem = to_pem(label)
  io.write(pem)
end

#to_s ⇒ String

Returns frozen binary copy of the buffer contents.

Returns:

  • (String) —

    frozen binary copy of the buffer contents



717
# File 'lib/pqc_asn1.rb', line 717

class SecureBuffer; end

#use ⇒ Object

SecureBuffer#use { |bytes| ... } — yield temporary heap copy for block.

SECURITY MODEL

This method provides secure access to encrypted key material by:

  1. GUARD-BASED UNLOCKING

    • Calls pqcsb_begin_read() which returns a guard struct containing a pointer to the protected mmap region (temporarily unprotected to PROT_READ)
    • Guard tracks access via atomic refcount (multiple #use calls can nest)
  2. TEMPORARY HEAP COPY

    • Creates a new Ruby String on the heap containing a copy of the key material
    • This allows Ruby code to work with the bytes (Ruby APIs need mutable Strings)
    • Trade-off: temporary copy exists on heap during block execution (unavoidable limitation of Ruby String semantics)
  3. GUARANTEED CLEANUP via rb_ensure

    • Even if the block raises an exception, pqcsb_use_ensure() guarantees: a) Heap copy is securely zeroed b) Heap copy is frozen (escaped references become useless) c) mmap region is re-protected to PROT_NONE (via pqcsb_end_read)
  4. NESTED CALL SAFETY

    • Multiple #use calls can nest (read_refs tracks depth)
    • Re-entry only re-protects on the outermost exit
    • Prevents premature re-protection while outer caller still needs access

WHY GUARD IN CONTEXT?

The guard MUST be stored as a pointer in the context struct so it remains valid through the rb_ensure lifecycle:

pqcsb_read_guard_t guard = pqcsb_begin_read(...);  // Guard on stack
ctx = { .guard = &guard };                          // Store POINTER
rb_ensure(..., pqcsb_use_ensure, (VALUE)&ctx);     // Ensure sees &guard

If we passed guard by value, it might go out of scope before ensure runs. Storing &guard ensures the original stack-allocated guard is accessible.

RETURN VALUE

Returns the block's return value (not the bytes), allowing: SecureBuffer#use { |b| b.bytesize } # => integer SecureBuffer#use { |b| b.reverse } # => reversed bytes



717
# File 'lib/pqc_asn1.rb', line 717

class SecureBuffer; end

#wipe! ⇒ self

Securely zero the buffer contents and mark it as wiped. Subsequent #use calls will raise.

Returns:

  • (self)


717
# File 'lib/pqc_asn1.rb', line 717

class SecureBuffer; end

#wiped? ⇒ Boolean

Returns true if #wipe! has been called.

Returns:

  • (Boolean) —

    true if #wipe! has been called



717
# File 'lib/pqc_asn1.rb', line 717

class SecureBuffer; end

#write_to(io) ⇒ Integer

Write the raw DER bytes directly to an IO object. The bytes pass through a temporary #use block and are written immediately, minimising exposure time on the Ruby heap.

Parameters:

  • io (IO) —

    writable IO (file, socket, StringIO, etc.)

Returns:

  • (Integer) —

    number of bytes written



1104
1105
1106
# File 'lib/pqc_asn1.rb', line 1104

def write_to(io)
  use { |bytes| io.write(bytes) }
end