Module: Rijn

Defined in:
lib/rijn/crypto.rb,
lib/rijn/errors.rb,
lib/rijn/version.rb

Defined Under Namespace

Classes: AuthenticationError, InvalidKeyError

Constant Summary collapse

KEY_LENGTHS =
{
  128 => 16,
  192 => 24,
  256 => 32
}.freeze
CIPHERS =
{
  16 => "aes-128-gcm",
  24 => "aes-192-gcm",
  32 => "aes-256-gcm"
}.freeze
NONCE_LENGTH =

Rijn uses a 12-byte nonce for AES-GCM.

12
AUTH_TAG_LENGTH =

Rijn stores a 16-byte authentication tag.

16
VERSION =

The current version of the Rijn library.

"0.4.0"

Class Method Summary collapse

Class Method Details

.cipher_for(key) ⇒ Object



113
114
115
# File 'lib/rijn/crypto.rb', line 113

def self.cipher_for(key)
  OpenSSL::Cipher.new(CIPHERS.fetch(key.bytesize))
end

.decode_key(key) ⇒ Object



101
102
103
104
105
# File 'lib/rijn/crypto.rb', line 101

def self.decode_key(key)
  Base64.strict_decode64(key)
rescue ArgumentError
  raise InvalidKeyError, "Key is not valid Base64."
end

.decrypt(encrypted_value, key) ⇒ String

Decrypts a value encrypted with AES-GCM.

Parameters:

  • encrypted_value (String)

    Base64-encoded encrypted value

  • key (String)

    Base64-encoded encryption key (128, 192, or 256 bits)

Returns:

  • (String)

    the decrypted plaintext value

Raises:



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/rijn/crypto.rb', line 69

def self.decrypt(encrypted_value, key)
  key = decode_key(key)
  validate_key!(key)

  # Rijn stores encrypted values as:
  #
  #   nonce (12 bytes) + ciphertext + authentication tag (16 bytes)
  #
  # The nonce is not secret and is required for decryption.
  # The authentication tag is used by AES-GCM to verify the ciphertext.
  decoded = Base64.strict_decode64(encrypted_value)

  nonce = decoded.byteslice(0, NONCE_LENGTH)
  ciphertext = decoded.byteslice(NONCE_LENGTH...-AUTH_TAG_LENGTH)
  tag = decoded.byteslice(-AUTH_TAG_LENGTH, AUTH_TAG_LENGTH)

  cipher = cipher_for(key)
  cipher.decrypt

  cipher.key = key
  cipher.iv = nonce
  cipher.auth_tag = tag

  begin
    cipher.update(ciphertext) + cipher.final
  rescue OpenSSL::Cipher::CipherError
    raise AuthenticationError, "Unable to decrypt value."
  end
end

.encrypt(value, key) ⇒ String

Encrypts a value using AES-GCM.

Parameters:

  • value (String)

    the plaintext value to encrypt

  • key (String)

    Base64-encoded encryption key (128, 192, or 256 bits)

Returns:

  • (String)

    Base64-encoded encrypted value

Raises:



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/rijn/crypto.rb', line 43

def self.encrypt(value, key)
  key = decode_key(key)
  validate_key!(key)

  cipher = cipher_for(key)
  cipher.encrypt

  nonce = SecureRandom.random_bytes(NONCE_LENGTH)

  cipher.key = key
  cipher.iv = nonce

  ciphertext = cipher.update(value) + cipher.final

  tag = cipher.auth_tag

  Base64.strict_encode64(nonce + ciphertext + tag)
end

.generate_key(bits = 256) ⇒ String

Generates a cryptographically secure AES-GCM encryption key.

Parameters:

  • bits (Integer) (defaults to: 256)

    key size in bits; must be 128, 192, or 256

Returns:

  • (String)

    Base64-encoded encryption key

Raises:



29
30
31
32
33
34
35
# File 'lib/rijn/crypto.rb', line 29

def self.generate_key(bits = 256)
  length = KEY_LENGTHS.fetch(bits) do
    raise InvalidKeyError, "Key must be 128, 192, or 256 bits."
  end

  Base64.strict_encode64(SecureRandom.random_bytes(length))
end

.validate_key!(key) ⇒ Object



107
108
109
110
111
# File 'lib/rijn/crypto.rb', line 107

def self.validate_key!(key)
  unless KEY_LENGTHS.value?(key.bytesize)
    raise InvalidKeyError, "Key must be 128, 192, or 256 bits."
  end
end