Module: Armor

Defined in:
lib/armor.rb

Constant Summary collapse

Digest =
RUBY_VERSION >= "2.1.0" ? OpenSSL::Digest : OpenSSL::Digest::Digest

Class Method Summary collapse

Class Method Details

.compare(a, b) ⇒ Object

Time-attack safe comparison operator.



100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/armor.rb', line 100

def self.compare(a, b)
  return false unless a.length == b.length

  cmp = b.bytes.to_a
  result = 0

  a.bytes.each_with_index do |char, index|
    result |= char ^ cmp[index]
  end

  return result == 0
end

.concatenate(digest, password, salt, iterations, i) ⇒ Object

The function F is the xor (^) of c iterations of chained PRFs. The first iteration of PRF uses Password as the PRF key and Salt concatenated to i encoded as a big-endian 32-bit integer.

Note that i is a 1-based index. Subsequent iterations of PRF use Password as the PRF key and the output of the previous PRF computation as the salt:

Definition:

F(Password, Salt, Iterations, i) = U1 ^ U2 ^ ... ^ Uc


77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/armor.rb', line 77

def self.concatenate(digest, password, salt, iterations, i)

  # U1 -> password, salt and 1 encoded as big-endian 32-bit integer.
  u = randomize(digest, password, salt + [i].pack("N"))

  ret = u

  # U2 through Uc:
  2.upto(iterations) do

    # calculate Un
    u = randomize(digest, password, u)

    # xor it with the previous results
    ret = xor(ret, u)
  end

  ret
end

.digest(password, salt) ⇒ Object

Syntactic sugar for the underlying mathematical definition of PBKDF2.

This function does not allow passing in of a dkLen (in other words does not allow truncation of the final derived key).

Returns: Binary representation of the derived key (DK).

The default value for iter is set to 5000, and it can be configured via ‘ENV`.

The default value for hash is “sha512”, and it can also be configured via ‘ENV`.



18
19
20
21
22
23
24
25
26
# File 'lib/armor.rb', line 18

def self.digest(password, salt)
  iter = ENV["ARMOR_ITER"] || 5000
  hash = ENV["ARMOR_HASH"] || "sha512"

  digest = Digest.new(hash)
  length = digest.digest_length

  hex(pbkdf2(digest, password, salt, Integer(iter), length))
end

.hex(str) ⇒ Object

Binary to hex convenience method.



33
34
35
# File 'lib/armor.rb', line 33

def self.hex(str)
  str.unpack("H*").first
end

.pbkdf2(digest, password, salt, c, dk_len) ⇒ Object

The PBKDF2 key derivation function has five input parameters:

DK = PBKDF2(PRF, Password, Salt, c, dkLen)

where:

  • PRF is a pseudorandom function of two parameters

  • Password is the master password from which a derived key is generated

  • Salt is a cryptographic salt

  • c is the number of iterations desired

  • dkLen is the desired length of the derived key

    DK is the generated derived key.



51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/armor.rb', line 51

def self.pbkdf2(digest, password, salt, c, dk_len)
  blocks_needed = (dk_len.to_f / digest.size).ceil

  result = ""

  # main block-calculating loop:
  1.upto(blocks_needed) do |n|
    result << concatenate(digest, password, salt, c, n)
  end

  # truncate to desired length:
  result.slice(0, dk_len)
end

.randomize(digest, password, seed) ⇒ Object



28
29
30
# File 'lib/armor.rb', line 28

def self.randomize(digest, password, seed)
  OpenSSL::HMAC.digest(digest, password, seed)
end

.xor(a, b) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
# File 'lib/armor.rb', line 113

def self.xor(a, b)
  result = "".encode("ASCII-8BIT")

  b_bytes = b.bytes.to_a

  a.bytes.each_with_index do |c, i|
    result << (c ^ b_bytes[i])
  end

  result
end