Module: SessionKeys

Defined in:
lib/session_keys.rb,
lib/session_keys/version.rb

Overview

SessionKeys deterministic cryptographic key generation.

Constant Summary collapse

SCRYPT_OPSLIMIT_INTERACTIVE =

Opslimit represents a maximum amount of computations to perform. Raising this number will make the function require more CPU cycles to compute a key.

Number of scrypt computations for scrypt to perform for interactive security setting. Set to SCRYPT_MEMLIMIT_INTERACTIVE / 32

For interactive, online operations, ‘SCRYPT_OPSLIMIT_INTERACTIVE` and `SCRYPT_MEMLIMIT_INTERACTIVE` provide a safe base line for these two parameters. However, using higher values may improve security.

See : download.libsodium.org/doc/password_hashing/scrypt.html

2**19
SCRYPT_MEMLIMIT_INTERACTIVE =

Memlimit is the maximum amount of RAM that the function will use, in bytes. It is highly recommended to allow the function to use at least 16 megabytes.

Max RAM in Bytes to be used by scrypt for interactive security setting.

2**24
SCRYPT_OPSLIMIT_SENSITIVE =

Number of scrypt computations for scrypt to perform for sensitive security setting. Set to SCRYPT_MEMLIMIT_SENSITIVE / 32

For highly sensitive data, ‘SCRYPT_OPSLIMIT_SENSITIVE` and `SCRYPT_MEMLIMIT_SENSITIVE` can be used as an alternative. But with these parameters, deriving a key takes about 2 seconds on a 2.8 Ghz Core i7 CPU and requires up to 1 gigabyte of dedicated RAM.

2**25
SCRYPT_MEMLIMIT_SENSITIVE =

Max RAM in Bytes to be used by scrypt for sensitive security setting.

2**30
SCRYPT_DIGEST_SIZE_ID =

Size in Bytes of the scrypt derived output for the id

32
SCRYPT_DIGEST_SIZE_PASSWORD =

Size in Bytes of the scrypt derived output for the password

256
PEPPER =

A site-wide 32 Byte common random value that will be concatenated with a value being hashed for some additional measure of security against dictionary style attacks. This value was randomly chosen but must be the same across implementations and is assumed public.

'f01f0a0c44a2d1e7e5b00d7dc78941d404474a90ce7f4ae9d1432bf76fa169e7'.freeze
VERSION =
'0.1.0'.freeze

Class Method Summary collapse

Class Method Details

.generate(id, password, strength = :sensitive, min_password_entropy = 75) ⇒ Hash

Deterministically generates a collection of derived encryption key material from a provided id and password. Uses SHA256 and scrypt for key derivation.

Parameters:

  • id (String)

    a unique UTF-8 String identifier such as a username or email address. Max length 256 characters.

  • password (String)

    a cryptographically strong UTF-8 password or passphrase. Max length 256 characters.

  • strength (Symbol) (defaults to: :sensitive)

    the desired strength of the key derivation. Can be the symbols :interactive or (:sensitive).

  • min_password_entropy (Integer) (defaults to: 75)

    the minimum (75) estimated entropy allowed for the password. This will be measured with Zxcvbn.

Returns:

  • (Hash)

    returns a Hash of keys and derived key material.

Raises:

  • (ArgumentError)

    if invalid arguments are provided.



67
68
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/session_keys.rb', line 67

