opensecret 0.0.988 → 0.0.9925

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +56 -159
  3. data/bin/opensecret +2 -2
  4. data/bin/ops +17 -2
  5. data/lib/extension/string.rb +14 -16
  6. data/lib/{interpreter.rb → interprete.rb} +53 -29
  7. data/lib/keytools/binary.map.rb +49 -0
  8. data/lib/keytools/kdf.api.rb +249 -0
  9. data/lib/keytools/kdf.bcrypt.rb +64 -29
  10. data/lib/keytools/kdf.pbkdf2.rb +92 -83
  11. data/lib/keytools/kdf.scrypt.rb +190 -0
  12. data/lib/keytools/key.64.rb +326 -0
  13. data/lib/keytools/key.algo.rb +109 -0
  14. data/lib/keytools/key.api.rb +1281 -0
  15. data/lib/keytools/key.db.rb +265 -0
  16. data/lib/keytools/{key.module.rb → key.docs.rb} +55 -0
  17. data/lib/keytools/key.error.rb +110 -0
  18. data/lib/keytools/key.id.rb +271 -0
  19. data/lib/keytools/key.iv.rb +107 -0
  20. data/lib/keytools/key.local.rb +265 -0
  21. data/lib/keytools/key.mach.rb +248 -0
  22. data/lib/keytools/key.now.rb +402 -0
  23. data/lib/keytools/key.pair.rb +259 -0
  24. data/lib/keytools/key.pass.rb +120 -0
  25. data/lib/keytools/key.rb +428 -298
  26. data/lib/keytools/keydebug.txt +295 -0
  27. data/lib/logging/gem.logging.rb +3 -3
  28. data/lib/modules/cryptology/collect.rb +20 -0
  29. data/lib/session/require.gem.rb +1 -1
  30. data/lib/usecase/cmd.rb +417 -0
  31. data/lib/usecase/id.rb +36 -0
  32. data/lib/usecase/import.rb +174 -0
  33. data/lib/usecase/init.rb +78 -0
  34. data/lib/usecase/login.rb +70 -0
  35. data/lib/usecase/logout.rb +30 -0
  36. data/lib/usecase/open.rb +126 -0
  37. data/lib/{interprete → usecase}/put.rb +100 -47
  38. data/lib/usecase/read.rb +89 -0
  39. data/lib/{interprete → usecase}/safe.rb +0 -0
  40. data/lib/{interprete → usecase}/set.rb +0 -0
  41. data/lib/usecase/token.rb +111 -0
  42. data/lib/{interprete → usecase}/use.rb +0 -0
  43. data/lib/version.rb +1 -1
  44. data/opensecret.gemspec +4 -3
  45. metadata +39 -33
  46. data/lib/exception/cli.error.rb +0 -53
  47. data/lib/exception/errors/cli.errors.rb +0 -31
  48. data/lib/interprete/begin.rb +0 -232
  49. data/lib/interprete/cmd.rb +0 -621
  50. data/lib/interprete/export.rb +0 -163
  51. data/lib/interprete/init.rb +0 -205
  52. data/lib/interprete/key.rb +0 -119
  53. data/lib/interprete/open.rb +0 -148
  54. data/lib/interprete/seal.rb +0 -129
  55. data/lib/keytools/digester.rb +0 -245
  56. data/lib/keytools/key.data.rb +0 -227
  57. data/lib/keytools/key.derivation.rb +0 -341
  58. data/lib/modules/mappers/collateral.rb +0 -282
  59. data/lib/modules/mappers/envelope.rb +0 -127
  60. data/lib/modules/mappers/settings.rb +0 -170
  61. data/lib/notepad/scratch.pad.rb +0 -224
  62. data/lib/store-commands.txt +0 -180
@@ -15,7 +15,17 @@ module OpenKey
15
15
  # performed to create the key.
16
16
  #
17
17
  # <b>One million (1,000,000) should be the iteration count's lower bound.</b>
18
- class Pbkdf2KeyGen
18
+ #
19
+ # == Upgrading the OpenSSL <em>pbkdf2_hmac</em> Behaviour
20
+ #
21
+ # As soon as the new Ruby and OpenSSL libraries become commonplace this class should
22
+ # be upgraded to use the <b>new and improved {OpenSSL::KDF.pbkdf2_hmac} behaviour</b>
23
+ # rather than {OpenSSL::PKCS5.pbkdf2_hmac}.
24
+ #
25
+ # The difficulty is in detecting the operating system's C libraries that are directly
26
+ # accessed for OpenSSL functionality. If the distinction can be made accurately, those
27
+ # with newer libraries can reap the benefits immediately.
28
+ class KeyPbkdf2
19
29
 
20
30
 
21
31
  # <b>One million iterations</b> is necessary due to the
