safedb 0.3.1011 → 0.4.1002

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +56 -19
  3. data/README.md +15 -15
  4. data/Rakefile +7 -0
  5. data/bin/safe +2 -2
  6. data/lib/{interprete.rb → cli.rb} +168 -121
  7. data/lib/controller/admin/README.md +47 -0
  8. data/lib/controller/admin/access.rb +47 -0
  9. data/lib/controller/admin/checkin.rb +83 -0
  10. data/lib/controller/admin/checkout.rb +57 -0
  11. data/lib/controller/admin/diff.rb +75 -0
  12. data/lib/{usecase → controller/admin}/export.rb +15 -14
  13. data/lib/controller/admin/goto.rb +52 -0
  14. data/lib/controller/admin/import.rb +54 -0
  15. data/lib/controller/admin/init.rb +113 -0
  16. data/lib/controller/admin/login.rb +88 -0
  17. data/lib/{usecase → controller/admin}/logout.rb +0 -0
  18. data/lib/controller/admin/open.rb +39 -0
  19. data/lib/{usecase → controller/admin}/token.rb +2 -2
  20. data/lib/controller/admin/tree.md +54 -0
  21. data/lib/{usecase → controller/admin}/use.rb +0 -0
  22. data/lib/controller/admin/view.rb +61 -0
  23. data/lib/{usecase → controller/api}/docker/README.md +0 -0
  24. data/lib/{usecase → controller/api}/docker/docker.rb +1 -1
  25. data/lib/{usecase → controller/api}/jenkins/README.md +0 -0
  26. data/lib/{usecase → controller/api}/jenkins/jenkins.rb +1 -1
  27. data/lib/{usecase → controller/api}/terraform/README.md +1 -1
  28. data/lib/{usecase → controller/api}/terraform/terraform.rb +1 -1
  29. data/lib/{usecase → controller/api}/vpn/README.md +1 -1
  30. data/lib/{usecase → controller/api}/vpn/vpn.ini +0 -0
  31. data/lib/{usecase → controller/api}/vpn/vpn.rb +0 -0
  32. data/lib/{usecase → controller}/config/README.md +0 -0
  33. data/lib/{usecase → controller}/edit/README.md +0 -0
  34. data/lib/controller/edit/editverse.rb +48 -0
  35. data/lib/controller/edit/put.rb +35 -0
  36. data/lib/controller/edit/remove.rb +29 -0
  37. data/lib/{usecase/update/README.md → controller/edit/rename.md} +0 -0
  38. data/lib/{usecase → controller}/files/README.md +1 -1
  39. data/lib/controller/files/read.rb +36 -0
  40. data/lib/{usecase/files/eject.rb → controller/files/write.rb} +15 -20
  41. data/lib/{usecase → controller}/id.rb +0 -0
  42. data/lib/controller/query/print.rb +26 -0
  43. data/lib/controller/query/queryverse.rb +39 -0
  44. data/lib/controller/query/show.rb +50 -0
  45. data/lib/{session/require.gem.rb → controller/requirer.rb} +13 -9
  46. data/lib/{usecase → controller}/set.rb +4 -4
  47. data/lib/controller/usecase.rb +244 -0
  48. data/lib/{usecase → controller}/verse.rb +0 -0
  49. data/lib/{usecase → controller}/visit/README.md +0 -0
  50. data/lib/{usecase → controller}/visit/visit.rb +0 -0
  51. data/lib/factbase/facts.safedb.net.ini +7 -7
  52. data/lib/{keytools/key.docs.rb → model/README.md} +102 -66
  53. data/lib/model/book.rb +484 -0
  54. data/lib/model/branch.rb +48 -0
  55. data/lib/model/checkin.feature +33 -0
  56. data/lib/{configs/README.md → model/configs.md} +4 -4
  57. data/lib/model/content.rb +214 -0
  58. data/lib/model/indices.rb +132 -0
  59. data/lib/model/safe_tree.rb +51 -0
  60. data/lib/model/state.inspect.rb +221 -0
  61. data/lib/model/state.migrate.rb +334 -0
  62. data/lib/model/text_chunk.rb +68 -0
  63. data/lib/{extension → utils/extend}/array.rb +0 -0
  64. data/lib/{extension → utils/extend}/dir.rb +0 -0
  65. data/lib/{extension → utils/extend}/file.rb +0 -0
  66. data/lib/utils/extend/hash.rb +76 -0
  67. data/lib/{extension → utils/extend}/string.rb +6 -6
  68. data/lib/{session/fact.finder.rb → utils/facts/fact.rb} +0 -0
  69. data/lib/utils/identity/identifier.rb +356 -0
  70. data/lib/{keytools/key.ident.rb → utils/identity/machine.id.rb} +67 -4
  71. data/lib/utils/inspect/inspector.rb +81 -0
  72. data/lib/{keytools/kdf.bcrypt.rb → utils/kdfs/bcrypt.rb} +0 -0
  73. data/lib/{keytools → utils/kdfs}/kdf.api.rb +16 -16
  74. data/lib/{keytools/key.local.rb → utils/kdfs/kdfs.rb} +40 -40
  75. data/lib/{keytools/kdf.pbkdf2.rb → utils/kdfs/pbkdf2.rb} +0 -0
  76. data/lib/{keytools/kdf.scrypt.rb → utils/kdfs/scrypt.rb} +0 -0
  77. data/lib/{keytools → utils}/key.error.rb +2 -2
  78. data/lib/{keytools → utils}/key.pass.rb +2 -2
  79. data/lib/{keytools → utils/keys}/key.64.rb +0 -0
  80. data/lib/{keytools → utils/keys}/key.rb +6 -2
  81. data/lib/{keytools/key.iv.rb → utils/keys/random.iv.rb} +0 -0
  82. data/lib/{logging/gem.logging.rb → utils/logs/logger.rb} +6 -5
  83. data/lib/{keytools/key.pair.rb → utils/store/datamap.rb} +48 -30
  84. data/lib/{keytools/key.db.rb → utils/store/datastore.rb} +38 -104
  85. data/lib/utils/store/merge-boys-school.json +40 -0
  86. data/lib/utils/store/merge-girls-school.json +48 -0
  87. data/lib/utils/store/merge-merged-data.json +56 -0
  88. data/lib/utils/store/struct.rb +75 -0
  89. data/lib/utils/store/test-commands.sh +24 -0
  90. data/lib/{keytools/key.now.rb → utils/time/timestamp.rb} +32 -21
  91. data/lib/version.rb +1 -1
  92. metadata +86 -73
  93. data/lib/extension/hash.rb +0 -33
  94. data/lib/keytools/key.algo.rb +0 -109
  95. data/lib/keytools/key.api.rb +0 -1326
  96. data/lib/keytools/key.id.rb +0 -322
  97. data/lib/modules/cryptology/amalgam.rb +0 -70
  98. data/lib/modules/cryptology/engineer.rb +0 -99
  99. data/lib/modules/mappers/dictionary.rb +0 -288
  100. data/lib/session/time.stamp.rb +0 -340
  101. data/lib/session/user.home.rb +0 -49
  102. data/lib/usecase/cmd.rb +0 -471
  103. data/lib/usecase/edit/delete.rb +0 -46
  104. data/lib/usecase/files/file_me.rb +0 -78
  105. data/lib/usecase/files/read.rb +0 -169
  106. data/lib/usecase/files/write.rb +0 -89
  107. data/lib/usecase/goto.rb +0 -57
  108. data/lib/usecase/import.rb +0 -157
  109. data/lib/usecase/init.rb +0 -61
  110. data/lib/usecase/login.rb +0 -72
  111. data/lib/usecase/open.rb +0 -71
  112. data/lib/usecase/print.rb +0 -40
  113. data/lib/usecase/put.rb +0 -81
  114. data/lib/usecase/show.rb +0 -138
  115. data/lib/usecase/update/rename.rb +0 -180
  116. data/lib/usecase/view.rb +0 -71
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/ruby
2
+
3
+ module SafeDb
4
+
5
+ # These textual chunks will hold information blocks that are printed into files
6
+ # or printed out for user consumption on standard out.
7
+ class TextChunk
8
+
9
+ # Construct the header for the ciphertext content files written out
10
+ # onto the filesystem including information such as the application version
11
+ # and human readable time.
12
+ #
13
+ # @param book_id [String] the identifier of the book that is being queried and edited
14
+ # @return [String] a textual block that can be printed at the top of crypt files
15
+ def self.crypt_header( book_id )
16
+
17
+ <<-CRYPT_HEADER
18
+ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
19
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
20
+ #{Indices::SAFE_URL_NAME} ciphertext block
21
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
22
+ Safe Book Id := #{book_id}
23
+ Time Created := #{KeyNow.readable()}
24
+ Safe Version := #{Indices::SAFE_VERSION_STRING}
25
+ Safe Website := #{Indices::SAFE_GEM_WEBSITE}
26
+ RubyGems.org := https://rubygems.org/gems/safedb
27
+ CRYPT_HEADER
28
+
29
+ end
30
+
31
+
32
+ # Print a message stating that the book has not been opened at any
33
+ # chapter and verse location.
34
+ def self.not_open_message()
35
+
36
+ <<-UNOPENED_MESSAGE
37
+
38
+ Please open a chapter and verse to put, edit or query data.
39
+
40
+ #{Indices::COMMANDER} open contacts monica
41
+
42
+ then add monica's contact details
43
+
44
+ #{Indices::COMMANDER} put email monica.lewinsky@gmail.com
45
+ #{Indices::COMMANDER} put phone +1-357-246-8901
46
+ #{Indices::COMMANDER} put twitter @monica_x
47
+ #{Indices::COMMANDER} put skype.id 6363430539
48
+ #{Indices::COMMANDER} put birthday \"1st April 1978\"
49
+
50
+ also hilary's
51
+
52
+ #{Indices::COMMANDER} open contacts hilary
53
+ #{Indices::COMMANDER} put email hilary@whitehouse.gov
54
+
55
+ then save the changes to your book and logout."
56
+
57
+ #{Indices::COMMANDER} commit"
58
+ #{Indices::COMMANDER} logout"
59
+
60
+ UNOPENED_MESSAGE
61
+
62
+ end
63
+
64
+
65
+ end
66
+
67
+
68
+ end
File without changes
File without changes
File without changes
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/ruby
2
+
3
+ # Reopen the core ruby Hash class and add the below methods to it.
4
+ class Hash
5
+
6
+
7
+ # Recursively merge (deep merge) this {Hash} data structure with another
8
+ # given in the parameter.
9
+ #
10
+ # The core ruby {Hash.merge()} instance method only performs first level
11
+ # merges which is not ideal in order to intelligently merge a deep tree.
12
+ #
13
+ # <tt>Merge Behaviour</tt>
14
+ #
15
+ # Currently this behaviour works only for Hash data structures that have
16
+ # string keys (only) and either string or Hash values. It is interesting
17
+ # only when duplicate keys are encountered at the same level. If the
18
+ # duplicate key's value is
19
+ #
20
+ # 1. a String - the incoming value is rejected and logged
21
+ # 2. a Hash - the method is recalled to recursively merge
22
+ #
23
+ # @param merge_me [Hash] the incoming hash data structure to merge
24
+ def merge_recursively!( merge_me )
25
+
26
+ self.merge!( merge_me ) do | key, value_1, value_2 |
27
+
28
+ is_mergeable = value_1.kind_of?( Hash ) && value_2.kind_of?( Hash )
29
+ are_both_str = value_1.kind_of?( String ) && value_2.kind_of?( String )
30
+ not_the_same = are_both_str && ( value_1 != value_2 )
31
+
32
+ reject_message( key, value_1, value_2 ) if not_the_same
33
+ value_1.merge_recursively!( value_2 ) if is_mergeable
34
+ value_1
35
+
36
+ end
37
+
38
+ end
39
+
40
+
41
+ # Print a line to standard out stating that a same key (duplicate) map
42
+ # entry has been encountered and here is the one we are rejecting.
43
+ def reject_message( key, value_1, value_2 )
44
+ puts "Refused to allow { #{key} => #{value_2} } to overwrite { #{key} => #{value_1} }"
45
+ end
46
+
47
+
48
+ # This method adds (logging its own contents) behaviour to
49
+ # the standard library {Hash} class.
50
+ #
51
+ # @note This behaviour does not consider that SECRETS may be inside
52
+ # the key value maps - it logs itself without a care in the world.
53
+ # This functionality must be included if this behaviourr is used by
54
+ # any cryptography classes.
55
+ #
56
+ # The <tt>DEBUG</tt> log level is used for logging. To change this
57
+ # create a new parameterized method.
58
+ def log_contents
59
+
60
+ log.debug(x) { "# --- ----------------------------------------------" }
61
+ log.debug(x) { "# --- Map has [#{self.length}] key/value pairs." }
62
+ log.debug(x) { "# --- ----------------------------------------------" }
63
+
64
+ self.each do |the_key, the_value|
65
+
66
+ padded_key = sprintf '%-33s', the_key
67
+ log.debug(x) { "# --- #{padded_key} => #{the_value}" }
68
+
69
+ end
70
+
71
+ log.debug(x) { "# --- ----------------------------------------------" }
72
+
73
+ end
74
+
75
+
76
+ end
@@ -28,7 +28,7 @@ class String
28
28
  # @example
