opensecret 0.0.962 → 0.0.988
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +16 -10
- data/bin/opensecret +3 -4
- data/bin/ops +5 -0
- data/lib/extension/string.rb +114 -0
- data/lib/factbase/facts.opensecret.io.ini +9 -21
- data/lib/interprete/begin.rb +232 -0
- data/lib/interprete/cmd.rb +621 -0
- data/lib/{plugins/usecases/unlock.rb → interprete/export.rb} +25 -70
- data/lib/interprete/init.rb +205 -0
- data/lib/interprete/key.rb +119 -0
- data/lib/interprete/open.rb +148 -0
- data/lib/{plugins/usecases → interprete}/put.rb +19 -6
- data/lib/{plugins/usecases → interprete}/safe.rb +2 -1
- data/lib/{plugins/usecases/lock.rb → interprete/seal.rb} +24 -34
- data/lib/interprete/set.rb +46 -0
- data/lib/interprete/use.rb +43 -0
- data/lib/interpreter.rb +165 -0
- data/lib/keytools/binary.map.rb +245 -0
- data/lib/keytools/digester.rb +245 -0
- data/lib/keytools/doc.conversion.to.ones.and.zeroes.ruby +179 -0
- data/lib/keytools/doc.rsa.radix.binary-mapping.ruby +190 -0
- data/lib/keytools/doc.star.schema.strategy.txt +77 -0
- data/lib/keytools/doc.using.pbkdf2.kdf.ruby +95 -0
- data/lib/keytools/doc.using.pbkdf2.pkcs.ruby +266 -0
- data/lib/keytools/kdf.bcrypt.rb +180 -0
- data/lib/keytools/kdf.pbkdf2.rb +164 -0
- data/lib/keytools/key.data.rb +227 -0
- data/lib/keytools/key.derivation.rb +341 -0
- data/lib/keytools/key.module.rb +140 -0
- data/lib/keytools/key.rb +481 -0
- data/lib/logging/gem.logging.rb +1 -2
- data/lib/modules/cryptology.md +43 -0
- data/lib/{plugins/ciphers → modules/cryptology}/aes-256.rb +6 -0
- data/lib/{crypto → modules/cryptology}/amalgam.rb +6 -0
- data/lib/modules/cryptology/blowfish.rb +130 -0
- data/lib/modules/cryptology/cipher.rb +207 -0
- data/lib/modules/cryptology/collect.rb +118 -0
- data/lib/{plugins → modules/cryptology}/crypt.io.rb +5 -0
- data/lib/{crypto → modules/cryptology}/engineer.rb +7 -1
- data/lib/{crypto → modules/cryptology}/open.bcrypt.rb +0 -0
- data/lib/modules/mappers/collateral.rb +282 -0
- data/lib/modules/mappers/dictionary.rb +288 -0
- data/lib/modules/mappers/envelope.rb +127 -0
- data/lib/modules/mappers/settings.rb +170 -0
- data/lib/modules/storage/coldstore.rb +186 -0
- data/lib/{opensecret/plugins.io/git/git.flow.rb → modules/storage/git.store.rb} +11 -0
- data/lib/notepad/scratch.pad.rb +17 -0
- data/lib/session/fact.finder.rb +13 -0
- data/lib/session/require.gem.rb +5 -0
- data/lib/store-commands.txt +180 -0
- data/lib/version.rb +1 -1
- data/opensecret.gemspec +5 -6
- metadata +74 -29
- data/lib/crypto/blowfish.rb +0 -85
- data/lib/crypto/collect.rb +0 -140
- data/lib/crypto/verify.rb +0 -33
- data/lib/opensecret.rb +0 -236
- data/lib/plugins/cipher.rb +0 -203
- data/lib/plugins/ciphers/blowfish.rb +0 -126
- data/lib/plugins/coldstore.rb +0 -181
- data/lib/plugins/envelope.rb +0 -116
- data/lib/plugins/secrets.uc.rb +0 -94
- data/lib/plugins/usecase.rb +0 -239
- data/lib/plugins/usecases/init.rb +0 -145
- data/lib/plugins/usecases/open.rb +0 -108
- data/lib/session/attributes.rb +0 -279
- data/lib/session/dictionary.rb +0 -191
- data/lib/session/file.path.rb +0 -53
- data/lib/session/session.rb +0 -80
@@ -0,0 +1,227 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
module OpenKey
|
5
|
+
|
6
|
+
require 'inifile'
|
7
|
+
|
8
|
+
# This is a key-value store backed by unencrypted (plain-text) permanent
|
9
|
+
# file-system storage in INI format.
|
10
|
+
#
|
11
|
+
# == Key-Value Pair Groupings
|
12
|
+
#
|
13
|
+
# The key-value pairs can be collated into a
|
14
|
+
#
|
15
|
+
# - custom group with a name specified to methods {read} and {write}
|
16
|
+
# - default group that is accessible via the methods {get} and {put}
|
17
|
+
#
|
18
|
+
# The name given to the default group can be specified to the constructor.
|
19
|
+
# If none is provided the aptly named "default" is used.
|
20
|
+
|
21
|
+
|
22
|
+
# An OpenSession dictionary is a <b>2D (two dimensional) hash</b> data
|
23
|
+
# structure backed by an encrypted file.
|
24
|
+
#
|
25
|
+
# It supports operations to <b>read from</b> and <b>write to</b> a known
|
26
|
+
# filepath and given the correct symmetric encryption key it will
|
27
|
+
#
|
28
|
+
# - decrypt <b>after reading from</b> the file and
|
29
|
+
# - encrypt <b>before writing to</b> the file
|
30
|
+
#
|
31
|
+
# This dictionary extends {Hash} in order to deliver on its core key value
|
32
|
+
# storage and retrieve use cases. Extend this dictionary and provide
|
33
|
+
# context specific methods through constants to read and write context
|
34
|
+
# specific data.
|
35
|
+
#
|
36
|
+
# == The <em>Current</em> Dictionary Section
|
37
|
+
#
|
38
|
+
# This KeyData is <b>two-dimensional</b> so all key-value pairs are stored
|
39
|
+
# under the auspices of a section.
|
40
|
+
#
|
41
|
+
# The KeyData can track the <b>current section</b> for you and all data
|
42
|
+
# exchanges can occur in lieu of a single section if you so wish by using
|
43
|
+
# the provided {put} and {get} methods.
|
44
|
+
#
|
45
|
+
# To employ section management functionality you should pass in a current
|
46
|
+
# <b>section id</b> when creating the dictionary.
|
47
|
+
#
|
48
|
+
# @example
|
49
|
+
# To use the dictionary in the raw (unextended) format you create
|
50
|
+
# write and read it like this.
|
51
|
+
#
|
52
|
+
# ----------------------------------------------------------------------
|
53
|
+
#
|
54
|
+
# my_dictionary = KeyData.create( "/path/to/backing/file" )
|
55
|
+
#
|
56
|
+
# my_dictionary["user23"] = {}
|
57
|
+
# my_dictionary["user23"]["Name"] = "Joe Bloggs"
|
58
|
+
# my_dictionary["user23"]["Email"] = "joebloggs@example.com"
|
59
|
+
# my_dictionary["user23"]["Phone"] = "+44 07342 800080"
|
60
|
+
#
|
61
|
+
# my_dictionary.write( "crypt-key-1234-wxyz" )
|
62
|
+
#
|
63
|
+
# ----------------------------------------------------------------------
|
64
|
+
#
|
65
|
+
# my_dictionary = KeyData.create( "/path/to/backing/file", "crypt-key-1234-wxyz" )
|
66
|
+
# puts my_dictionary.has_key? "user23" # => true
|
67
|
+
# puts my_dictionary["user23"].length # => 3
|
68
|
+
# puts my_dictionary["user23"]["Email"] # => "joebloggs@example.com"
|
69
|
+
#
|
70
|
+
# ----------------------------------------------------------------------
|
71
|
+
class KeyData
|
72
|
+
|
73
|
+
|
74
|
+
# Initialize the key value store and auto write a time stamp that
|
75
|
+
# has nano-second accuracy with a key whose name is gleened from
|
76
|
+
# the constant {KeyData::INIT_TIME_STAMP_NAME}.
|
77
|
+
#
|
78
|
+
# The path to the backing INI file is gleened from the first
|
79
|
+
# backing file path parameter.
|
80
|
+
#
|
81
|
+
# @param backing_file_path [String]
|
82
|
+
# the expected location of the file-backed key-value store.
|
83
|
+
# If the folder and/or file do not exist the folder is created
|
84
|
+
# and then the file is created along with the time stamps.
|
85
|
+
#
|
86
|
+
# @param the_default_group [String]
|
87
|
+
# the name of the default group. If none is presented this value
|
88
|
+
# will default to the aptly named "default".
|
89
|
+
def initialize backing_file_path, the_reference
|
90
|
+
|
91
|
+
@file_path = backing_file_path
|
92
|
+
@reference = the_reference
|
93
|
+
|
94
|
+
create_dir_if_necessary
|
95
|
+
put_stamps_if_necessary
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
# Stash the setting directive and its value into the configuration file
|
101
|
+
# using the default settings group.
|
102
|
+
#
|
103
|
+
# The default settings group is resolved via {Collateral::CONTEXT_NAME}
|
104
|
+
#
|
105
|
+
# @param key_name [String] the name of the key whose value is to be written
|
106
|
+
# @param key_value [String] the data item value of the key specified
|
107
|
+
def put key_name, key_value
|
108
|
+
write @reference, key_name, key_value
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
# Stash the setting directive and its value into the configuration file
|
113
|
+
# using the default settings group.
|
114
|
+
#
|
115
|
+
# The default settings group is resolved via {Collateral::CONTEXT_NAME}
|
116
|
+
#
|
117
|
+
# @param key_name [String] the name of the key whose value is to be written
|
118
|
+
# @return [String]
|
119
|
+
# return the value of the configuration directive in the default group
|
120
|
+
def get key_name
|
121
|
+
read @reference, key_name
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
# Write the key/value pair in the parameter into this key/value store's
|
126
|
+
# base file-system backing INI file.
|
127
|
+
#
|
128
|
+
# This method assumes the existence of the backing configuration file at
|
129
|
+
# the @file_path instance variable that was set during initialization.
|
130
|
+
#
|
131
|
+
# Observable value is the written key/value pair within the specified
|
132
|
+
# section. The alternate flows are
|
133
|
+
#
|
134
|
+
# - if the section does not exist it is created
|
135
|
+
# - if the section and key exist the value is inserted or overwritten
|
136
|
+
#
|
137
|
+
# @param section_name [String] name grouping the section of config values
|
138
|
+
# @param key [String] the key name of config directive to be written into the file
|
139
|
+
# @param value [String] value of the config directive to be written into the file
|
140
|
+
#
|
141
|
+
def write section_name, key, value
|
142
|
+
|
143
|
+
config_map = IniFile.new( :filename => @file_path, :encoding => 'UTF-8' )
|
144
|
+
config_map = IniFile.load( @file_path ) if File.file? @file_path
|
145
|
+
config_map[section_name][key] = value
|
146
|
+
config_map.write
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
# Given the configuration key name and the context name, get the
|
152
|
+
# corresponding key value from the configuration file whose path
|
153
|
+
# is acquired using the {self#get_filepath} method.
|
154
|
+
#
|
155
|
+
# @param key_name [String] the key whose value is to be retrieved
|
156
|
+
#
|
157
|
+
# @return [String] the value configured for the parameter key
|
158
|
+
#
|
159
|
+
# @raise ArgumentError for any one of a long list of reasons that
|
160
|
+
# cause the key value to not be retrieved. This can range from
|
161
|
+
# non-existent directories and files, non readable files, incorrect
|
162
|
+
# configurations right down to missing keys or even missing values.
|
163
|
+
def read section_name, key_name
|
164
|
+
|
165
|
+
raise ArgumentError.new "No configuration file found => [ #{@file_path} ]" unless File.exists? @file_path
|
166
|
+
|
167
|
+
the_text = File.read @file_path
|
168
|
+
raise ArgumentError.new "Configuration file is empty => [ #{@file_path} ]" if the_text.empty?
|
169
|
+
|
170
|
+
the_data = IniFile.load @file_path
|
171
|
+
key_exists = the_data[ section_name ].has_key?( key_name )
|
172
|
+
raise ArgumentError.new "Key [#{key_name}] not found in section [#{section_name}] => #{the_data.to_s}" unless key_exists
|
173
|
+
|
174
|
+
rawvalue = the_data[section_name][key_name]
|
175
|
+
raise ArgumentError.new "Empty value 4 key [#{section_name}][#{key_name}] => #{the_data.to_s}" if rawvalue.empty?
|
176
|
+
|
177
|
+
keyvalue = rawvalue.chomp.strip
|
178
|
+
raise ArgumentError.new "Whitespace value 4 key [#{section_name}][#{key_name}] => #{the_data.to_s}" if keyvalue.empty?
|
179
|
+
|
180
|
+
return keyvalue
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
|
185
|
+
# Get the time stamp that was written to the key-value store at
|
186
|
+
# the point it was first initialized and then subsequently written
|
187
|
+
# out (serialized) onto the file-system.
|
188
|
+
#
|
189
|
+
# The time stamp returned marks the first time this key-value store
|
190
|
+
# was conceived by a use case actor and subsequently serialized.
|
191
|
+
#
|
192
|
+
# @return [String]
|
193
|
+
# the string time stamp denoting the first time this key-value
|
194
|
+
# store was first initialized and then subsequently written out
|
195
|
+
# (serialized) onto the file-system.
|
196
|
+
def time_stamp
|
197
|
+
return get INIT_TIME_STAMP_NAME
|
198
|
+
end
|
199
|
+
|
200
|
+
|
201
|
+
|
202
|
+
private
|
203
|
+
|
204
|
+
|
205
|
+
|
206
|
+
def create_dir_if_necessary
|
207
|
+
|
208
|
+
config_directory = File.dirname @file_path
|
209
|
+
return if (File.exist? config_directory) && (File.directory? config_directory)
|
210
|
+
FileUtils.mkdir_p config_directory
|
211
|
+
|
212
|
+
end
|
213
|
+
|
214
|
+
|
215
|
+
def put_stamps_if_necessary
|
216
|
+
|
217
|
+
return if File.file? @file_path
|
218
|
+
|
219
|
+
put INIT_TIME_STAMP_NAME, OpenSession::Stamp.yyjjj_hhmm_ss_nanosec
|
220
|
+
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
end
|
225
|
+
|
226
|
+
|
227
|
+
end
|
@@ -0,0 +1,341 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
module OpenKey
|
5
|
+
|
6
|
+
|
7
|
+
# The OpenKey underlying security strategy is to lock a master index file
|
8
|
+
# with a <b>symmetric encryption key</b> that is based on two randomly generated
|
9
|
+
# and amalgamated <b>55 and 45 character keys</b> and then to lock that key
|
10
|
+
# <b>(and only that key)</b> with a 256 bit symmetric encryption key derived from
|
11
|
+
# a human password and generated by at least two cryptographic workhorses known
|
12
|
+
# as <b>key derivation functions</b>.
|
13
|
+
#
|
14
|
+
# Random powerful keys are derived are seeded with 55 random bytes and
|
15
|
+
# then fed through the master key generator and its two key derivation
|
16
|
+
# functions (BCrypt and PBKDF2).
|
17
|
+
#
|
18
|
+
# == What Does the Master Encryption Key Generator Do?
|
19
|
+
#
|
20
|
+
# This class sits at the core of implementing that strategy and works to produce
|
21
|
+
# 256 bit encryption key derived from a human password which is then minced by
|
22
|
+
# two best of breed key derivation functions (BCrypt and PBKDF2).
|
23
|
+
#
|
24
|
+
# BCrypt (Blowfish) and PBKDF2 are the leading <b>key derivation functions</b>
|
25
|
+
# whose modus operandi is to convert <b>low entropy</b> human generated passwords
|
26
|
+
# into a high entropy key that is computationally infeasible to acquire via brute
|
27
|
+
# force.
|
28
|
+
#
|
29
|
+
# == How to Create the Encryption Key
|
30
|
+
#
|
31
|
+
# To create a high entropy encryption key this method takes the first
|
32
|
+
# 168 bits from the 186 bit BCrypt key produced by {BCryptKeyGen} and
|
33
|
+
# the first 96 bits from the 132 bit PBKDF2 key produced inside the
|
34
|
+
# {Pbkdf2KeyGen} class and amalgamates them to produce a 264 bit key.
|
35
|
+
#
|
36
|
+
# The 264 bit key is then digested to produce a 256bit encryption key.
|
37
|
+
class KeyDerivation
|
38
|
+
|
39
|
+
|
40
|
+
# BCrypt (Blowfish) and PBKDF2 are the leading <b>key derivation functions</b>
|
41
|
+
# whose modus operandi is to convert <b>low entropy</b> human generated passwords
|
42
|
+
# into a high entropy key that is computationally infeasible to acquire via brute
|
43
|
+
# force.
|
44
|
+
BCRYPT_SALT_KEY_NAME = "bcrypt.salt"
|
45
|
+
|
46
|
+
|
47
|
+
# BCrypt (Blowfish) and PBKDF2 are the leading <b>key derivation functions</b>
|
48
|
+
# whose modus operandi is to convert <b>low entropy</b> human generated passwords
|
49
|
+
# into a high entropy key that is computationally infeasible to acquire via brute
|
50
|
+
# force.
|
51
|
+
PBKDF2_SALT_KEY_NAME = "pbkdf2.salt"
|
52
|
+
|
53
|
+
|
54
|
+
# To create a high entropy encryption key the first 168 bits from the
|
55
|
+
# 186 bit BCrypt key produced by {BCryptKeyGen} is sliced off and used
|
56
|
+
# as the lead part of the generated key.
|
57
|
+
BCRYPT_KEY_CONTRIBUTION_SIZE = 168
|
58
|
+
|
59
|
+
|
60
|
+
# The first 96 bits from the 132 bit PBKDF2 key produced inside the
|
61
|
+
# {Pbkdf2KeyGen} class is amalgamated to the BCrypt 168 bit key to produce
|
62
|
+
# a 264 bit key.
|
63
|
+
PBKDF2_KEY_CONTRIBUTION_SIZE = 96
|
64
|
+
|
65
|
+
|
66
|
+
AMALGAM_KEY_RAW_BIT_SIZE = BCRYPT_KEY_CONTRIBUTION_SIZE + PBKDF2_KEY_CONTRIBUTION_SIZE
|
67
|
+
|
68
|
+
AMALGAM_KEY_SIX_BIT_COUNT = AMALGAM_KEY_RAW_BIT_SIZE / 6
|
69
|
+
|
70
|
+
AMALGAM_KEY_EIGHT_BYTE_COUNT = AMALGAM_KEY_RAW_BIT_SIZE / 8
|
71
|
+
|
72
|
+
|
73
|
+
# To acquire a <b>machine generated symmetric encryption key</b> pass
|
74
|
+
# in a {Key} initialized with {Key.from_random_bytes} and this method
|
75
|
+
# will digest it for extra security and produce a gold standard 256 bit
|
76
|
+
# encryption key ready to use with the AES256 algorithm.
|
77
|
+
#
|
78
|
+
# Do not use the {Key.from_random_bytes} as an encryption key, instead
|
79
|
+
# <b>encrypt and then persist</b> the key if you will need to decrypt the
|
80
|
+
# cipher text at some future date.
|
81
|
+
#
|
82
|
+
# <b>The 48 Bytes map to 64 Base64 Characters</b>
|
83
|
+
#
|
84
|
+
# To re-acquire the key for decryption, <b>read and unencrypt</b> the
|
85
|
+
# <b>64 base64 characters</b> with <b>Key.from_base64</b> and then pass
|
86
|
+
# it again to this method to <b>re-acquire</b> the original symmetric
|
87
|
+
# encryption/decryption key.
|
88
|
+
#
|
89
|
+
# | -------- | ------------ | -------------------------------- |
|
90
|
+
# | Bits | Bytes | Base64 |
|
91
|
+
# | -------- | ------------ | -------------------------------- |
|
92
|
+
# | 384 Bits | is 48 bytes | and 64 characters |
|
93
|
+
# | 256 Bits | 32 precisely | 43 Chars (42 + 4 remainder bits) |
|
94
|
+
# | -------- | ------------ | -------------------------------- |
|
95
|
+
#
|
96
|
+
# For <b>simplicity's sake</b>, try to employ a <b>bit length</b> with
|
97
|
+
# a bit count that is a <b>multiple of both 6 and 8</b>. This method mashes
|
98
|
+
# up the raw key and provides you with a powerful 256 bit key.
|
99
|
+
#
|
100
|
+
# @param the_key [OpenKey::Key]
|
101
|
+
# use <b>Key.from_random_bytes</b> to create a seed whose bit length
|
102
|
+
# is a <b>multiple of <em>both 6 and 8</em></b>. This method will mash
|
103
|
+
# up the raw key, thus provisioning a powerful 256 bit encryption key.
|
104
|
+
#
|
105
|
+
# @return [OpenKey::Key]
|
106
|
+
# the raw key will be mashed up and this method will faithfully return
|
107
|
+
# a powerful 256 bit encryption key.
|
108
|
+
def self.from_key the_key
|
109
|
+
return Digest::SHA256.digest( Digest::SHA384.digest( the_key.to_binary_bytes ) )
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
|
114
|
+
# This method generates a 256 bit symmetric encryption key derived from
|
115
|
+
# a human password and passed through two cryptographic workhorses
|
116
|
+
# (BCrypt and PBKDF2), the best of breed <b>key derivation functions</b>.
|
117
|
+
#
|
118
|
+
# == BCrypt and the PBKDF2 Cryptographic Algorithms
|
119
|
+
#
|
120
|
+
# BCrypt (Blowfish) and PBKDF2 are the leading <b>key derivation functions</b>
|
121
|
+
# that exists to convert <b>low entropy</b> human generated passwords into a high
|
122
|
+
# entropy key that is computationally infeasible to acquire through brute force.
|
123
|
+
#
|
124
|
+
# == Creating a High Entropy Encryption Key
|
125
|
+
#
|
126
|
+
# To create a high entropy encryption key this method takes the first
|
127
|
+
# 168 bits from the 186 bit BCrypt key produced by {BCryptKeyGen} and
|
128
|
+
# the first 96 bits from the 132 bit PBKDF2 key produced inside the
|
129
|
+
# {Pbkdf2KeyGen} class and amalgamates them to produce a 264 bit key.
|
130
|
+
#
|
131
|
+
# Note that all four of the above numbers are divisable by six (6), for
|
132
|
+
# representation with a 64 character set, and eight (8), for transport
|
133
|
+
# via the byte (8 bit) protocols.
|
134
|
+
#
|
135
|
+
# <b>Size of BCrypt and PBKDF2 Derived Keys</b>
|
136
|
+
#
|
137
|
+
# ----------- | --------- | ----------------- | ----------- |
|
138
|
+
# ----------- | --------- | ----------------- | ----------- |
|
139
|
+
# | Algorithm | Bit Count | Base64 Chars | 8 Bit Bytes |
|
140
|
+
# ----------- | --------- | ----------------- | ----------- |
|
141
|
+
# | BCrypt | 168 Bits | 28 characters | 21 bytes |
|
142
|
+
# | Pbkdf2 | 96 Bits | 16 characters | 12 bytes |
|
143
|
+
# ----------- | --------- | ----------------- | ----------- |
|
144
|
+
# | Total | 264 Bits | 44 characters | 33 bytes |
|
145
|
+
# ----------- | --------- | ----------------- | ----------- |
|
146
|
+
#
|
147
|
+
# <b>256 Bit Encryption Key | Remove 8 Bits</b>
|
148
|
+
#
|
149
|
+
# The manufactured encryption key, an amalgam of the above now has
|
150
|
+
# 264 bits carried by 44 Base64 characters.
|
151
|
+
#
|
152
|
+
# Just before it is used to encrypt vital keys, eight (8) bits are
|
153
|
+
# removed from the end of the key. The key is then converted into a
|
154
|
+
# powerful 32 byte (256 bit) encryption agent and is hashed by the
|
155
|
+
# SHA256 digest and delivered.
|
156
|
+
#
|
157
|
+
# @param human_secret [String]
|
158
|
+
# a robust human generated password with as much entropy as can
|
159
|
+
# be mustered. Remember that 40 characters spread randomly over
|
160
|
+
# the key space of about 90 characters and not relating to any
|
161
|
+
# dictionary word or name is the way to generate a powerful key
|
162
|
+
# that has embedded a near 100% entropy rating.
|
163
|
+
#
|
164
|
+
# @param dictionary [Hash]
|
165
|
+
# an instantiated hash object in which we will write the salts to
|
166
|
+
# be persisted and regurgitated during the regenerate process.
|
167
|
+
#
|
168
|
+
# @return [Key]
|
169
|
+
# the 256 bit symmetric encryption key derived from a human password
|
170
|
+
# and passed through two cryptographic workhorses.
|
171
|
+
def self.from_password human_secret, dictionary
|
172
|
+
|
173
|
+
bcrypt_salt = BCryptKeyGen.generate_salt
|
174
|
+
pbkdf2_salt = Pbkdf2KeyGen.generate_salt
|
175
|
+
|
176
|
+
dictionary.put( BCRYPT_SALT_KEY_NAME, bcrypt_salt )
|
177
|
+
dictionary.put( PBKDF2_SALT_KEY_NAME, pbkdf2_salt )
|
178
|
+
|
179
|
+
return generate_from_secret_and_salts human_secret, bcrypt_salt, pbkdf2_salt
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
# Regenerate the viciously unretrievable nor reversable key that was
|
185
|
+
# generated in the past and with the same salts that were used during
|
186
|
+
# the original key derivation process.
|
187
|
+
#
|
188
|
+
# @param dictionary [Hash]
|
189
|
+
# an instantiated and populated hash object containing the salts
|
190
|
+
# which were created in the past during the generation. These are
|
191
|
+
# now vital for a successful regeneration.
|
192
|
+
#
|
193
|
+
# @return [Key]
|
194
|
+
# the 256 bit symmetric encryption key that was previously generated
|
195
|
+
# from the secret and the cryptographic salts within the dictionary.
|
196
|
+
def self.regenerate human_secret, dictionary
|
197
|
+
|
198
|
+
bcrypt_salt = dictionary.get( BCRYPT_SALT_KEY_NAME )
|
199
|
+
pbkdf2_salt = dictionary.get( PBKDF2_SALT_KEY_NAME )
|
200
|
+
return generate_from_secret_and_salts human_secret, bcrypt_salt, pbkdf2_salt
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
|
205
|
+
# Derive a <b>short term (session scoped) encryption key</b> from the
|
206
|
+
# surrounding shell execution environment whilst giving two (2) important
|
207
|
+
# guarantees.
|
208
|
+
#
|
209
|
+
# The two guarantees governing the returned key are that it is
|
210
|
+
#
|
211
|
+
# - <b>the same</b> whenever called within this executing shell
|
212
|
+
# - <b>different</b> different when another shell is employed
|
213
|
+
#
|
214
|
+
# The much higher collision rate is tolerable because the key's lifetime
|
215
|
+
# is only <b>as long as commands are being typed into a given shell</b>
|
216
|
+
# or command prompt in the case of Windows.
|
217
|
+
#
|
218
|
+
# This method uses a one-way function to return a combinatorial digested
|
219
|
+
# session identification string using a number of distinct parameters that
|
220
|
+
# deliver the important behaviours of changing in certain circumstances
|
221
|
+
# and remaining unchanged in others.
|
222
|
+
#
|
223
|
+
# <b>Change | When Should the key Change?</b>
|
224
|
+
#
|
225
|
+
# What is really important is that the <b>key changes when</b>
|
226
|
+
#
|
227
|
+
# - the <b>command shell</b> changes
|
228
|
+
# - the workstation <b>shell user is switched</b>
|
229
|
+
# - the host machine <b>workstation</b> is changed
|
230
|
+
# - the user <b>SSH's</b> into another shell
|
231
|
+
#
|
232
|
+
# A distinct workstation is identified by the first MAC address and the
|
233
|
+
# hostname of the machine.
|
234
|
+
#
|
235
|
+
# <b>Unchanged | When Should the Key Remain Unchanged?</b>
|
236
|
+
#
|
237
|
+
# Remaining <b>unchanged</b> in certain scenarios is a feature that is
|
238
|
+
# just as important as changing in others. The key must remain
|
239
|
+
# <b>unchanged</b> when
|
240
|
+
#
|
241
|
+
# - the <b>user returns to a command shell</b>
|
242
|
+
# - the user exits their <b>remote SSH session</b>
|
243
|
+
# - <b>sudo is used</b> to execute the commands
|
244
|
+
# - the user comes back to their <b>workstation</b>
|
245
|
+
# - the clock ticks into another day, month, year ...
|
246
|
+
#
|
247
|
+
# @return [OpenKey::Key]
|
248
|
+
# a digested key suitable for short term (session scoped) use with the
|
249
|
+
# guarantee that the same key will be returned whenever called from within
|
250
|
+
# the same executing shell environment and a different key when not.
|
251
|
+
def self.from_session
|
252
|
+
|
253
|
+
require 'macaddr'
|
254
|
+
|
255
|
+
# Do not change the order of this data because it is reversed and
|
256
|
+
# the parent's shell ID hotly followed by the MAC address are the
|
257
|
+
# most significant data points.
|
258
|
+
|
259
|
+
raw_data_string = [
|
260
|
+
Socket.gethostname,
|
261
|
+
OpenSession::Home.instance.username,
|
262
|
+
Mac.addr.to_alphanumeric,
|
263
|
+
Process.ppid.to_s
|
264
|
+
].join.reverse
|
265
|
+
|
266
|
+
return Digest::SHA256.digest( Digest::SHA512.digest( raw_data_string ) )
|
267
|
+
|
268
|
+
end
|
269
|
+
|
270
|
+
|
271
|
+
|
272
|
+
private
|
273
|
+
|
274
|
+
|
275
|
+
|
276
|
+
def self.generate_from_secret_and_salts human_secret, bcrypt_salt, pbkdf2_salt
|
277
|
+
bcrypt_key = OpenKey::BCryptKeyGen.generate_key( human_secret, bcrypt_salt )
|
278
|
+
pbkdf2_key = OpenKey::Pbkdf2KeyGen.generate_key( human_secret.reverse, pbkdf2_salt )
|
279
|
+
return merge_then_digest( bcrypt_key, pbkdf2_key )
|
280
|
+
|
281
|
+
end
|
282
|
+
|
283
|
+
|
284
|
+
def self.merge_then_digest bcrypt_key, pbkdf2_key
|
285
|
+
|
286
|
+
assert_bcrypt_key_bit_length bcrypt_key
|
287
|
+
assert_pbkdf2_key_bit_length pbkdf2_key
|
288
|
+
|
289
|
+
raw_key = bcrypt_key.to_s[ 0 .. (BCRYPT_KEY_CONTRIBUTION_SIZE-1) ] + pbkdf2_key.to_s[ 0 .. (PBKDF2_KEY_CONTRIBUTION_SIZE-1) ]
|
290
|
+
|
291
|
+
assert_amalgam_key_bit_length raw_key
|
292
|
+
assert_amalgam_key_six_bit_count raw_key
|
293
|
+
assert_amalgam_key_eight_bit_count raw_key
|
294
|
+
|
295
|
+
rawbytes_key = [ raw_key.to_s ].pack("B*")
|
296
|
+
digested_key = OpenSSL::Digest::SHA256.new.digest( rawbytes_key )
|
297
|
+
return Key.new ( Base64.urlsafe_encode64( digested_key ) )
|
298
|
+
|
299
|
+
end
|
300
|
+
|
301
|
+
|
302
|
+
def self.assert_bcrypt_key_bit_length bcrypt_key
|
303
|
+
bcrypt_key_bit_length = bcrypt_key.to_s.bytesize
|
304
|
+
bcrypt_keysize_msg = "Expecting #{BCryptKeyGen::BCRYPT_KEY_TRANSPORT_LENGTH} not #{bcrypt_key_bit_length} bits in bcrypt key."
|
305
|
+
raise RuntimeError, bcrypt_keysize_msg unless bcrypt_key_bit_length == BCryptKeyGen::BCRYPT_KEY_TRANSPORT_LENGTH
|
306
|
+
end
|
307
|
+
|
308
|
+
|
309
|
+
def self.assert_pbkdf2_key_bit_length pbkdf2_key
|
310
|
+
pbkdf2_key_bit_length = pbkdf2_key.to_s.bytesize
|
311
|
+
pbkdf2_keysize_msg = "Expecting #{Pbkdf2KeyGen::PBKDF2_KEY_TRANSPORT_LENGTH} not #{pbkdf2_key_bit_length} bits in pbkdf2 key."
|
312
|
+
raise RuntimeError, pbkdf2_keysize_msg unless pbkdf2_key_bit_length == Pbkdf2KeyGen::PBKDF2_KEY_TRANSPORT_LENGTH
|
313
|
+
end
|
314
|
+
|
315
|
+
|
316
|
+
def self.assert_amalgam_key_bit_length amalgam_key
|
317
|
+
|
318
|
+
amalgam_key_bit_length = amalgam_key.to_s.bytesize
|
319
|
+
amalgam_keysize_msg = "Expecting #{AMALGAM_KEY_RAW_BIT_SIZE} not #{amalgam_key_bit_length} bits in amalgam key."
|
320
|
+
raise RuntimeError, amalgam_keysize_msg unless amalgam_key_bit_length == AMALGAM_KEY_RAW_BIT_SIZE
|
321
|
+
end
|
322
|
+
|
323
|
+
|
324
|
+
def self.assert_amalgam_key_six_bit_count amalgam_key
|
325
|
+
amalgam_key_six_bit_length = amalgam_key.to_s.bytesize / 6
|
326
|
+
amalgam_key_six_bit_msg = "Expecting #{AMALGAM_KEY_SIX_BIT_COUNT} six bit blocks not #{amalgam_key_six_bit_length}."
|
327
|
+
raise RuntimeError, amalgam_key_six_bit_msg unless amalgam_key_six_bit_length == AMALGAM_KEY_SIX_BIT_COUNT
|
328
|
+
end
|
329
|
+
|
330
|
+
|
331
|
+
def self.assert_amalgam_key_eight_bit_count amalgam_key
|
332
|
+
amalgam_key_eight_bit_length = amalgam_key.to_s.bytesize / 8
|
333
|
+
amalgam_key_eight_bit_msg = "Expecting #{AMALGAM_KEY_EIGHT_BYTE_COUNT} eight bit blocks not #{amalgam_key_eight_bit_length}."
|
334
|
+
raise RuntimeError, amalgam_key_eight_bit_msg unless amalgam_key_eight_bit_length == AMALGAM_KEY_EIGHT_BYTE_COUNT
|
335
|
+
end
|
336
|
+
|
337
|
+
|
338
|
+
end
|
339
|
+
|
340
|
+
|
341
|
+
end
|