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
-
.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.
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.
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 |