29
29
  # cipher_text = "Hello crypt world".encrypt_block_encode "ABC123XYZ"
30
30
  # original_txt = cipher_text.block_decode_decrypt "ABC123XYZ"
31
- # puts original_txt # "Hello crypt world"
31
+ # (print out) original_txt # "Hello crypt world"
32
32
  #
33
33
  # @param crypt_key [String]
34
34
  # a strong long encryption key that is used to encrypt this string before
@@ -47,7 +47,7 @@ class String
47
47
  # @example
48
48
  # cipher_text = "Hello crypt world".decrypt_block_encode "ABC123XYZ"
49
49
  # original_txt = cipher_text.block_decode_decrypt "ABC123XYZ"
50
- # puts original_txt # "Hello crypt world"
50
+ # (print out) original_txt # "Hello crypt world"
51
51
  #
52
52
  # @param crypt_key [String]
53
53
  # a strong long decryption key that is used to decrypt this string after
@@ -76,7 +76,7 @@ class String
76
76
  # @example
77
77
  # cipher_text = "Hello @:==:@ world".encrypt_url_encode "ABC123XYZ"
78
78
  # original_txt = cipher_text.url_decode_decrypt "ABC123XYZ"
79
- # puts original_txt # "Hello @:==:@ world"
79
+ # (print out) original_txt # "Hello @:==:@ world"
80
80
  #