def self.generate(id, password, strength = :sensitive, min_password_entropy = 75)
  unless id.is_a?(String) && id.encoding.name == 'UTF-8'
    raise ArgumentError, 'invalid id, not a UTF-8 string'
  end

  unless id.length.between?(1,256)
    raise ArgumentError, 'invalid id, must be between 1 and 256 characters in length'
  end

  unless password.is_a?(String) && password.encoding.name == 'UTF-8'
    raise ArgumentError, 'invalid password, not a UTF-8 string'
  end

  # Enforce max length only due to Zxcvbn taking a *long* time to
  # process long strings and determine entropy.
  unless password.length.between?(1,256)
    raise ArgumentError, 'invalid password, must be between 1 and 256 characters in length'
  end

  unless min_password_entropy.is_a?(Integer) && min_password_entropy.between?(1, 512)
    raise ArgumentError, 'invalid min_password_entropy, must be an Integer between 1 and 512'
  end

  password_test = Zxcvbn.test(password)
  unless password_test.entropy.round >= min_password_entropy
    raise ArgumentError, "invalid password, must be at least #{min_password_entropy} bits of estimated entropy"
  end

  unless [:interactive, :sensitive].include?(strength)
    raise ArgumentError, 'invalid strength, must be :interactive (min), or :sensitive (strong)'
  end

  start_processing_time = Time.now

  # Run the ID and a 'pepper' (an app common salt) through scrypt. This will be
  # the system ID for this user. This processing is done to prevent knowledge
  # of the user on the server side and prevent the ability to reverse this
  # ID back into a username or email. Using scrypt instead of a SHA256 Hash
  # is so that it will also take unreasonable effort for someone with a list
  # of user identifiers from looking up users on the system quickly even if
  # provided with a local copy of the DB.
  id_sha256_bytes = RbNaCl::Hash.sha256(id.bytes.pack('C*'))

  id_sha256_pepper_bytes = RbNaCl::Hash.sha256(
    "#{id}#{id.length}#{PEPPER}#{PEPPER.length}".bytes.pack('C*')
  )

  id_scrypt_hex = RbNaCl::PasswordHash.scrypt(
    id_sha256_bytes,
    id_sha256_pepper_bytes,
    SCRYPT_OPSLIMIT_INTERACTIVE,
    SCRYPT_MEMLIMIT_INTERACTIVE,
    SCRYPT_DIGEST_SIZE_ID
  ).bytes.map { |byte| '%02x' % byte }.join

  # libsodium : By design, a password whose length is 65 bytes or more is
  # reduced to SHA-256(password). This can have security implications if the
  # password is present in another password database using raw, unsalted
  # SHA-256. Or when upgrading passwords previously hashed with unsalted
  # SHA-256 to scrypt. If this is a concern, passwords should be pre-hashed
  # before being hashed using scrypt.
  password_sha256_bytes = RbNaCl::Hash.sha256(password.bytes.pack('C*'))

  password_sha256_pepper_bytes = RbNaCl::Hash.sha256(
    "#{id_scrypt_hex}#{id_scrypt_hex.length}#{PEPPER}#{PEPPER.length}".bytes.pack('C*')
  )

  # Derive SCRYPT_DIGEST_SIZE_PASSWORD secret bytes. They will be split
  # into 32 Byte chunks to serve as deterministic seeds for ID or key
  # generation. Some derived bytes are reserved for future use.
  password_digest = RbNaCl::PasswordHash.scrypt(
    password_sha256_bytes,
    password_sha256_pepper_bytes,
    strength == :interactive ? SCRYPT_OPSLIMIT_INTERACTIVE : SCRYPT_OPSLIMIT_SENSITIVE,
    strength == :interactive ? SCRYPT_MEMLIMIT_INTERACTIVE : SCRYPT_MEMLIMIT_SENSITIVE,
    SCRYPT_DIGEST_SIZE_PASSWORD
  ).bytes

  # Break up the scrypt digest into 32 Byte seeds.
  secret_bytes = []
  (SCRYPT_DIGEST_SIZE_PASSWORD/32).times { secret_bytes << password_digest.shift(32) }

  # Seed 0 : RbNaCl::SimpleBox
  # The seed bytes are used as a 32 Byte key suitable for
  # simple symetric key encryption using `RbNaCl::SimpleBox`. SimpleBox is
  # a wrapper around NaCl SecretBox construct with automated nonce management.
  #
  # To encrypt/decreypt with this object try:
  #  ciphertext = nacl_simple_box.encrypt('foobar')
  #  plaintext = nacl_simple_box.decrypt(ciphertext)
  nacl_simple_box_key = secret_bytes[0].pack('C*').force_encoding('ASCII-8BIT')
  nacl_simple_box = RbNaCl::SimpleBox.from_secret_key(nacl_simple_box_key)

  # Seed 1 : NaCl Box Keypair
  nacl_enc_sec_seed = secret_bytes[1].pack('C*').force_encoding('ASCII-8BIT')
  nacl_enc_sec_key = RbNaCl::PrivateKey.new(nacl_enc_sec_seed)
  nacl_enc_pub_key = nacl_enc_sec_key.public_key

  # Seed 2 : NaCl Signing Keypair
  nacl_sig_sec_seed = secret_bytes[2].pack('C*').force_encoding('ASCII-8BIT')
  nacl_sig_sec_key = RbNaCl::SigningKey.new(nacl_sig_sec_seed)
  nacl_sig_pub_key = nacl_sig_sec_key.verify_key

  # Seed 3 : Reserved for future use.
  # Seed 4 : Reserved for future use.
  # Seed 5 : Reserved for future use.
  # Seed 6 : Reserved for future use.
  # Seed 7 : Reserved for future use.

  {
    id: id_scrypt_hex,
    nacl_simple_box: nacl_simple_box,
    nacl_enc_pub_key: nacl_enc_pub_key,
    nacl_enc_sec_key: nacl_enc_sec_key,
    nacl_sig_pub_key: nacl_sig_pub_key,
    nacl_sig_sec_key: nacl_sig_sec_key,
    nacl_enc_pub_key_b64: Base64.strict_encode64(nacl_enc_pub_key.to_bytes),
    nacl_sig_pub_key_b64: Base64.strict_encode64(nacl_sig_pub_key.to_bytes),
    process_time: ((Time.now - start_processing_time)*1000).round(2)
  }
end