@@ -25,46 +35,59 @@ module OpenKey
25
35
  PBKDF2_ITERATION_COUNT = 1000000
26
36
 
27
37
 
28
- # The current output key length needed is 96 bits which is
29
- # 12 bytes. However the algorithm's minimum byte length is
30
- # 16 so that is what we must use.
31
- PBKDF2_OUTPUT_KEY_LENGTH = 16
32
-
33
-
34
- # The documented recommended salt length in bytes is in between
35
- # 16 and 24 bytes. The setting here is at the upper bound of
36
- # that range.
37
- PBKDF2_SALT_LENGTH_BITS = 24 * 8
38
-
39
-
40
- # When the key is transported using a 64 character set where
41
- # each character is represented by 6 bits - the PBKDF2 key
42
- # expands to 132 bits rather than the original 128 bits.
38
+ # Documentation for this algorithm says this about the key length.
43
39
  #
44
- # This expansion is because of the remainder.
40
+ # Make the key length <b>larger than or equal to the output length</b>
41
+ # of the <b>underlying digest function</b>, otherwise an attacker could
42
+ # simply try to brute-force the key.
45
43
  #
46
- # 128 bits divided by 6 is 21 characters plus a remainder of two
47
- # (2) bits which must be transported one extra base64 character.
44
+ # According to PKCS#5, security is limited by the output length of
45
+ # the underlying digest function, i.e. security is not improved if a
46
+ # key length strictly larger than the digest output length is chosen.
48
47
  #
49
- # Hence the 22 transported characters are then observed to
50
- # be 132 bits in length (22 times 6).
51
- PBKDF2_KEY_TRANSPORT_LENGTH = 132
48
+ # Therefore, when using PKCS5 for password storage, it suffices to
49
+ # store values equal to the digest output length, nothing is gained
50
+ # by storing larger values.
51
+ PBKDF2_EXPORT_KEY_LENGTH = OpenSSL::Digest::SHA384.new.digest_length
52
+
52
53
 
53
-
54
+ # For a 384 bit digest the key length is 48 bytes and the bit length
55
+ # is 384 bits.
56
+ PBKDF2_EXPORT_BIT_LENGTH = PBKDF2_EXPORT_KEY_LENGTH * 8
54
57
 
55
- # Retun a random cryptographic salt generated from twenty-four
58
+
59
+
60
+ # The documented recommended salt length in bytes for the PBKDF2
61
+ # algorithm is between <b>16 and 24 bytes</b>. The setting here is
62
+ # at the upper bound of that range.
63
+ PBKDF2_SALT_LENGTH_BYTES = 24
64
+
65
+
66
+ # Return a random cryptographic salt generated from twenty-four
56
67
  # random bytes produced by a secure random number generator. The
57
- # returned salt is Base64 encoded.
68
+ # returned salt is a Base64 encoded string that can be stored
69
+ # and given straight into the {KeyPbkdf2.generate_key} method.
70
+ #
71
+ # + ------------ + -------- + ------------ + ------------------- +
72
+ # | | Bits | Bytes | Base64 |
73
+ # | ------------ | -------- | ------------ | ------------------- |
74
+ # | PBKDF2 Salt | 192 Bits | 24 bytes | 32 characters |
75
+ # + ------------ + -------- + ------------ + ------------------- +
76
+ #
77
+ # The returned salt consists of 32 base64 characters which can be
78
+ # stored and fed into generate key. Before being used, the generate
79
+ # key method will convert the 32 base64 characters back into a
80
+ # <b>24 byte binary</b> formatted string.
58
81
  #
59
82
  # @return [String]
60
- # a base64 encoded representation of a twenty-four (24) randomly
61
- # and securely generated bytes.
62
- def self.generate_salt
63
- return Base64.urlsafe_encode64( SecureRandom.random_bytes( 24 ) )
83
+ # The returned salt consists of 32 base64 characters which can be
84
+ # stored and fed into the {KeyPbkdf2.generate_key}. These characters
85
+ # represent twenty-four (24) randomly and securely generated bytes.
86
+ def self.generate_pbkdf2_salt
87
+ return Key64.from_bits( Key.to_random_bits( PBKDF2_SALT_LENGTH_BYTES ) )
64
88
  end
65
89
 
66
90
 
67
-
68
91
  # Generate a 128 bit binary key from the PBKDF2 password derivation
69
92
  # function. The most important input to this function is the human
70
93
  # generated key. The best responsibly sourced key with at least 95%
@@ -81,6 +104,31 @@ module OpenKey
81
104
  # The {Key} returned by this method encapsulates the derived
82
105
  # key of the byte (bit) length specified.
83
106
  #
