safedb 0.01.0001
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.yardopts +3 -0
- data/Gemfile +10 -0
- data/LICENSE +21 -0
- data/README.md +793 -0
- data/Rakefile +16 -0
- data/bin/safe +5 -0
- data/lib/configs/README.md +58 -0
- data/lib/extension/array.rb +162 -0
- data/lib/extension/dir.rb +35 -0
- data/lib/extension/file.rb +123 -0
- data/lib/extension/hash.rb +33 -0
- data/lib/extension/string.rb +572 -0
- data/lib/factbase/facts.safedb.net.ini +38 -0
- data/lib/interprete.rb +462 -0
- data/lib/keytools/PRODUCE_RAND_SEQ_USING_DEV_URANDOM.txt +0 -0
- data/lib/keytools/kdf.api.rb +243 -0
- data/lib/keytools/kdf.bcrypt.rb +265 -0
- data/lib/keytools/kdf.pbkdf2.rb +262 -0
- data/lib/keytools/kdf.scrypt.rb +190 -0
- data/lib/keytools/key.64.rb +326 -0
- data/lib/keytools/key.algo.rb +109 -0
- data/lib/keytools/key.api.rb +1391 -0
- data/lib/keytools/key.db.rb +330 -0
- data/lib/keytools/key.docs.rb +195 -0
- data/lib/keytools/key.error.rb +110 -0
- data/lib/keytools/key.id.rb +271 -0
- data/lib/keytools/key.ident.rb +243 -0
- data/lib/keytools/key.iv.rb +107 -0
- data/lib/keytools/key.local.rb +259 -0
- data/lib/keytools/key.now.rb +402 -0
- data/lib/keytools/key.pair.rb +259 -0
- data/lib/keytools/key.pass.rb +120 -0
- data/lib/keytools/key.rb +585 -0
- data/lib/logging/gem.logging.rb +132 -0
- data/lib/modules/README.md +43 -0
- data/lib/modules/cryptology/aes-256.rb +154 -0
- data/lib/modules/cryptology/amalgam.rb +70 -0
- data/lib/modules/cryptology/blowfish.rb +130 -0
- data/lib/modules/cryptology/cipher.rb +207 -0
- data/lib/modules/cryptology/collect.rb +138 -0
- data/lib/modules/cryptology/crypt.io.rb +225 -0
- data/lib/modules/cryptology/engineer.rb +99 -0
- data/lib/modules/mappers/dictionary.rb +288 -0
- data/lib/modules/storage/coldstore.rb +186 -0
- data/lib/modules/storage/git.store.rb +399 -0
- data/lib/session/fact.finder.rb +334 -0
- data/lib/session/require.gem.rb +112 -0
- data/lib/session/time.stamp.rb +340 -0
- data/lib/session/user.home.rb +49 -0
- data/lib/usecase/cmd.rb +487 -0
- data/lib/usecase/config/README.md +57 -0
- data/lib/usecase/docker/README.md +146 -0
- data/lib/usecase/docker/docker.rb +49 -0
- data/lib/usecase/edit/README.md +43 -0
- data/lib/usecase/edit/delete.rb +46 -0
- data/lib/usecase/export.rb +40 -0
- data/lib/usecase/files/README.md +37 -0
- data/lib/usecase/files/eject.rb +56 -0
- data/lib/usecase/files/file_me.rb +78 -0
- data/lib/usecase/files/read.rb +169 -0
- data/lib/usecase/files/write.rb +89 -0
- data/lib/usecase/goto.rb +57 -0
- data/lib/usecase/id.rb +36 -0
- data/lib/usecase/import.rb +157 -0
- data/lib/usecase/init.rb +63 -0
- data/lib/usecase/jenkins/README.md +146 -0
- data/lib/usecase/jenkins/jenkins.rb +208 -0
- data/lib/usecase/login.rb +71 -0
- data/lib/usecase/logout.rb +28 -0
- data/lib/usecase/open.rb +71 -0
- data/lib/usecase/print.rb +40 -0
- data/lib/usecase/put.rb +81 -0
- data/lib/usecase/set.rb +44 -0
- data/lib/usecase/show.rb +138 -0
- data/lib/usecase/terraform/README.md +91 -0
- data/lib/usecase/terraform/terraform.rb +121 -0
- data/lib/usecase/token.rb +35 -0
- data/lib/usecase/update/README.md +55 -0
- data/lib/usecase/update/rename.rb +180 -0
- data/lib/usecase/use.rb +41 -0
- data/lib/usecase/verse.rb +20 -0
- data/lib/usecase/view.rb +71 -0
- data/lib/usecase/vpn/README.md +150 -0
- data/lib/usecase/vpn/vpn.ini +31 -0
- data/lib/usecase/vpn/vpn.rb +54 -0
- data/lib/version.rb +3 -0
- data/safedb.gemspec +34 -0
- metadata +193 -0
@@ -0,0 +1,207 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
module SafeDb
|
5
|
+
|
6
|
+
module ToolBelt
|
7
|
+
|
8
|
+
require "base64"
|
9
|
+
|
10
|
+
# {SafeDb::Cipher} is a base class that enables cipher varieties
|
11
|
+
# to be plugged and played with minimal effort. This Cipher implements much
|
12
|
+
# of the use case functionality - all extension classes need to do, is
|
13
|
+
# to subclass and implement only the core behaviour that define its identity.
|
14
|
+
#
|
15
|
+
# == Double Encryption | Cipher Parent vs Cipher Child
|
16
|
+
#
|
17
|
+
# Double encryption first with a symmetric and then an asymmetric one fulfills
|
18
|
+
# the +safe+ promise of making the stored ciphertext utterly worthless.
|
19
|
+
#
|
20
|
+
# The child ciphers implement the inner symmetric encyption whilst the parent
|
21
|
+
# implements the outer asymmetric encryption algorithm.
|
22
|
+
#
|
23
|
+
# The process is done twice resulting in two stores that are mirrored in structure.
|
24
|
+
# The front end store holds doubly encrypted keys whist the backend store holds
|
25
|
+
# the doubly encrypted secrets.
|
26
|
+
#
|
27
|
+
# Attackers wouldn't be able to distinguish one from the other. Even if they
|
28
|
+
# theoretically cracked the asymmetric encryption - they would then be faced
|
29
|
+
# with a powerful symmetric encryption algorithm which could be any one of the
|
30
|
+
# leading ciphers such as TwoFish or the Advanced Encryption Standard (AES).
|
31
|
+
#
|
32
|
+
# == Ciphers at 3 Levels
|
33
|
+
#
|
34
|
+
# Ciphers are implemented at three distinct levels.
|
35
|
+
#
|
36
|
+
# <b>Low Level Ciphers</b>
|
37
|
+
#
|
38
|
+
# Low level ciphers are given text to encrypt and an instantiated dictionary
|
39
|
+
# in which to place the encryption parameters such as keys and initialization
|
40
|
+
# vectors (iv)s.
|
41
|
+
#
|
42
|
+
# Some more specific ciphers can handle authorization data for example the
|
43
|
+
# Galois Counter Mode (GCM) cipher.
|
44
|
+
#
|
45
|
+
# Low level ciphers know nothing about text IO nor reading and writing to
|
46
|
+
# persistence structures like files, queues and databases.
|
47
|
+
#
|
48
|
+
# <b>Mid Level Ciphers</b>
|
49
|
+
#
|
50
|
+
# Mid level ciphers talk to the low level ciphers and bring in input and output
|
51
|
+
# textual formats like SafeDb's two-part block structures.
|
52
|
+
#
|
53
|
+
# Mid level ciphers still know nothing of persistence structures like files,
|
54
|
+
# queues and databases.
|
55
|
+
#
|
56
|
+
# <b>Use Case Level Ciphers</b>
|
57
|
+
#
|
58
|
+
# The ciphers operating at the use case level talk to mid level ciphers. They
|
59
|
+
# interact with the <b>safe store API</b> which brings persistence
|
60
|
+
# functions such as <b>read/write</b> as well as remoting functions such as
|
61
|
+
# <b>push/pull</b>.
|
62
|
+
#
|
63
|
+
# Use Case level ciphers interact with the latest crypt technologies due to
|
64
|
+
# interface separation. Also they talk classes implementing persistence stores
|
65
|
+
# allowing assets liek Git, S3, DropBox, simple files, SSH filesystems, Samba
|
66
|
+
# to hold locked key and material crypts.
|
67
|
+
#
|
68
|
+
# Databases stores will be introduced soon allowing safe to plug in and
|
69
|
+
# exploit database managers like Mongo, Hadoop, MySQL, Maria, and PostgreSQL.
|
70
|
+
#
|
71
|
+
# Plugging into DevOps orchestration platforms like Terraform, Ansible, Chef
|
72
|
+
# and Puppet will soon be available. Add this with integrations to other credential
|
73
|
+
# managers like HashiCorp's Vault, Credstash, Amazon KMS, Git Secrets, PGP,
|
74
|
+
# LastPass, KeePass and KeePassX.
|
75
|
+
#
|
76
|
+
# == How to Implement a Cipher
|
77
|
+
#
|
78
|
+
# Extend this base class to inherit lots of +unexciting+ functionality
|
79
|
+
# that essentially
|
80
|
+
#
|
81
|
+
# - manages the main encryption and decryption use case flow
|
82
|
+
# - +concatenates+ the symmetric encryption meta data with ciphertext +after encryption+
|
83
|
+
# - _splits_ and objectifies the key/value metadata plus ciphertext +before decryption+
|
84
|
+
# - +handles file read/writes+ in conjunction with the store plugins
|
85
|
+
# - handles +exceptions+ and +malicious input detection+ and incubation
|
86
|
+
# - +_performs the asymmetric encryption_+ of the cipher's symmetrically encrypted output
|
87
|
+
#
|
88
|
+
# == What Behaviour Must Ciphers Implement
|
89
|
+
#
|
90
|
+
# Ciphers bring the cryptographic mathematics and implementation algorithms
|
91
|
+
# to the table. So when at home they must implement
|
92
|
+
#
|
93
|
+
# - <tt>do_symmetric_encryption(plain_text)</tt> - resulting in ciphertext
|
94
|
+
# - <tt>do_symmetric_decryption(ciphertext, encryption_dictionary)</tt> » plaintext
|
95
|
+
#
|
96
|
+
# and also set the <tt>@dictionary</tt> hash (map) of pertinent
|
97
|
+
# key/value pairs including the encryption algorithm, the encryption key and
|
98
|
+
# the ciphertext signature to thwart any at-rest tampering.
|
99
|
+
#
|
100
|
+
# That's It. Cipher children can rely on the {SafeDb::Cipher} parent to
|
101
|
+
# do the nitty gritty of file-handling plus managing stores and paths.
|
102
|
+
class Cipher
|
103
|
+
|
104
|
+
# Ciphers use <b>symmetric algorithms</b> to encrypt the given text, which
|
105
|
+
# is then wrapped up along with the encryption key and other <b>metadata</b>
|
106
|
+
# pertinent to the algorithm, they then encrypt this bundle with the
|
107
|
+
# <b>public key</b> provided and return the text that can safely be stored in
|
108
|
+
# a text file.
|
109
|
+
#
|
110
|
+
# Ciphers should never interact with the filesystem which makes them
|
111
|
+
# reusable in API and remote store scenarios.
|
112
|
+
#
|
113
|
+
# Binary files should be converted into the base64 format before being
|
114
|
+
# presented to ciphers.
|
115
|
+
#
|
116
|
+
# Every component in the pipeline bears the responsibility for nullifying
|
117
|
+
# and rejecting malicious content.
|
118
|
+
#
|
119
|
+
# @param public_key [OpenSSL::PKey::RSA]
|
120
|
+
# an {OpenSSL::PKey::RSA} public key. The unique selling point of
|
121
|
+
# asymmetric encryption is it can be done without recourse to the heavily
|
122
|
+
# protected private key. Thus the encryption process can continue with
|
123
|
+
# just a public key as long as its authenticity is assured.
|
124
|
+
#
|
125
|
+
# @param payload_text [String]
|
126
|
+
# plaintext (or base64 encoded) text to encrypt
|
127
|
+
#
|
128
|
+
# @return [String] doubly (symmetric and asymmetric) encrypted cipher text
|
129
|
+
def self.encrypt_it public_key, payload_text
|
130
|
+
|
131
|
+
crypt_data = {}
|
132
|
+
crypted_payload = Base64.encode64( Aes256.do_encrypt( crypt_data, payload_text ) )
|
133
|
+
unified_material = CryptIO.inner_crypt_serialize crypt_data, crypted_payload
|
134
|
+
|
135
|
+
outer_crypt_key = Engineer.strong_key( 128 )
|
136
|
+
crypted_cryptkey = Base64.encode64( public_key.public_encrypt( outer_crypt_key ) )
|
137
|
+
|
138
|
+
crypted_material = Base64.encode64(Blowfish.encryptor unified_material, outer_crypt_key)
|
139
|
+
|
140
|
+
return CryptIO.outer_crypt_serialize( crypted_cryptkey, crypted_material )
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
# This method takes and <b><em>safe formatted</em></b> cipher-text block
|
146
|
+
# generated by {self.encrypt_it} and returns the original message that has effectively
|
147
|
+
# been doubly encrypted using a symmetric and asymmetric cipher. This type of
|
148
|
+
# encryption is standard best practice when serializing secrets.
|
149
|
+
#
|
150
|
+
# safe cipher-text blocks <b><em>look like a two(2) part bundle</em></b>
|
151
|
+
# but they are <b><em>actually a three(3) part bundle</em></b> because the second
|
152
|
+
# part is in itself an amalgam of two distinct objects, serialized as text blocks.
|
153
|
+
#
|
154
|
+
# <b>The 3 SafeDb Blocks</b>
|
155
|
+
#
|
156
|
+
# Even though the incoming text <b><em>appears to contain two (2) blocks</em></b>,
|
157
|
+
# it <b><em>actually contains three (3)</em></b>.
|
158
|
+
#
|
159
|
+
# - a massive symmetric encryption key (locked by an asymmetric keypair)
|
160
|
+
# - a dictionary denoting the algorithm and parameters used to encrypt the 3rd block
|
161
|
+
# - the true message whose encryption is parametized by the dictionary (in 2nd block)
|
162
|
+
#
|
163
|
+
# The second and third block are only revealed by asymmetrically decrypting
|
164
|
+
# the key in the first block and using it to symmetrically decrypt what appears
|
165
|
+
# to be a unified second block.
|
166
|
+
#
|
167
|
+
# @param private_key [OpenSSL::PKey::RSA]
|
168
|
+
# the <b>asymmetric private key</b> whose corresponding public key was
|
169
|
+
# employed during the encryption of a super-strong 128 character symmetric
|
170
|
+
# key embalmed by the first ciphertext block.
|
171
|
+
#
|
172
|
+
# @param os_block_text [String]
|
173
|
+
# the locked cipher text is the safe formatted block which comes
|
174
|
+
# in two main chunks. First is the <b>long strong</b> symmetric encryption
|
175
|
+
# key crypted with the public key portion of the private key in the first
|
176
|
+
# parameter.
|
177
|
+
#
|
178
|
+
# The second chunk is the symmetrically crypted text that was locked with
|
179
|
+
# the encryption key revealed in the first chunk.
|
180
|
+
#
|
181
|
+
# @return [String]
|
182
|
+
# the doubly encrypted plain text that is locked by a symmetric key and
|
183
|
+
# that symmetric key itself locked using the public key portion of the
|
184
|
+
# private key whose crypted form is presented in the first parameter.
|
185
|
+
def self.decrypt_it private_key, os_block_text
|
186
|
+
|
187
|
+
first_block = Base64.decode64( CryptIO.outer_crypt_deserialize os_block_text, true )
|
188
|
+
trail_block = Base64.decode64( CryptIO.outer_crypt_deserialize os_block_text, false )
|
189
|
+
|
190
|
+
decrypt_key = private_key.private_decrypt first_block
|
191
|
+
inner_block = Blowfish.decryptor( trail_block, decrypt_key )
|
192
|
+
|
193
|
+
crypt_props = Hash.new
|
194
|
+
cipher_text = CryptIO.inner_crypt_deserialize( crypt_props, inner_block )
|
195
|
+
|
196
|
+
return Aes256.do_decrypt( crypt_props, cipher_text )
|
197
|
+
|
198
|
+
end
|
199
|
+
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
|
207
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
module SafeDb
|
4
|
+
|
5
|
+
module ToolBelt
|
6
|
+
|
7
|
+
require 'io/console'
|
8
|
+
|
9
|
+
# This class will be refactored into an interface implemented by a set
|
10
|
+
# of plugins that will capture sensitive information from users from an
|
11
|
+
# Ubuntu, Windows, RHEL, CoreOS, iOS or CentOS command line interface.
|
12
|
+
#
|
13
|
+
# An equivalent REST API will also be available for bringing in sensitive
|
14
|
+
# information in the most secure (but simple) manner.
|
15
|
+
class Collect
|
16
|
+
|
17
|
+
|
18
|
+
# <tt>Collect something sensitive from the command line</tt> with a
|
19
|
+
# minimum length specified in the first parameter. This method can't
|
20
|
+
# know whether the information is a password, a pin number or whatever
|
21
|
+
# so it takes the integer minimum size at its word.
|
22
|
+
#
|
23
|
+
# <b>Question 5 to App Config | What is the Secret?</b>
|
24
|
+
#
|
25
|
+
# The client may need to acquire the secret if the answer to question 4 indicates the need
|
26
|
+
# to instantiate the keys and encrypt the application's plaintext database. The application
|
27
|
+
# should facilitate communication of the secret via
|
28
|
+
#
|
29
|
+
# - an environment variable
|
30
|
+
# - the system clipboard (cleared after reading)
|
31
|
+
# - a file whose path is a command parameter
|
32
|
+
# - a file in a pre-agreed location
|
33
|
+
# - a file in the present directory (with a pre-agreed name)
|
34
|
+
# - a URL from a parameter or pre-agreed
|
35
|
+
# - the shell's secure password reader
|
36
|
+
# - the DConf / GConf or GSettings configuration stores
|
37
|
+
# - a REST API
|
38
|
+
# - password managers like LastPass, KeePassX or 1Pass
|
39
|
+
# - the Amazon KMS (Key Management Store)
|
40
|
+
# - vaults from Ansible, Terraform and Kubernetes
|
41
|
+
# - credential managers like GitSecrets and Credstash
|
42
|
+
#
|
43
|
+
# @param min_size [Integer] the minimum size of the collected secret
|
44
|
+
# whereby one (1) is the least we can expect. The maximum bound is
|
45
|
+
# not constrained here so will fall under what is allowed by the
|
46
|
+
# interface, be it a CLI, Rest API, Web UI or Mobile App.
|
47
|
+
#
|
48
|
+
# @param prompt_twice [Boolean] indicate whether the user should be
|
49
|
+
# prompted twice. If true the prompt_2 text must be provided and
|
50
|
+
# converse is also true. A true value asserts that both times the
|
51
|
+
# user enters the same (case sensitive) string.
|
52
|
+
#
|
53
|
+
# @param prompt_1 [String] the text (aide memoire) used to prompt the user
|
54
|
+
#
|
55
|
+
# @param prompt_2 [String] if the prompt twice boolean is TRUE, this
|
56
|
+
# second prompt (aide memoire) must be provided.
|
57
|
+
#
|
58
|
+
# @return [String] the collected string text ( watch out for non-ascii chars)
|
59
|
+
# @raise [ArgumentError] if the minimum size is less than one
|
60
|
+
def self.secret_text min_size, prompt_twice, prompt_1, prompt_2=nil
|
61
|
+
|
62
|
+
assert_min_size min_size
|
63
|
+
|
64
|
+
sleep(1)
|
65
|
+
puts "\n#{prompt_1} : "
|
66
|
+
first_secret = STDIN.noecho(&:gets).chomp
|
67
|
+
|
68
|
+
assert_input_text_size first_secret.length, min_size
|
69
|
+
return first_secret unless prompt_twice
|
70
|
+
|
71
|
+
sleep(1)
|
72
|
+
puts "\n#{prompt_2} : "
|
73
|
+
check_secret = STDIN.noecho(&:gets).chomp
|
74
|
+
|
75
|
+
assert_same_size_text first_secret, check_secret
|
76
|
+
|
77
|
+
return first_secret
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
# --
|
83
|
+
# -- Raise an exception if asked to collect text that is less
|
84
|
+
# -- than 3 characters in length.
|
85
|
+
# --
|
86
|
+
def self.assert_min_size min_size
|
87
|
+
|
88
|
+
min_length_msg = "\n\nCrypts with 2 (or less) characters open up exploitable holes.\n\n"
|
89
|
+
raise ArgumentError.new min_length_msg if min_size < 3
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
# --
|
95
|
+
# -- Output an error message and then exit if the entered input
|
96
|
+
# -- text size does not meet the minimum requirements.
|
97
|
+
# --
|
98
|
+
def self.assert_input_text_size input_size, min_size
|
99
|
+
|
100
|
+
if( input_size < min_size )
|
101
|
+
|
102
|
+
puts
|
103
|
+
puts "Input is too short. Please enter at least #{min_size} characters."
|
104
|
+
puts
|
105
|
+
|
106
|
+
exit
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
# --
|
114
|
+
# -- Assert that the text entered the second time is exactly (case sensitive)
|
115
|
+
# -- the same as the text entered the first time.
|
116
|
+
# --
|
117
|
+
def self.assert_same_size_text first_text, second_text
|
118
|
+
|
119
|
+
unless( first_text.eql? second_text )
|
120
|
+
|
121
|
+
puts
|
122
|
+
puts "Those two bits of text are not the same (in my book)!"
|
123
|
+
puts
|
124
|
+
|
125
|
+
exit
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
module SafeDb
|
5
|
+
|
6
|
+
module ToolBelt
|
7
|
+
|
8
|
+
# CryptIO concentrates on injecting and ingesting crypt properties into and
|
9
|
+
# out of a key/value dictionary as well as injecting and ingesting cryptographic
|
10
|
+
# materials into and out of text files.
|
11
|
+
#
|
12
|
+
# == Cryptographic Properties
|
13
|
+
#
|
14
|
+
# A crypt properties dictionary acts as <b>output from every encryption event</b>
|
15
|
+
# and <b>input to every decryption event</b>. The most common properties include
|
16
|
+
#
|
17
|
+
# - the symmetric key used for the encryption and decryption
|
18
|
+
# - the iv (initialization vector) that adds another dimension of strength
|
19
|
+
# - authorization data that thwarts switch attacks by tying context to content
|
20
|
+
# - the cipher algorithm, its implementation and its encryption strength
|
21
|
+
# - the various glue strings that allow related ciphertext to occupy a file
|
22
|
+
#
|
23
|
+
# == Why Pad?
|
24
|
+
#
|
25
|
+
# Many ciphers (like Blowfish) constrains plain text lengths to multiples
|
26
|
+
# of 8 (or 16) and a common <b>right pad with spaces</b> strategy is employed
|
27
|
+
# as a workaround. safe does it diferently.
|
28
|
+
#
|
29
|
+
# == Why isn't Space Padding Used?
|
30
|
+
#
|
31
|
+
# If safe padded plaintext (ending in one or more spaces) with
|
32
|
+
# spaces, the decrypt phase (after right stripping spaces) would return
|
33
|
+
# plain text string <b>shorter than the original</b>.
|
34
|
+
#
|
35
|
+
# == Why Unusual Padding and Separators
|
36
|
+
#
|
37
|
+
# Why does safe employ unusual strings for padding and separation.
|
38
|
+
#
|
39
|
+
# The separator string must be unusual to make it unlikely for it to occur in any
|
40
|
+
# of the map's key value pairs nor indeed the chunk of text being glued. Were
|
41
|
+
# this to happen, the separate and reconstitute phase may not accurately return
|
42
|
+
# the same two entities we are employed to unite.
|
43
|
+
#
|
44
|
+
# == So How is Padding Done?
|
45
|
+
#
|
46
|
+
# Instead of single space padding - safe uses an unlikely 7 character
|
47
|
+
# padder which is repeated until the multiple is reached.
|
48
|
+
#
|
49
|
+
# <tt><-|@|-></tt>
|
50
|
+
#
|
51
|
+
# == So How is Padding Done?
|
52
|
+
#
|
53
|
+
# The <b>padder length must be a prime number</b> or infinite loops could occur.
|
54
|
+
#
|
55
|
+
# If the padder string is likely to occur in the plain text, another
|
56
|
+
# padder (or strategy) should and could be employed.
|
57
|
+
#
|
58
|
+
class CryptIO
|
59
|
+
|
60
|
+
|
61
|
+
# The safe text padder. See the class description for an analysis
|
62
|
+
# of the use of this type of padder.
|
63
|
+
TEXT_PADDER = "<-|@|->"
|
64
|
+
|
65
|
+
# An unusual string that glues together an encryption dictionary and
|
66
|
+
# a chunk of base64 encoded and encrypted ciphertext.
|
67
|
+
# The string must be unusual enough to ensure it does not occur within
|
68
|
+
# the dictionary metadata keys or values.
|
69
|
+
INNER_GLUE_STRING = "\n<-|@| < || safe inner crypt material axis || > |@|->\n\n"
|
70
|
+
|
71
|
+
# An unusual string that glues together the asymmetrically encrypted outer
|
72
|
+
# encryption key with the outer crypted text.
|
73
|
+
OUTER_GLUE_STRING = "\n<-|@| < || safe outer crypt material axis || > |@|->\n\n"
|
74
|
+
|
75
|
+
# Text header for key-value pairs hash map that will be serialized.
|
76
|
+
DICT_HEADER_NAME = "crypt.properties"
|
77
|
+
|
78
|
+
# Name for the class of cipher employed.
|
79
|
+
DICT_CIPHER_NAME = "cipher.class"
|
80
|
+
|
81
|
+
# Name for the {Base64} encoded symmetric (lock/unlock) crypt key.
|
82
|
+
DICT_CRYPT_KEY = "encryption.key"
|
83
|
+
|
84
|
+
# Dictionary name for the encryption iv (initialization vector)
|
85
|
+
DICT_CRYPT_IV = "encryption.iv"
|
86
|
+
|
87
|
+
# Dictionary name for the Base64 (urlsafe) encoded plaintext digest.
|
88
|
+
DICT_TEXT_DIGEST = "plaintext.digest"
|
89
|
+
|
90
|
+
|
91
|
+
# Serialize and then unify a hash map and a textual chunk using
|
92
|
+
# a known but unusual separator string in a manner that protects
|
93
|
+
# content integrity during the serialize / deserialize process.
|
94
|
+
#
|
95
|
+
# This crypt serialization uses a specific "inner glue" as the
|
96
|
+
# string that separates the serialized key/value dictionary and
|
97
|
+
# the encoded textual block.
|
98
|
+
#
|
99
|
+
# @param hash_map [String]
|
100
|
+
# this hash (dictionary) will be serialized into INI formatted text
|
101
|
+
# using behaviour from {Hash} and {IniFile}.
|
102
|
+
#
|
103
|
+
# @param text_chunk [String]
|
104
|
+
# the usually Base64 encrypted textual block to be glued at the
|
105
|
+
# bottom of the returned block.
|
106
|
+
#
|
107
|
+
# @return [String] serialized and glued together result of map plus text
|
108
|
+
#
|
109
|
+
# @raise [ArgumentError]
|
110
|
+
# if the dictionary hash_map is either nil or empty.
|
111
|
+
def self.inner_crypt_serialize hash_map, text_chunk
|
112
|
+
|
113
|
+
nil_or_empty_hash = hash_map.nil? || hash_map.empty?
|
114
|
+
raise ArgumentError, "Cannot serialize nil or empty properties." if nil_or_empty_hash
|
115
|
+
ini_map = IniFile.new
|
116
|
+
ini_map[ DICT_HEADER_NAME ] = hash_map
|
117
|
+
return ini_map.to_s + INNER_GLUE_STRING + text_chunk
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
# Deserialize an safe formatted text which contains an
|
123
|
+
# encryption properties dictionary (serialized in INI format)
|
124
|
+
# and a Base64 encoded crypt block which is the subject of the
|
125
|
+
# encryption dictionary.
|
126
|
+
#
|
127
|
+
# The crypt serialization used a specific "inner glue" as the
|
128
|
+
# string that separates the serialized key/value dictionary and
|
129
|
+
# the encoded textual block. We now employ this glue to split
|
130
|
+
# the serialized dictionary from the textual block.
|
131
|
+
#
|
132
|
+
# @param hash_map [String]
|
133
|
+
# send an instantiated hash (dictionary) which will be populated
|
134
|
+
# by this deserialize operation. The dictionary propeties can
|
135
|
+
# then be used to decrypt the returned ciphertext.
|
136
|
+
#
|
137
|
+
# @param text_block [String]
|
138
|
+
# the first of a two-part amalgamation is a hash (dictionary) in
|
139
|
+
# INI serialized form and the second part is a Base64 encrypted
|
140
|
+
# textual block.
|
141
|
+
#
|
142
|
+
# The deserialized key/value pairs will be stuffed into the
|
143
|
+
# non nil (usually empty) hash map in the first parameter and
|
144
|
+
# the block (in the 2nd part) will be Base64 decoded and
|
145
|
+
# returned by this method.
|
146
|
+
#
|
147
|
+
# @return [String]
|
148
|
+
# The encoded block in the 2nd part of the 2nd parameter will be
|
149
|
+
# Base64 decoded and returned.
|
150
|
+
#
|
151
|
+
# @raise [ArgumentError]
|
152
|
+
# if the dictionary hash_map is either nil or empty. Also if
|
153
|
+
# the inner glue tying the two parts together is missing an
|
154
|
+
# {ArgumentError} will be thrown.
|
155
|
+
def self.inner_crypt_deserialize hash_map, text_block
|
156
|
+
|
157
|
+
raise ArgumentError, "Cannot populate a nil hash map." if hash_map.nil?
|
158
|
+
assert_contains_glue text_block, INNER_GLUE_STRING
|
159
|
+
|
160
|
+
serialized_map = text_block.split(INNER_GLUE_STRING).first.strip
|
161
|
+
encoded64_text = text_block.split(INNER_GLUE_STRING).last.strip
|
162
|
+
ini_props_hash = IniFile.new( :content => serialized_map )
|
163
|
+
encrypt_values = ini_props_hash[DICT_HEADER_NAME]
|
164
|
+
|
165
|
+
hash_map.merge!( encrypt_values )
|
166
|
+
return Base64.decode64( encoded64_text )
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
|
171
|
+
# Using an outer divider (glue) - attach the asymmetrically encrypted outer
|
172
|
+
# encryption key with the outer encrypted text.
|
173
|
+
#
|
174
|
+
# @param crypt_material_x [String] asymmetrically encrypted (encoded) outer encryption key
|
175
|
+
# @param crypt_material_y [String] symmetrically encrypted inner metadata and payload crypt
|
176
|
+
#
|
177
|
+
# @return [String] concatenated result of the two crypt materials and divider string
|
178
|
+
def self.outer_crypt_serialize crypt_material_x, crypt_material_y
|
179
|
+
return crypt_material_x + OUTER_GLUE_STRING + crypt_material_y
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
# Given two blocks of text that were bounded together by the
|
184
|
+
# {self.outer_crypt_serialize} method we must return either the
|
185
|
+
# first block (true) or the second (false).
|
186
|
+
#
|
187
|
+
# @param crypt_material [String]
|
188
|
+
# large block of text in two parts that is divided by the
|
189
|
+
# outer glue string.
|
190
|
+
#
|
191
|
+
# @param top_block [Boolean]
|
192
|
+
# if true the top (of the two) blocks will be returned
|
193
|
+
# otherwise the bottom block is returned.
|
194
|
+
#
|
195
|
+
# @return [String] either the first or second block of text
|
196
|
+
#
|
197
|
+
# @raise [ArgumentError]
|
198
|
+
# If the outer glue string tying the two parts together is
|
199
|
+
# missing an {ArgumentError} will be thrown.
|
200
|
+
def self.outer_crypt_deserialize os_material, top_block
|
201
|
+
|
202
|
+
assert_contains_glue os_material, OUTER_GLUE_STRING
|
203
|
+
return os_material.split(OUTER_GLUE_STRING).first.strip if top_block
|
204
|
+
return os_material.split(OUTER_GLUE_STRING).last.strip
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
def self.assert_contains_glue os_crypted_block, glue_string
|
212
|
+
|
213
|
+
no_glue_msg = "\nGlue string not in safe cipher block.\n#{glue_string}\n"
|
214
|
+
raise ArgumentError, no_glue_msg unless os_crypted_block.include? glue_string
|
215
|
+
|
216
|
+
end
|
217
|
+
|
218
|
+
|
219
|
+
end
|
220
|
+
|
221
|
+
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
module SafeDb
|
4
|
+
|
5
|
+
module ToolBelt
|
6
|
+
|
7
|
+
|
8
|
+
require 'securerandom'
|
9
|
+
|
10
|
+
# This class will be refactored into an interface implemented by a set
|
11
|
+
# of plugins that will capture sensitive information from users from an
|
12
|
+
# Ubuntu, Windows, RHEL, CoreOS, iOS or CentOS command line interface.
|
13
|
+
#
|
14
|
+
# An equivalent REST API will also be available for bringing in sensitive
|
15
|
+
# information in the most secure (but simple) manner.
|
16
|
+
class Engineer
|
17
|
+
|
18
|
+
|
19
|
+
# --
|
20
|
+
# -- Get a viable machine password taking into account the human
|
21
|
+
# -- password length and the specified mix_ratio.
|
22
|
+
# --
|
23
|
+
# -- machine password length = human password length * mix_ratio - 1
|
24
|
+
# --
|
25
|
+
def self.machine_key human_password_length, mix_ratio
|
26
|
+
|
27
|
+
machine_raw_secret = strong_key( human_password_length * ( mix_ratio + 1) )
|
28
|
+
return machine_raw_secret[ 0..( human_password_length * mix_ratio - 1 ) ]
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# --
|
34
|
+
# -- Engineer a raw password that is similar (approximate) in
|
35
|
+
# -- length to the integer parameter.
|
36
|
+
# --
|
37
|
+
def self.strong_key approx_length
|
38
|
+
|
39
|
+
non_alphanum = SecureRandom.urlsafe_base64(approx_length);
|
40
|
+
return non_alphanum.delete("-_")
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# Amalgamate the parameter passwords using a specific mix ratio. This method
|
46
|
+
# produces cryptographically stronger secrets than algorithms that simply
|
47
|
+
# concatenate two string keys together. If knowledge of one key were gained, this
|
48
|
+
# amalgamation algorithm still provides extremely strong protection even when
|
49
|
+
# one of the keys has a single digit length.
|
50
|
+
#
|
51
|
+
# This +length constraint formula+ binds the two input strings together with
|
52
|
+
# the integer mix ratio.
|
53
|
+
#
|
54
|
+
# <tt>machine password length = human password length * mix_ratio - 1</tt>
|
55
|
+
#
|
56
|
+
# @param human_password [String] the first password (shorter one) to amalgamate
|
57
|
+
# @param machine_password [String] the second password (longer one) to amalgamate
|
58
|
+
# @param mix_ratio [Fixnum] the mix ratio that must be respected by the
|
59
|
+
# previous two parameters.
|
60
|
+
# @return [String] an amalgamated (reproducible) union of the 2 parameter passwords
|
61
|
+
#
|
62
|
+
# @raise [ArgumentError] if the length constraint assertion does not hold true
|
63
|
+
def self.get_amalgam_password human_password, machine_password, mix_ratio
|
64
|
+
|
65
|
+
size_error_msg = "Human pass length times mix_ratio must equal machine pass length."
|
66
|
+
lengths_are_perfect = human_password.length * mix_ratio == machine_password.length
|
67
|
+
raise ArgumentError.new size_error_msg unless lengths_are_perfect
|
68
|
+
|
69
|
+
machine_passwd_chunk = 0
|
70
|
+
amalgam_passwd_index = 0
|
71
|
+
amalgamated_password = ""
|
72
|
+
|
73
|
+
human_password.each_char do |passwd_char|
|
74
|
+
|
75
|
+
amalgamated_password[amalgam_passwd_index] = passwd_char
|
76
|
+
amalgam_passwd_index += 1
|
77
|
+
|
78
|
+
for i in 0..(mix_ratio-1) do
|
79
|
+
machine_pass_index = machine_passwd_chunk * mix_ratio + i
|
80
|
+
amalgamated_password[amalgam_passwd_index] = machine_password[machine_pass_index]
|
81
|
+
amalgam_passwd_index += 1
|
82
|
+
end
|
83
|
+
|
84
|
+
machine_passwd_chunk += 1
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
return amalgamated_password
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
end
|