81
81
  # @param crypt_key [String]
82
82
  # a strong long encryption key that is used to encrypt this string before
@@ -123,7 +123,7 @@ class String
123
123
  # @example
124
124
  # cipher_text = "Hello @:==:@ world".encrypt_url_encode "ABC123XYZ"
125
125
  # original_txt = cipher_text.url_decode_decrypt "ABC123XYZ"
126
- # puts original_txt # "Hello @:==:@ world"
126
+ # (print out) original_txt # "Hello @:==:@ world"
127
127
  #
128
128
  # @param crypt_key [String]
129
129
  # a strong long decryption key that is used to decrypt this string after
@@ -229,9 +229,9 @@ class String
229
229
  raise ArgumentError, "Begin delimiter is NIL or empty." if this_delimiter.nil? || this_delimiter.empty?
230
230
  raise ArgumentError, "End delimiter is NIL or empty." if that_delimiter.nil? || that_delimiter.empty?
231
231
 
232
- scanner_1 = StringScanner.new self
232
+ scanner_1 = ::StringScanner.new self
233
233
  scanner_1.scan_until /#{this_delimiter}/
234
- scanner_2 = StringScanner.new scanner_1.post_match
234
+ scanner_2 = ::StringScanner.new scanner_1.post_match
235
235
  scanner_2.scan_until /#{that_delimiter}/