107
+ # <b>PBKDF2 Output Key Length Note</b>
108
+ #
109
+ # Documentation for this algorithm says this about the key length.
110
+ #
111
+ # Typically, the key length should be larger than or equal to the
112
+ # output length of the underlying digest function, otherwise an
113
+ # attacker could simply try to brute-force the key. According to
114
+ # PKCS#5, security is limited by the output length of the underlying
115
+ # digest function, i.e. security is not improved if a key length
116
+ # strictly larger than the digest output length is chosen.
117
+ #
118
+ # Therefore, when using PKCS5 for password storage, it suffices to
119
+ # store values equal to the digest output length, nothing is gained
120
+ # by storing larger values.
121
+ #
122
+ # <b>Upgrading the OpenSSL <em>pbkdf2_hmac</em> Behaviour</b>
123
+ #
124
+ # As soon as the new Ruby and OpenSSL libraries become commonplace this class should
125
+ # be upgraded to use the <b>new and improved {OpenSSL::KDF.pbkdf2_hmac} behaviour</b>
126
+ # rather than {OpenSSL::PKCS5.pbkdf2_hmac}.
127
+ #
128
+ # The difficulty is in detecting the operating system's C libraries that are directly
129
+ # accessed for OpenSSL functionality. If the distinction can be made accurately, those
130
+ # with newer libraries can reap the benefits immediately.
131
+ #
84
132
  # @param human_secret [String]
85
133
  # a robust human generated password with as much entropy as can
86
134
  # be mustered. Remember that 40 characters spread randomly over
@@ -89,9 +137,11 @@ module OpenKey
89
137
  # that has embedded a near 100% entropy rating.
90
138
  #
91
139
  # @param pbkdf2_salt [String]
92
- # the salt string that has either been recently generated via the
93
- # {generate_salt} method or read from a persistence store and
94
- # resubmitted here in order to regenerate the same key.
140
+ # this parameter salt must consist of 32 base64 characters which
141
+ # will be converted into a <b>24 byte binary</b> formatted string.
142
+ # The salt string presented here must have either been recently
143
+ # generated by {generate_pbkdf2salt} or read from a persistence
144
+ # store and resubmitted here in order to regenerate the same key.
95
145
  #
96
146
  # @return [Key]
97
147
  # a key holder containing the key which can then be accessed via
@@ -99,61 +149,20 @@ module OpenKey
99
149
  # encapsulates the derived key with the specified byte count.
100
150
  def self.generate_key human_secret, pbkdf2_salt
101
151
 
102
- pbkdf2_key = OpenSSL::PKCS5.pbkdf2_hmac(
103
- human_secret,
104
- Base64.urlsafe_decode64( pbkdf2_salt ),
105
- PBKDF2_ITERATION_COUNT,
106
- PBKDF2_OUTPUT_KEY_LENGTH,
107
- OpenSSL::Digest::SHA512.new
108
- )
152
+ KeyError.not_new pbkdf2_salt, "PBKDF2 Algorithm Salt"
153
+ binary_salt = Key.to_binary_from_bit_string( Key64.to_bits( pbkdf2_salt ) )
154
+ err_msg = "Expected salt of #{PBKDF2_SALT_LENGTH_BYTES} bytes not #{binary_salt.length}."
155
+ raise ArgumentError, err_msg unless binary_salt.length == PBKDF2_SALT_LENGTH_BYTES
109
156
 
