Class: Nuckle::SecretBox

Inherits:
Object
  • Object
show all
Defined in:
lib/nuckle/secret_box.rb

Overview

Symmetric authenticated encryption: XSalsa20-Poly1305.

Compatible with NaCl crypto_secretbox / libsodium crypto_secretbox_xsalsa20poly1305.

Constant Summary collapse

KEYBYTES =
32
NONCEBYTES =
24
ZEROBYTES =
32
BOXZEROBYTES =
16
MACBYTES =
16

Instance Method Summary collapse

Constructor Details

#initialize(key) ⇒ SecretBox

Returns a new instance of SecretBox.

Parameters:

  • key (String)

    32-byte symmetric key (binary)

Raises:

  • (ArgumentError)


17
18
19
20
21
22
# File 'lib/nuckle/secret_box.rb', line 17

def initialize(key)
  key = key.b
  raise ArgumentError, "key must be #{KEYBYTES} bytes (got #{key.bytesize})" unless key.bytesize == KEYBYTES

  @key = key
end

Instance Method Details

#decrypt(nonce, ciphertext) ⇒ String Also known as: open

Decrypt ciphertext with 24-byte nonce.

Parameters:

  • nonce (String)

    24-byte nonce

  • ciphertext (String)

    authenticator (16 bytes) + ciphertext

Returns:

  • (String)

    plaintext

Raises:



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/nuckle/secret_box.rb', line 63

def decrypt(nonce, ciphertext)
  nonce      = nonce.b
  ciphertext = ciphertext.b
  raise ArgumentError, "nonce must be #{NONCEBYTES} bytes" unless nonce.bytesize == NONCEBYTES
  raise CryptoError, "ciphertext too short" if ciphertext.bytesize < MACBYTES

  mac = ciphertext.byteslice(0, MACBYTES)
  ct  = ciphertext.byteslice(MACBYTES..)

  # Generate Poly1305 key from first 32 bytes of XSalsa20 keystream
  poly_key = Internals::Salsa20.xsalsa20_stream(@key, nonce, ZEROBYTES)

  # Verify MAC
  expected_mac = Internals::Poly1305.mac(poly_key, ct)
  raise CryptoError, "decryption failed" unless Util.verify16(mac, expected_mac)

  # Decrypt
  padded = ("\x00" * ZEROBYTES + ct).b
  m      = Internals::Salsa20.xsalsa20_xor(@key, nonce, padded)
  m.byteslice(ZEROBYTES..)
end

#encrypt(nonce, plaintext) ⇒ String Also known as: box

Encrypt plaintext with 24-byte nonce.

Parameters:

  • nonce (String)

    24-byte nonce

  • plaintext (String)

Returns:

  • (String)

    authenticator (16 bytes) + ciphertext

Raises:

  • (ArgumentError)


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/nuckle/secret_box.rb', line 36

def encrypt(nonce, plaintext)
  nonce     = nonce.b
  plaintext = plaintext.b
  raise ArgumentError, "nonce must be #{NONCEBYTES} bytes" unless nonce.bytesize == NONCEBYTES

  # Pad with 32 zero bytes
  padded = ("\x00" * ZEROBYTES + plaintext).b
  c      = Internals::Salsa20.xsalsa20_xor(@key, nonce, padded)

  # First 32 bytes of c: bytes 0..31 of XSalsa20 keystream XOR'd with zeros
  # → bytes 0..31 ARE the keystream. Use first 32 as Poly1305 one-time key.
  poly_key = c.byteslice(0, ZEROBYTES)
  mac      = Internals::Poly1305.mac(poly_key, c.byteslice(ZEROBYTES..))

  # Return: 16-byte MAC + ciphertext (skip first 16 zero bytes of c)
  # Actually NaCl convention: c[0..15] are zeros, c[16..31] replaced by mac
  mac + c.byteslice(ZEROBYTES..)
end

#key_bytesInteger

Returns required key length in bytes.

Returns:

  • (Integer)

    required key length in bytes



28
# File 'lib/nuckle/secret_box.rb', line 28

def key_bytes   = KEYBYTES

#nonce_bytesInteger

Returns required nonce length in bytes.

Returns:

  • (Integer)

    required nonce length in bytes



26
27
# File 'lib/nuckle/secret_box.rb', line 26

def nonce_bytes = NONCEBYTES
# @return [Integer] required key length in bytes