opensecret 0.0.988 → 0.0.9925

Sign up to get free protection for your applications and to get access to all the features.
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