236
236
 
237
237
  in_between_text = scanner_2.pre_match.strip
File without changes
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/ruby
2
+ # coding: utf-8
3
+
4
+
5
+ module SafeDb
6
+
7
+
8
+ # This class derives <b>non secret but unique identifiers</b> based on different
9
+ # combinations of the <b>application, shell and machine (compute element)</b>
10
+ # references.
11
+ #
12
+ # == Identifier Are Not Secrets
13
+ #
14
+ # <b>And their starting values are retrievable</b>
15
+ #
16
+ # Note that the principle and practise of <b>identifiers is not about keeping secrets</b>.
17
+ # An identifier can easily give up its starting value/s if and when brute force is
18
+ # applied. The properties of a good iidentifier (ID) are
19
+ #
20
+ # - non repeatability (also known as uniqueness)
21
+ # - non predictability (of the next identifier)
22
+ # - containing alphanumerics (for file/folder/url names)
23
+ # - human readable (hence hyphens and separators)
24
+ # - non offensive (no swear words popping out)
25
+ #
26
+ # == Story | Identifiers Speak Volumes
27
+ #
28
+ # I told a friend what the turnover of his company was and how many clients he had.
29
+ # He was shocked and wanted to know how I had gleened this information.
30
+ #
31
+ # The invoices he sent me (a year apart). Both his invoice IDs (identifiers) and his
32
+ # user IDs where integers that counted up. So I could determine how many new clients
33
+ # he had in the past year, how many clients he had when I got the invoice, and I
34
+ # determined the turnover by guesstimating the average invoice amount.
35
+ #
36
+ # Many successful website attacks are owed to a predictable customer ID or a counter
37
+ # type branch ID within the cookies.
38
+ #
39
+ # == Good Identifiers Need Volumes
40
+ #
41
+ # IDs are not secrets - but even so, a large number of properties are required
42
+ # to produce a high quality ID.
43
+ #
44
+ class Identifier
45
+
46
+ # The ergonomic list of 24 characters highly suited for use within
47
+ # human readable and likable identifier strings.
48
+ ERGONOMIC_LIST = [
49
+ "b", "c", "d", "e", "h", "k",
50
+ "m", "n", "r", "t", "v", "w",
51
+ "x", "z", "0", "1", "2", "3",
52
+ "4", "5", "6", "7", "8", "9"
53
+ ]
54
+
55
+
56
+ # The identity chunk length is set at four (4) which means each of the
57
+ # fabricated identifiers comprises of four character segments divided by
58
+ # hyphens. Only the <b>62 alpha-numerics ( a-z, A-Z and 0-9 )</b> will
59
+ # appear within identifiers - which maintains simplicity and provides an
60
+ # opportunity to re-iterate that <b>identifiers</b> are designed to be
61
+ # <b>unpredictable</b>, but <b>not secret</b>.
62
+ IDENTITY_CHUNK_LENGTH = 4
63
+
64
+
65
+ # A hyphen is the chosen character for dividing the identifier strings
66
+ # into chunks of four (4) as per the {IDENTITY_CHUNK_LENGTH} constant.
67
+ SEGMENT_CHAR = "-"
68
+
69
+ ID_TRI_CHUNK_LEN = IDENTITY_CHUNK_LENGTH * 3
70
+ ID_TRI_TOTAL_LEN = ID_TRI_CHUNK_LEN + 2
71
+
72
+
73
+ # Get an ergonomic identifier that is a <b>one to one mapping</b> for the
74
+ # parameter string. In as far as is possible, two different input strings
75
+ # should never produce the same output identifier, nor should one input
76
+ # string be ambiguously mapped to two output identifiers.
77
+ #
78
+ # This algorithm must be brute force tested to verify the above assertions.
79
+ #
80
+ # <tt>The 24 Ergonomic Characters</tt>
81
+ #
82
+ # The returned identifier is ergonomic in that its characters come from a
83
+ # pool of 24 of the most suitable ID characters - pleasant to see, easy to
84
+ # digest and simple to convey.
85
+ #
86
+ # <b>How to Derive the Ergonomic Identifier</b>
87
+ #
88
+ # We pass the parameter string through a SHA512 digest algorithm and truncate
89
+ # the final 2 binary digits because 510 is a multiple of six and perfect for
90
+ # the transformation to a Base64 string.
91
+ #
92
+ # The Base64 transform gives us 85 characters from which we remove any non
93
+ # alphanumerics. We repeat all the above again with the parameter reversed
94
+ # and append the two resultants together.
95
+ #
96
+ # This harvests roughly 160 characters from which we downcase and walk
97
+ # through picking out a selection of just 24 ergonomic characters.
98
+ #
99
+ # @param source [String]
100
+ # the source string whose characters we digest and filter to produce a
101
+ # high quality, pleasing ergonomic identifier
102
+ #
103
+ # Before processing any leading or trailing whitespace is removed from
104
+ # the input string.
105
+ #
106
+ # @param id_length [Numeric]
107
+ # the number of identifier characters to return. This parameter must be
108
+ # even and divisible by 3 in case it needs to be split (for readability)
109
+ # into two or three segments.
110
+ #
111
+ # There is a logical maximum above which it is foolish to venture. The
112
+ # max is about two-thirds of a sixth of a thousand characters which is
113
+ # slightly over 100.
114
+ #
115
+ # @return [String]
116
+ # An identifier that is guaranteed to be the same whenever the same
117
+ # input string is provided.
118
+ #
119
+ # This algorithms quality is predicated on the premise that two different
120
+ # input strings should never produce the same output, nor should one input
121
+ # string be ambiguously mapped to two output identifiers.
122
+ #
123
+ # The default behaviour is to split the output identifier into 2 segments
124
+ # separated by a hyphen.
125
+ def self.derive_ergonomic_identifier( source, id_length )
126
+
127
+ abort "The source string cannot be nil or empty." if source.nil?() or source.empty?()
128
+ abort "The source cannot consist only of whitespace." if source.strip().empty?()
129
+ abort "The ID length must not be less than 2." unless id_length > 1
130
+ abort "The ID length must be a multiple of 2." unless id_length % 2 == 0
131
+ abort "Prudent identifiers do not exceed 80 characters." unless id_length < 80
132
+
133
+ digested_bits = Key.from_binary( Digest::SHA512.digest( source.strip() ) ).to_s +
134
+ Key.from_binary( Digest::SHA512.digest( source.strip().reverse() ) ).to_s
135
+
136
+ digest_string = Key64.from_bits( digested_bits[ 0 .. ( 1020 - 1 ) ] ).to_alphanumeric
137
+ filtered_digest = ergonomic_filter( digest_string, id_length )
138
+ return filtered_digest.insert( id_length/2, SEGMENT_CHAR )
139
+
140
+ end
141
+
142
+
143
+ # This ergonomic filter produces a pleasing readable identifier that is
144
+ # down cased and does not contain characters like o, l, s, a, i, or u.
145
+ #
146
+ # Swear words can pop up so most vowels are removed to save your blushes.
147
+ #
148
+ # @param raw_digest [String]
149
+ # the source string whose characters we filter in order to produce a
150
+ # high quality, pleasing ergonomic identifier
151
+ #
152
+ # @param id_length [Numeric]
153
+ # the number of identifier characters to return. This parameter must be
154
+ # even and divisible by 3 in case it needs to be split (for readability)
155
+ # into two or three segments.
156
+ #
157
+ # @return [String]
158
+ # The filtered identifier containing only the 24 desirable characters.
159
+ #
160
+ def self.ergonomic_filter( raw_digest, id_length )
161
+
162
+ id_characters = ""
163
+ raw_digest.downcase().each_char() do | digest_char |
164
+ id_characters.concat( digest_char ) if ERGONOMIC_LIST.include?( digest_char )
165
+ break if id_characters.length() == id_length
166
+ end
167
+
168
+ return id_characters
169
+
170
+ end
171
+
172
+ =begin
173
+ require_relative "../keys/key.64"
174
+ require_relative "../keys/key"
175
+ input1 = "apollo@akora"
176
+ output1 = Identifier.derive_ergonomic_identifier( input1, 90 )
177
+ puts "Input #{input1} produced output #{output1}"
178
+ =end
179
+
180
+ # This method produces a soft random identifier by grabbing a secure
181
+ # random binary string, transforming it to base64, removing any and all
182
+ # hyphens and underscores, downcasing the result and finally truncating
183
+ # it to produce a random identifier of the desired length.
184
+ #
185
+ # Do not use this method to produce passwords or secrets because it
186
+ # provides IDs from a pool of only 36 characters with a fixed length so
187
+ # can be brute forced with ease. Only use it for producing identifiers.
188
+ #
189
+ # @param id_length [Number]
190
+ # the length of the returned identifier. This value should not exceed
191
+ # 50 characters as the source pool is a good size - but is by no means
192
+ # infinitely long.
193
+ def self.get_random_identifier( id_length )
194
+
195
+ require 'securerandom'
196
+ random_ref = SecureRandom.urlsafe_base64( id_length ).delete("-_").downcase
197
+ return random_ref[ 0 .. ( id_length - 1 ) ]
198
+
199
+ end
200
+
201
+
202
+ # The branch ID generated here is a derivative of the 150 character
203
+ # shell token.
204
+ #
205
+ # The algorithm for deriving the branch ID is as follows.
206
+ #
207
+ # - convert the 150 characters to an alphanumeric string
208
+ # - convert the result to a bit string and then to a key
209
+ # - put the key's binary form through a 384 bit digest
210
+ # - convert the digest's output to 64 YACHT64 characters
211
+ # - remove the (on average 2) non-alphanumeric characters
212
+ # - cherry pick a spread out 12 characters from the pool
213
+ # - hiphenate the character positions five (5) and ten (10)
214
+ # - ensure the length of the resultant ID is fourteen (14)
215
+ #
216
+ # The resulting branch id will look something like this
217
+ #
218
+ # g3sf-pab5-9xvd
219
+ #
220
+ # @param shell_token [String]
221
+ # a triply segmented (and one liner) text token
222
+ #
223
+ # @return [String]
224
+ # a 14 character string that cannot feasibly be repeated
225
+ # within the keyspace of even a gigantic organisation.
226
+ #
227
+ # This method guarantees that the branch id will always be the same when
228
+ # called by commands within the same shell in the same machine.
229
+ def self.derive_branch_id( shell_token )
230
+
231
+ assert_shell_token_size( shell_token )
232
+ random_length_id_key = Key.from_char64( shell_token.to_alphanumeric )
233
+ a_384_bit_key = random_length_id_key.to_384_bit_key()
234
+ a_64_char_str = a_384_bit_key.to_char64()
235
+ base_64_chars = a_64_char_str.to_alphanumeric
236
+
237
+ id_chars_pool = cherry_picker( ID_TRI_CHUNK_LEN, base_64_chars )
238
+ id_hyphen_one = id_chars_pool.insert( IDENTITY_CHUNK_LENGTH, SEGMENT_CHAR )
239
+ id_characters = id_hyphen_one.insert( ( IDENTITY_CHUNK_LENGTH * 2 + 1 ), SEGMENT_CHAR )
240
+
241
+ err_msg = "Shell ID needs #{ID_TRI_TOTAL_LEN} not #{id_characters.length} characters."
242
+ raise RuntimeError, err_msg unless id_characters.length == ID_TRI_TOTAL_LEN
243
+
244
+ return id_characters.downcase
245
+
246
+ end
247
+
248
+
249
+ # Cherry pick a given number of characters from the character pool
250
+ # so that a good spread is achieved. This picker is the anti-pattern
251
+ # of just axing the first 5 characters from a 100 character string
252
+ # essentially wasting over 90% of the available entropy.
253
+ #
254
+ # This is the <b>algorithem to cherry pick</b> a spread of characters
255
+ # from the pool in the second parameter.
256
+ #
257
+ # - if the character pool length is a multiple of num_chars all is good otherwise
258
+ # - constrict to the <b>highest multiple of the pick size below</b> the pool length
259
+ # - divide that number by num_chars to get the first offset and character spacing
260
+ # - if spacing is 3, the first character is the 3rd, the second the 6th and so on
261
+ # - then return the cherry picked characters
262
+ #
263
+ # @param pick_size [FixNum] the number of characters to cherry pick
264
+ # @param char_pool [String] a pool of characters to cherry pick from
265
+ # @return [String]
266
+ # a string whose length is the one indicated by the first parameter
267
+ # and whose characters contain a predictable, repeatable spread from
268
+ # the character pool parameter
269
+ def self.cherry_picker( pick_size, char_pool )
270
+
271
+ hmb_limit = highest_multiple_below( pick_size, char_pool.length )
272
+ jump_size = hmb_limit / pick_size
273
+ read_point = jump_size
274
+ picked_chars = ""
275
+ loop do
276
+ picked_chars += char_pool[ read_point - 1 ]
277
+ read_point += jump_size
278
+ break if read_point > hmb_limit
279
+ end
280
+
281
+ err_msg = "Expected cherry pick size to be #{pick_size} but it was #{picked_chars.length}."
282
+ raise RuntimeError, err_msg unless picked_chars.length == pick_size
283
+
284
+ return picked_chars
285
+
286
+ end
287
+
288
+
289
+ # Affectionately known as <b>a hmb</b>, this method returns the
290
+ # <b>highest multiple</b> of the first parameter that is below
291
+ # <b>(either less than or equal to)</b> the second parameter.
292
+ #
293
+ # - -------- - ------- - ----------------- -
294
+ # | Small | Big | Highest Multiple |
295
+ # | Number | Number | Below Big Num |
296
+ # | -------- - ------- - ----------------- |
297
+ # | 5 | 25 | 25 |
298
+ # | 3 | 20 | 18 |
299
+ # | 8 | 63 | 56 |
300
+ # | 1 | 1 | 1 |
301
+ # | 26 | 28 | 26 |
302
+ # | 1 | 7 | 7 |
303
+ # | 16 | 16 | 16 |
304
+ # | -------- - ------- - ----------------- |
305
+ # | 10 | 8 | ERROR |
306
+ # | -4 | 17 | ERROR |
307
+ # | 4 | -17 | ERROR |
308
+ # | 0 | 32 | ERROR |
309
+ # | 29 | 0 | ERROR |
310
+ # | -4 | 0 | ERROR |
311
+ # | -------- - ------- - ----------------- |
312
+ # - -------- - ------- - ----------------- -
313
+ #
314
+ # Zeroes and negative numbers cannot be entertained, nor can the
315
+ # small number be larger than the big one.
316
+ #
317
+ # @param small_num [FixNum]
318
+ # the highest multiple of this number below the one in the
319
+ # next parameter is what will be returned.
320
+ #
321
+ # @param big_num [FixNum]
322
+ # returns either this number or the nearest below it that is
323
+ # a multiple of the number in the first parameter.
324
+ #
325
+ # @raise [ArgumentError]
326
+ # if the first parameter is greater than the second
327
+ # if either or both parameters are zero or negative
328
+ def self.highest_multiple_below small_num, big_num
329
+
330
+ arg_issue = (small_num > big_num) || small_num < 1 || big_num < 1
331
+ err_msg = "Invalid args #{small_num} and #{big_num} to HMB function."
332
+ raise ArgumentError, err_msg if arg_issue
333
+
334
+ for index in 0 .. ( big_num - 1 )
335
+ invex = big_num - index # an [invex] is an inverted index
336
+ return invex if invex % small_num == 0
337
+ end
338
+
339
+ raise ArgumentError, "Could not find a multiple of #{small_num} lower than #{big_num}"
340
+
341
+ end
342
+
343
+
344
+ private
345
+
346
+
347
+ def self.assert_shell_token_size shell_token
348
+ err_msg = "shell token has #{shell_token.length} and not #{KeyDerivation::SHELL_TOKEN_SIZE} chars."
349
+ raise RuntimeError, err_msg unless shell_token.length == KeyDerivation::SHELL_TOKEN_SIZE
350
+ end
351
+
352
+
353
+ end
354
+
355
+
356
+ end