110
- return Key.new ( Base64.urlsafe_encode64( pbkdf2_key ) )
111
-
112
-
113
- # ----> -----------------------------------------------------
114
- # ----> -----------------------------------------------------
115
- # ----> ruby --version
116
- # ----> ruby 2.3.1p112 (2016-04-26) [x86_64-linux-gnu]
117
- # ----> -----------------------------------------------------
118
- # ----> -----------------------------------------------------
119
-
120
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
121
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
122
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
123
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
124
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
125
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
126
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
127
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
128
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
129
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
130
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
131
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
132
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
133
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
134
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
135
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
136
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
137
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
138
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
139
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
140
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
141
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
142
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
143
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
144
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
145
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
146
- ########### If Condition => IF is Ruby 2.4.1 or greater Use this else use one above
147
-
148
- pbkdf2_key = OpenSSL::KDF.pbkdf2_hmac(
157
+ pbkdf2_key = OpenSSL::PKCS5.pbkdf2_hmac(
149
158
  human_secret,
150
- Base64.urlsafe_decode64( @crypt_salt ),
159
+ binary_salt,
151
160
  PBKDF2_ITERATION_COUNT,
152
- PBKDF2_OUTPUT_KEY_LENGTH,
153
- OpenSSL::Digest::SHA512.new
161
+ PBKDF2_EXPORT_KEY_LENGTH,
162
+ OpenSSL::Digest::SHA384.new
154
163
  )
155
164
 
156
- return Key.new ( Base64.urlsafe_encode64( pbkdf2_key ) )
165
+ return Key.from_binary( pbkdf2_key )
157
166
 
158
167
  end
159
168
 
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/ruby
2
+ # coding: utf-8
3
+
4
+ module OpenKey
5
+
6
+
7
+ # SCrypt is a <b>Key Derivation Function (KDF)</b> with a reliable OpenSSL
8
+ # implementation that converts <b>low entropy</b> password-like text to a
9
+ # high entropy key that is computationally infeasible to acquire through brute
10
+ # force.
11
+ #
12
+ # SCrypt is incredibly resistant to attacks using dedicated hardware with
13
+ # massive memory to boot.
14
+ #
15
+ class KdfSCrypt
16
+
17
+ # SCrypt salts are recommended to contain 16 and 32 bytes
18
+ # inclusive. Here we opt for 24 bytes which unrolls out to
19
+ # 192 bits which serializes into 32 base64 characters.
20
+ SCRYPT_SALT_BYTE_LENGTH = 24
21
+
22
+ # The iteration count is determined using the powers of
23
+ # two so if the iteration integer is 12 there will be two
24
+ # to the power of 12 ( 2^12 ) giving 4096 iterations.
25
+ # The minimum number is 4 (16 iterations) and the max is 31.
26
+ # @example
27
+ # Configuring 16 into this directive results in
28
+ # 2^16 = 65,536 iterations
29
+ #
30
+ # This is a safe default and will slow the derivation time
31
+ # to about a second on a powerful 2020 laptop.
32
+ SCRYPT_ITERATION_INTEGER = 16
33
+
34
+ # The scrypt algorithm produces a key that is 181 bits in
35
+ # length. The algorithm then converts the binary 181 bits
36
+ # into a (6-bit) Radix64 character.
37
+ #
38
+ # 181 / 6 = 30 remainder 1 (so 31 characters are needed).
39
+ SCRYPT_KEY_LENGTH = 31
40
+
41
+
42
+ # When the key is transported using a 64 character set where
43
+ # each character is represented by 6 bits - the Scrypt key
44
+ # expands to 186 bits rather than the original 181 bits.
45
+ #
46
+ # This expansion is because of the remainder.
47
+ #
48
+ # 181 bits divided by 6 is 30 characters plus 1 character
49
+ # for the extra bit.
50
+ #
51
+ # The 31 transported characters then appear as
52
+ # 31 times 6 which equals 186 bits.
53
+ SCRYPT_KEY_TRANSPORT_LENGTH = 186
54
+
55
+ # The scrypt algorithm salt string should be 22 characters
56
+ # and may include forward slashes and periods.
57
+ SCRYPT_SALT_LENGTH = 22
58
+
59
+ # Scrypt outputs a single line of text that holds the prefix
60
+ # then the Radix64 encoded salt and finally the Radix64
61
+ # encoded hash key.
62
+ #
63
+ # The prefix consists of <b>two sections</b> sandwiched within
64
+ # two dollar <b>$</b> signs at the extremeties and a third dollar
65
+ # separating them.
66
+ #
67
+ # The two sections are the
68
+ # - Scrypt algorithm <b>version number</b> (2a or 2b) and
69
+ # - a power of 2 integer defining the no. of interations
70
+ SCRYPT_OUTPUT_TEXT_PREFIX = "$2a$#{SCRYPT_ITERATION_INTEGER}$"
71
+
72
+
73
+ # Generate a secure random and unpredictable salt suitable for
74
+ # the SCrypt algorithm. SCrypt salts are recommended to contain
75
+ # 16 and 32 bytes inclusive. Here we opt for 24 bytes which
76
+ # unrolls to 192 bits which in turn is 32 base64 characters.
77
+ #
78
+ # The {OpenKey::KdfSCrypt::SCRYPT_SALT_BYTE_LENGTH} constant
79
+ # defines the <b>number of random bytes</b> required for a robust
80
+ # SCrypt salt.
81
+ #
82
+ # The salt can be persisted and then resubmitted in order to
83
+ # regenerate the same key in the future.
84
+ #
85
+ # @return [String]
86
+ # the salt in a bit string format which can be converted to
87
+ # in order to feed the derivation function or indeed converted
88
+ # to base64 in order to persist it.
89
+ def self.generate_scrypt_salt
90
+ return Key.to_random_bits( SCRYPT_SALT_BYTE_LENGTH )
91
+ end
92
+
93
+
94
+
95
+ # Key generators should first use the {generate_salt} method to create
96
+ # a Scrypt salt string and then submit it to this method together with
97
+ # a human generated password in order to derive a key.
98
+ #
99
+ # The salt can be persisted and then resubmitted again to this method
100
+ # in order to regenerate the same key at any time in the future.
101
+ #
102
+ # Generate a binary key from the scrypt password derivation function.
103
+ #
104
+ # This differs from a server side password to hash usage in that we
105
+ # are interested in the 186bit key that scrypt produces. This method
106
+ # returns this reproducible key for use during symmetric encryption and
107
+ # decryption.
108
+ #
109
+ # @param secret_text [String]
110
+ # a robust human generated password with as much entropy as can
111
+ # be mustered. Remember that 40 characters spread randomly over
112
+ # the key space of about 90 characters and not relating to any
113
+ # dictionary word or name is the way to generate a powerful key
114
+ # that has embedded a near 100% entropy rating.
115
+ #
116
+ # @param scrypt_salt [String]
117
+ # the salt string that has either been recently generated via the
118
+ # {generate_salt} method or read from a persistence store and
119
+ # resubmitted here (in the future) to regenerate the same key.
120
+ #
121
+ # @return [Key]
122
+ # a key holder containing the key which can then be accessed via
123
+ # many different formats.
124
+ def self.generate_key secret_text, scrypt_salt
125
+
126
+ binary_salt = Key.to_binary_from_bit_string( scrypt_salt )
127
+
128
+ require "openssl"
129
+
130
+ puts ""
131
+ puts $LOADED_FEATURES.grep(/openssl/)
132
+ puts ""
133
+
134
+ scrypt_key = OpenSSL::KDF.scrypt(secret_text, salt: binary_salt, N: 2**SCRYPT_ITERATION_INTEGER, r: 8, p: 1, length: 33)
135
+
136
+
137
+
138
+ =begin
139
+ hashed_secret = Scrypt::Engine.hash_secret( secret_text, to_scrypt_salt(scrypt_salt) )
140
+ encoded64_key = Scrypt::Password.new( hashed_secret ).to_s
141
+ key_begin_index = SCRYPT_OUTPUT_TEXT_PREFIX.length + SCRYPT_SALT_LENGTH
142
+ radix64_key_str = encoded64_key[ key_begin_index .. -1 ]
143
+ key_length_mesg = "The scrypt key length should have #{SCRYPT_KEY_LENGTH} characters."
144
+ raise RuntimeError, key_length_mesg unless radix64_key_str.length == SCRYPT_KEY_LENGTH
145
+
146
+ return Key.new(radix64_key_str)
147
+ =end
148
+ return scrypt_key
149
+ end
150
+
151
+
152
+
153
+ private
154
+
155
+
156
+ def self.scrypt_test_method
157
+
158
+ puts ""
159
+ puts "##############################################################################"
160
+
161
+ key_count = 20
162
+ for n in 0 .. key_count
163
+ scrypt_saltbits = OpenKey::KdfSCrypt.generate_scrypt_salt
164
+ scrypt_key = OpenKey::KdfSCrypt.generate_key( "abonekanoby", scrypt_saltbits )
165
+ scrypt_saltchar = OpenKey::Key64.from_bits( scrypt_saltbits )
166
+ puts "#{n} Salt => #{scrypt_saltchar} (#{scrypt_saltchar.length}) => Key => #{scrypt_key} (#{scrypt_key.length})"
167
+ end
168
+
169
+ puts "##############################################################################"
170
+ puts ""
171
+
172
+ end
173
+
174
+
175
+
176
+ def self.to_scrypt_salt the_salt
177
+ return SCRYPT_OUTPUT_TEXT_PREFIX + the_salt
178
+ end
179
+
180
+ def self.assert_scrypt_salt the_salt
181
+ raise RuntimeError, "scrypt salt not expected to be nil." if the_salt.nil?
182
+ salt_length_msg = "A scrypt salt is expected to contain #{SCRYPT_SALT_LENGTH} characters."
183
+ raise RuntimeError, salt_length_msg unless the_salt.length == SCRYPT_SALT_LENGTH
184
+ end
185
+
186
+
187
+ end
188
+
189
+
190
+ end
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/ruby
2
+
3
+ module OpenKey
4
+
5
+ # First use the class methods to source keys, then use a key's instance
6
+ # methods to access its properties and in concert with other symmetrical
7
+ # information, you can use the keys to lock (encrypt) or unlock (decrypt)
8
+ # other keys and objects.
9
+ #
10
+ # == Sourcing and Deriving Keys
11
+ #
12
+ # Keys can be
13
+ #
14
+ # - sourced from a secure random byte generating function
15
+ # - sourced from ciphertext and another (decryption) key
16
+ # - generated by passing a secret through key derivation functions
17
+ # - regenerated from a secret and previously stored salts
18
+ # - sourced from the current unique workstation shell environment
19
+ # - sourced from an environment variable containing ciphertext
20
+ #
21
+ #
22
+ # Keys need to be viewed (represented) in multiple ways and the essence
23
+ # of the key viewer is to input keys {as_bits}, {as_bytes} and {as_base64}
24
+ # and then output the same key (in as far as is possible) - as bits, as
25
+ # bytes and as base64.
26
+ #
27
+ # == Key | To and From Behaviour
28
+ #
29
+ # Use the <b>From</b> methods to create Keys from a variety of resources
30
+ # such as
31
+ #
32
+ # - a base64 encoded string
33
+ # - a binary byte string
34
+ # - a string of one and zero bits
35
+ # - a hexadecimal representation
36
+ #
37
+ # Once you have instantiated the key, you will then be able to convert it
38
+ # (within reason due to bit, byte and base64 lengths) to any of the above
39
+ # key representations.
40
+ #
41
+ # == Key | Bits Bytes and Base64
42
+ #
43
+ # The shoe doesn't always fit when its on the other foot and this is best
44
+ # illustratd with a table that maps bits to 8 bit bytes and 6 bit Base64
45
+ # characters.
46
+ #
47
+ # | --------- | -------- | ------------ | ------------------------------- |
48
+ # | Fit? | Bits | Bytes | (and) Base64 |
49
+ # | --------- | -------- | ------------ | ------------------------------- |
50
+ # | Perfect | 168 Bits | is 21 bytes | 28 Chars - bcrypt chops to this |
51
+ # | Perfect | 216 Bits | is 27 bytes | 36 Chars - |
52
+ # | Perfect | 264 Bits | is 33 bytes | 44 Chars - holder 4 256bit keys |
53
+ # | Perfect | 384 Bits | is 48 bytes | 64 Chars - 216 + 168 equals 384 |
54
+ # | --------- | -------- | ------------ | ------------------------------- |
55
+ # | Imperfect | 128 Bits | 16 precisely | 22 Chars - 21 + 2 remain bits |
56
+ # | Imperfect | 186 Bits | 23 remain 2 | 31 Characers precisely |
57
+ # | Imperfect | 256 Bits | 32 precisely | 43 Chars - 42 + 4 remain bits |
58
+ # | --------- | -------- | ------------ | ------------------------------- |
59
+ #
60
+ # Yes, the shoe doesn't always fit when it's on the other foot.
61
+ #
62
+ # == Schoolboy Error
63
+ #
64
+ # <b>The strategy is so simple, we call it a schoolboy error.</b>
65
+ #
66
+ # If we want to use a key with n bits and either n % 6 or n % 8 (or both)
67
+ # are not zero - <b>we instantiate a Key</b> with the lowest common
68
+ # denominator of 6 and 8 that exceeds n.
69
+ #
70
+ # So when we request a byte, or base64 representation the viewer will
71
+ # truncate (not round down) to the desired length.
72
+ #
73
+ #
74
+ # == YACHT 64 | Yet Another Character Table
75
+ #
76
+ # This binary key class is a dab hand at converting base64 strings
77
+ # into their 6-bit binary string equivalents.
78
+ #
79
+ # It can convert non-alphanumeric characters within either Base64 or
80
+ # Radix64 into the OpenKey YACHT64 standard which has a forward slash
81
+ # but neither a plus sign nor a period character.
82
+ #
83
+ # <b>The Big4 Character Sets | Base64 | UrlSafe64 | Radix64 | YACHT64</b>
84
+ #
85
+ # Base64 and Radix64 (from OpenBSD) differ in both the order of characters
86
+ # and their choice of the two non-alphanumeric characters. Base64 can also
87
+ # contain line breaks and equal signs for padding. UrlSafe base64 has different
88
+ # choices for the two non alphanum characters in keeping with URL standards.
89
+ #
90
+ # The character sets for each of the four 64 fomats are as follows.
91
+ #
92
+ # - Base-64 is <b>A to Z</b> then <b>a to z</b> then <b>0 to 9</b> then <b>+</b> then <b>/</b>
93
+ # - Radix64 is <b>.</b> then <b>/</b> then <b>0 to 9</b> then <b>A to Z</b> then <b>a to z</b>
94
+ # - UrlSafeBase64 is Base64 but chars 63/64 are an <b>underscore (_)</b> and <b>hyphen (-)</b>
95
+ # - UrlSafeBase64 <b>does not have line breaks and carriage returns</b> (unlike Base64)
96
+ # - <b>OpenKey 64 (YACHT64)</b> uses the same 62 characters plus an @ sign and a forward slash
97
+ # - The 64 <b>OpenKey 64</b> characters are <b>obfuscated into a random order</b>
98
+ #
99
+ # == 4 Non-AlphaNumerics | Base64 | Radix64 | YACHT64
100
+ #
101
+ # The behaviour here is happy to convert base64 strings produced by either
102
+ # Radix64 or Base64 or UrlSafe Base64. Howeverr it aware of the
103
+ # <b>non alpha-numeric characters</b> and converts them before processing
104
+ # with the modus operandi that says
105
+ #
106
+ # - ignore the forward slash in <b>YACHT64, Base64 and Radix64</b>
107
+ # - convert the <b>plus (+)</b> in Base64 to the <b>@ symbol</b> in YACHT64
108
+ # - convert the <b>period (.)</b> in <b>Radix64</b> to the @ symbol in YACHT64
109
+ # - convert <b>hyphen (-)</b> in <b>Url Safe Base64</b> into a fwd slash
110
+ # - convert <b>underscore (_)</b> in <b>Url Safe Base64</b> to an @ sign
111
+ # - <b>delete the (=) equals</b> padding character used by Base64
112
+ #
113
+ # Neither the OpenBSD backed Radix64 nor the OpenKey (YACHT64) entertain the
114
+ # concept of padding.
115
+ #
116
+ # == Mapping Each Character to 6 Binary Bits
117
+ #
118
+ # We need 6 binary bits to represent a base64 character (and 4
119
+ # bits for hexadecimal). Here is an example mapping between
120
+ # a base 64 character, an integer and the six bit binary.
121
+ #
122
+ # Character Integer Binary (6 Bit)
123
+ #
124
+ # a 0 000000
125
+ # b 1 000001
126
+ # c 2 000010
127
+ #
128
+ # y 25 011001
129
+ # z 26 011010
130
+ # A 27 011011
131
+ # B 28 011100
132
+ #
133
+ # 8 60 111100
134
+ # 9 61 111101
135
+ # / 62 111110
136
+ # + 63 111111
137
+ #
138
+ class Key64
139
+
140
+ # YACHT stands for <b>Yet Another Character Table</b> and it
141
+ # can map binary sequences onto 64 well chosen characters.
142
+ #
143
+ # The 64 character sets are all similar in that they hold 64
144
+ # characters and they define two non alphanumeric characters
145
+ # because the 26 lowercase, 26 uppercase and 10 digits only
146
+ # adds up to an <b><em>agonisingly close</em></b> 62 characters.
147
+ #
148
+ YACHT64_CHARACTER_SET = [
149
+ "a", "9", "W", "B", "f", "K", "O", "z",
150
+ "3", "s", "1", "5", "c", "n", "E", "J",
151
+ "L", "A", "l", "6", "I", "w", "o", "g",
152
+ "k", "N", "t", "Y", "S", "%", "T", "b",
153
+ "V", "R", "H", "0", "@", "Z", "8", "F",
154
+ "G", "j", "u", "m", "M", "h", "4", "p",
155
+ "q", "d", "7", "v", "e", "2", "U", "X",
156
+ "r", "C", "y", "Q", "D", "x", "P", "i"
157
+ ]
158
+
159
+
160
+ # Radix64 strings can contain period characters in their midst.
161
+ PERIOD = "."
162
+
163
+ # Radix64 strings can contain forward slashes in their midst.
164
+ FORWARD_SLASH = "/"
165
+
166
+ # YACHT64 strings can contain at symbols in their midst.
167
+ AT_SYMBOL = "@"
168
+
169
+ # YACHT64 strings can contain percent signs in their midst.
170
+ PERCENT_SIGN = "%"
171
+
172
+
173
+ # Convert the parameter string of ones and zeroes into an
174
+ # internal base64 character set known as YACHT for yet another
175
+ # character table.
176
+ #
177
+ # @param bit_string [String]
178
+ # a string of ones and zeroes that can be sliced into
179
+ # six character chunks with each chunk then being mapped
180
+ # to a YACHT64 character.
181
+ #
182
+ # @return [String]
183
+ # printable characters from a set of 62 alpha-numerics
184
+ # plus an @ symbol and a percent % sign.
185
+ #
186
+ # @raise ArgumentError
187
+ # If the bit string is nil.
188
+ # Or if the bit string length is not a multiple of six.
189
+ # Or if it contains any character that is not a 1 or 0.
190
+ def self.from_bits bit_string
191
+
192
+ nil_err_msg = "The parameter bit string cannot be nil."
193
+ raise ArgumentError, nil_err_msg if bit_string.nil?
194
+
195
+ bit_size_msg = "The bit string length is not a multiple of #{SIX}."
196
+ raise ArgumentError, bit_size_msg unless bit_string.length % SIX == 0
197
+
198
+ num_unknowns = bit_string.delete("10").length
199
+ unknowns_msg = "The bit string has #{num_unknowns} characters that are not 1 or 0."
200
+ raise ArgumentError, unknowns_msg if num_unknowns > 0
201
+
202
+ characters64 = ""
203
+ char_count = bit_string.length / SIX
204
+ for n in 0 .. (char_count-1)
205
+ six_bit_chunk = bit_string[ (n*SIX), SIX ]
206
+ six_bit_index = six_bit_chunk.to_i(2)
207
+ characters64 += Key64.character(six_bit_index)
208
+ end
209
+
210
+ code_size_msg = "Length is #{characters64.length} but #{char_count} is expected."
211
+ raise RuntimeError, code_size_msg unless characters64.length == char_count
212
+
213
+ return characters64
214
+
215
+ end
216
+
217
+
218
+ # Convert the parameter characters based on an internal base64
219
+ # character set (known as YACHT) into a <b>bit string</b> of ones
220
+ # and zeroes.
221
+ #
222
+ # @param char64_string [String]
223
+ # The base64 character sequence which which will be used to
224
+ # derive the returned bit string. Naturally this character
225
+ # sequencee cannot be nil, nor can it contain any characters
226
+ # that are not present in {Key64::YACHT64_CHARACTER_SET}.
227
+ #
228
+ # @return [String]
229
+ # a string of ones and zeroes that have been strung out
230
+ # from each YACHT64 character. The returned string length of
231
+ # ones and zeroes will be exactly 6 times the length of the
232
+ # input parameter.
233
+ #
234
+ # @raise [ArgumentError]
235
+ # If a nil or zero length character string is received.
236
+ # Or if the character sequence contains a character not present
237
+ # in the {Key64::YACHT64_CHARACTER_SET}.
238
+ #
239
+ # @raise [RuntimeError]
240
+ # if the conversion does not result in 6 bits for every character
241
+ # in the parameter string.
242
+ def self.to_bits char64_string
243
+
244
+ bit_string = ""
245
+ char64_string.each_char do |the_char|
246
+
247
+ yacht64_index = YACHT64_CHARACTER_SET.index(the_char)
248
+ assert_yacht64_index( the_char, yacht64_index )
249
+ bit_string += "%06d" % [ yacht64_index.to_s(2) ]
250
+
251
+ end
252
+
253
+ assert_bit_lengths char64_string, bit_string
254
+ return bit_string
255
+
256
+ end
257
+
258
+
259
+ # Convert a string of Radix64 characters into a bit representation which
260
+ # will be 6 times longer than the input parameter. This method first
261
+ # converts the string into the internal YACHT64 format and then converts
262
+ # that to a bit string using the {Key64.to_bits} method.
263
+ #
264
+ # @param radix64_string [String]
265
+ # the radix64 string to convert into bits. This string will be a subset
266
+ # of the usual 62 character suspects together with period and forward
267
+ # slash characters.
268
+ #
269
+ # This parameter should not contain newlines nor carriage returns.
270
+ #
271
+ # @return [String]
272
+ # a string of ones and zeroes that represent the bits converted from the
273
+ # radix64 input. The return value will be exactly 6 times the number of
274
+ # input characters.
275
+ def self.from_radix64_to_bits radix64_string
276
+
277
+ yacht64_chars = radix64_string.gsub( PERIOD, AT_SYMBOL ).gsub( FORWARD_SLASH, PERCENT_SIGN )
278
+ out_bitstring = to_bits( yacht64_chars )
279
+ assert_bit_lengths( radix64_string, out_bitstring )
280
+ return out_bitstring
281
+
282
+ end
283
+
284
+
285
+
286
+ private
287
+
288
+
289
+
290
+ SIX = 6
291
+
292
+ def self.character char_index
293
+
294
+ index_oob_msg = "The character index must be between 0 and 63 inclusive."
295
+ index_is_oob = char_index < 0 || char_index > 63
296
+ raise ArgumentError, index_oob_msg if index_is_oob
297
+ return YACHT64_CHARACTER_SET[ char_index ]
298
+
299
+ end
300
+
301
+ def self.assert_bit_lengths( in_string, out_string )
302
+
303
+ in_length = in_string.length
304
+ out_length = out_string.length
305
+ good_ratio = out_length == in_length * SIX
306
+ size_msg = "Bit string length [#{out_length}] not 6 times more than [#{in_length}]."
307
+ raise RuntimeError, size_msg unless good_ratio
308
+
309
+ end
310
+
311
+ def self.assert_yacht64_index the_char, yacht64_index
312
+
313
+ nil_msg = "Character [ #{the_char} ] not in YACHT character set."
314
+ raise ArgumentError, nil_msg if yacht64_index.nil?
315
+
316
+ index_msg = "Index of character [ #{the_char} ] not within expected bounds."
317
+ all_good = ( yacht64_index >= 0 ) && ( yacht64_index <= 63 )
318
+ raise ArgumentError, index_msg unless all_good
319
+
320
+ end
321
+
322
+
323
+ end
324
+
325
+
326
+ end