Module: PWS::Format::V1_0

Defined in:
lib/pws/format/1.0.rb

Overview

PWS file format for versions ~> 1.0.0 see at bottom block for a short format description

Constant Summary collapse

TEMPLATE =
'a64 a16 N a64 a*'.freeze
DEFAULT_ITERATIONS =
75_000
MAX_ITERATIONS =
10_000_000
MAX_ENTRY_LENGTH =

N

4_294_967_295

Class Method Summary collapse

Class Method Details

.decrypt(saved_data, options = {}) ⇒ Object

Raises:

  • (ArgumentError)


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/pws/format/1.0.rb', line 71

def decrypt(saved_data, options = {})
  raise ArgumentError, 'No password given' if \
      !options[:password]
  raise ArgumentError, 'No data given' if \
      !saved_data || saved_data.empty?
  salt, iv, iterations, sha, encrypted_data = saved_data.unpack(TEMPLATE)

  raise NoAccess, 'Password file invalid' if \
      salt.size != 64             ||
      iterations > MAX_ITERATIONS ||
      iv.size != 16               ||
      sha.size != 64

  encryption_key, hmac_key = kdf(
    options[:password],
    salt,
    iterations,
  ).unpack('a256 a256')

  begin
    unencrypted_data = Encryptor.decrypt(
      encrypted_data,
      key: encryption_key[0...32],
      iv:  iv,
    )
  rescue OpenSSL::Cipher::CipherError
    raise NoAccess, 'Could not decrypt'
  end

  raise NoAccess, 'Password file invalid' unless \
      sha == hmac(hmac_key, salt, iv, iterations, unencrypted_data)

  unencrypted_data
end

.encrypt(unencrypted_data, options = {}) ⇒ Object

Raises:

  • (ArgumentError)


23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/pws/format/1.0.rb', line 23

def encrypt(unencrypted_data, options = {})
  raise ArgumentError, 'No password given' if \
      !options[:password]

  iterations = ( options[:iterations] || DEFAULT_ITERATIONS ).to_i
  raise ArgumentError, 'Invalid iteration count given' if \
      iterations > MAX_ITERATIONS || iterations < 2

  salt = SecureRandom.random_bytes(64)
  iv  = Encryptor.random_iv

  encryption_key, hmac_key = kdf(
    options[:password],
    salt,
    iterations,
  ).unpack('a256 a256')

  sha = hmac(hmac_key, salt, iv, iterations, unencrypted_data)

  encrypted_data = Encryptor.encrypt(
    unencrypted_data,
    key: encryption_key[0...32],
    iv:  iv,
  )

  [salt, iv, iterations, sha, encrypted_data].pack(TEMPLATE)
end

.hmac(key, *strings) ⇒ Object

support



126
127
128
129
130
# File 'lib/pws/format/1.0.rb', line 126

def hmac(key, *strings)
  OpenSSL::HMAC.new(key, OpenSSL::Digest::SHA512.new).update(
    strings.map(&:to_s).join
  ).digest
end

.kdf_openssl(password, salt, iterations) ⇒ Object



132
133
134
135
136
137
138
139
140
# File 'lib/pws/format/1.0.rb', line 132

def kdf_openssl(password, salt, iterations)
  OpenSSL::PKCS5::pbkdf2_hmac(
    password,
    salt,
    iterations,
    512,
    OpenSSL::Digest::SHA512.new,
  )
end

.kdf_ruby(password, salt, iterations) ⇒ Object



142
143
144
145
146
147
148
149
150
# File 'lib/pws/format/1.0.rb', line 142

def kdf_ruby(password, salt, iterations)
  PBKDF2.new(
    password: password,
    salt: salt,
    iterations: iterations,
    key_length: 512,
    hash_function: OpenSSL::Digest::SHA512,
  ).bin_string
end

.marshal(application_data, options = {}) ⇒ Object



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

def marshal(application_data, options = {})
  number_of_dummy_bytes = 100_000 + SecureRandom.random_number(1_000_000)
  ordered_data = application_data.to_a
  [
    number_of_dummy_bytes,
    application_data.size,
    SecureRandom.random_bytes(number_of_dummy_bytes) +
    array_to_data_string(ordered_data.map{ |_, e| e[:password].to_s }) +
    array_to_data_string(ordered_data.map{ |k, _| k.to_s }) +
    array_to_data_string(ordered_data.map{ |_, e| e[:timestamp].to_i }) +
    SecureRandom.random_bytes(100_000 + SecureRandom.random_number(1_000_000))
  ].pack('N N a*')
end

.read(encrypted_data, options = {}) ⇒ Object

    • -



67
68
69
# File 'lib/pws/format/1.0.rb', line 67

def read(encrypted_data, options = {})
  unmarshal(decrypt(encrypted_data, options))
end

.unmarshal(saved_data, options = {}) ⇒ Object



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/pws/format/1.0.rb', line 106

def unmarshal(saved_data, options = {})
  number_of_dummy_bytes, data_size, raw_data = saved_data.unpack('N N a*')
  i = number_of_dummy_bytes
  passwords, names, timestamps = 3.times.map{
    data_size.times.map{
      next_element, i = get_next_data_string(raw_data, i)
      next_element
    }
  }
  Hash[
    names.zip(
      passwords.zip(timestamps).map{ |e,f|
        { password: e.to_s, timestamp: f.to_i }
      }
    )
  ]
end

.write(application_data, options = {}) ⇒ Object



19
20
21
# File 'lib/pws/format/1.0.rb', line 19

def write(application_data, options = {})
  encrypt(marshal(application_data), options)
end