Class: CoinOp::Crypto::PassphraseBox

Inherits:
Object
  • Object
show all
Extended by:
Encodings
Includes:
Encodings
Defined in:
lib/coin-op/crypto.rb

Overview

A wrapper for NaCl’s Secret Box, taking a user-supplied passphrase and deriving a secret key, rather than using a (far more secure) randomly generated secret key.

NaCl Secret Box provides a high level interface for authenticated symmetric encryption. When creating the box, you must supply a key. When using the box to encrypt, you must supply a random nonce. Nonces must never be re-used.

Secret Box decryption requires the ciphertext and the nonce used to create it.

The PassphraseBox class takes a passphrase, rather than a randomly generated key. It uses PBKDF2 to generate a key that, while not random, is somewhat resistant to brute force attacks. Great care should still be taken to avoid passphrases that are subject to dictionary attacks.

Constant Summary collapse

ITERATIONS =

PBKDF2 work factor

90_000
ITERATIONS_WINDOW =
20_000
SALT_RANDOM_BYTES =
16
KEY_SIZE =
32
AES_CIPHER =
'AES-256-CBC'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Encodings

base58, decode_base58, decode_hex, hex, int_to_byte_array

Constructor Details

#initialize(passphrase, mode = :aes, salt = SecureRandom.random_bytes(SALT_RANDOM_BYTES), iterations = nil) ⇒ PassphraseBox

Initialize with an existing salt and iterations to allow decryption. Otherwise, creates new values for these, meaning it creates an entirely new secret-box.



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
# File 'lib/coin-op/crypto.rb', line 71

def initialize(passphrase, mode=:aes, salt=SecureRandom.random_bytes(SALT_RANDOM_BYTES), iterations=nil)
  @salt = salt 
  @iterations = iterations || ITERATIONS + SecureRandom.random_number(ITERATIONS_WINDOW) 
  @mode = mode

  if @mode == :aes
    @key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(
      passphrase,
      @salt,
      # TODO: decide on a very safe work factor
      # https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet
      #
      @iterations, # number of iterations
      KEY_SIZE * 2 # key length in bytes
    )

    @aes_key = @key[0, KEY_SIZE]
    @hmac_key = @key[KEY_SIZE, KEY_SIZE]
    @cipher = OpenSSL::Cipher.new(AES_CIPHER)
    @cipher.padding = 0
  elsif @mode == :nacl
    @key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(
      passphrase,
      @salt,
      # TODO: decide on a very safe work factor
      # https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet
      #
      @iterations, # number of iterations
      KEY_SIZE     # key length in bytes
    )
    @box = RbNaCl::SecretBox.new(@key)
  end

end

Instance Attribute Details

#saltObject (readonly)

Returns the value of attribute salt.



66
67
68
# File 'lib/coin-op/crypto.rb', line 66

def salt
  @salt
end

Class Method Details

.decrypt(passphrase, hash) ⇒ Object

PassphraseBox.decrypt “my great password”,

:salt => salt, :nonce => nonce, :ciphertext => ciphertext


51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/coin-op/crypto.rb', line 51

def self.decrypt(passphrase, hash)
  salt, iv, nonce, ciphertext =
    hash.values_at(:salt, :iv, :nonce, :ciphertext).map {|s| decode_hex(s) }

  mode = nil
  if iv.empty?
    mode = :nacl
  elsif nonce.empty?
    mode = :aes
  end
  
  box = self.new(passphrase, mode, salt, hash[:iterations] || ITERATIONS)
  box.decrypt(iv, nonce, ciphertext)
end

.encrypt(passphrase, plaintext) ⇒ Object

Given passphrase and plaintext as strings, returns a Hash containing the ciphertext and other values needed for later decryption. Binary values are encoded as hexadecimal strings.



43
44
45
46
# File 'lib/coin-op/crypto.rb', line 43

def self.encrypt(passphrase, plaintext)
  box = self.new(passphrase)
  box.encrypt(plaintext)
end

Instance Method Details

#decrypt(iv, nonce, ciphertext) ⇒ Object



123
124
125
126
127
128
129
130
# File 'lib/coin-op/crypto.rb', line 123

def decrypt(iv, nonce, ciphertext)
  if @mode == :aes
    return decrypt_aes(iv, ciphertext)
  elsif @mode == :nacl
    return decrypt_nacl(nonce, ciphertext)
  end
  raise('Incompatible ciphertext')
end

#decrypt_aes(iv, ciphertext) ⇒ Object



132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/coin-op/crypto.rb', line 132

def decrypt_aes(iv, ciphertext)
  mac, ctext = ciphertext[-KEY_SIZE, KEY_SIZE], ciphertext[0...-KEY_SIZE]
  digest = OpenSSL::Digest::SHA256.new
  hmac_digest = OpenSSL::HMAC.digest(digest, @hmac_key, iv + ctext)
  if hmac_digest != mac
    raise('Invalid authentication code - this ciphertext may have been tampered with.')
  end
  @cipher.decrypt
  @cipher.iv = iv
  @cipher.key = @aes_key
  decrypted = @cipher.update(ctext)
  decrypted << @cipher.final
end

#decrypt_nacl(nonce, ciphertext) ⇒ Object



146
147
148
# File 'lib/coin-op/crypto.rb', line 146

def decrypt_nacl(nonce, ciphertext)
  @box.decrypt(nonce, ciphertext)
end

#encrypt(plaintext, iv = @cipher.random_iv) ⇒ Object



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/coin-op/crypto.rb', line 106

def encrypt(plaintext, iv=@cipher.random_iv)
  @cipher.encrypt
  @cipher.iv = iv
  @cipher.key = @aes_key
  encrypted = @cipher.update(plaintext)
  encrypted << @cipher.final
  digest = OpenSSL::Digest::SHA256.new
  hmac_digest = OpenSSL::HMAC.digest(digest, @hmac_key, iv + encrypted)
  ciphertext = encrypted + hmac_digest
  {
    iterations: @iterations,
    salt: hex(@salt),
    iv: hex(iv),
    ciphertext: hex(ciphertext)
  }
end