human_readable 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5307eeacfd4961a43325a641b37da77388aedcb419f1c2bd0ae1021eec63417b
4
+ data.tar.gz: 44414551a3e4333ab02beb913c69bf8afddce406b2e1701c2f7d79e1150a464d
5
+ SHA512:
6
+ metadata.gz: 1662e91b1671caf3ac01da210016eb0544d6b82a9eec0f93279eaa9460472bea09fa8ba078d5204551dbb948c81c0a9f874a62d4001c41cfe9cd8c7c2dda38a0
7
+ data.tar.gz: b728158418c8b4ea875dc80cccc4055107fbf48014821c325b8e76dfbe6c63f38394d57544c8f13909c376e47f45b19e812fc3ca100994ca2a0a42a2d12eaef9
@@ -0,0 +1,5 @@
1
+ Release History
2
+ ===============
3
+
4
+ # 0.8.0
5
+ * Initial release
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Mack Earnhardt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,67 @@
1
+ # HumanReadable
2
+
3
+ Human readable random tokens with limited ambiguous characters.
4
+
5
+ Focus is readability in poor conditions or from potentially damaged printed documents rather than cryptographic uses.
6
+ Despite this focus, SecureRandom is used to help avoid collisions.
7
+
8
+ Inspired by Douglas Crockford's [Base 32](https://www.crockford.com/base32.html), but attempts to correct mistakes by substituting the most likely misread.
9
+ To make substitution safer, the token includes a check character generated using the [Luhn mod N algorithm](https://en.wikipedia.org/wiki/Luhn_mod_N_algorithm).
10
+ Default character set is all caps based on this published study on [text legibility](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2016788/), which matches Crockford as well.
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'human_readable'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ $ bundle install
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install human_readable
27
+
28
+ ## Usage
29
+
30
+ For 10 characters of the default character set, use `HumanReadable.generate`.
31
+ For other lengths (2..x), use `HumanReadable.generate(output_size: 50)`.
32
+
33
+ ## Configuration
34
+
35
+ * Change available characters and substitution by manipulating `substitution_hash`
36
+ * To include non-default characters, add a self-reference to the hash
37
+ * Inspect available characters using `HumanReadable.charset`
38
+ * For convenience, numbers and symbols are allowed in the hash and are translated to characters during usage
39
+
40
+ **CAUTION:** Changing `substitution_hash` keys alters the check character, invalidating previous tokens.
41
+
42
+
43
+ HumanReadable.configure do |c|
44
+ # Default: substitution_hash = { I: 1, L: 1, O: 0, U: :V }
45
+
46
+ # Modifications
47
+ c.substitution_hash[:B] = 8
48
+ c.substitution_hash[:U] = nil
49
+ c.substitution_hash['$'] = '$'
50
+ # or equivalently
51
+ c.substitution_hash = { I: 1, L: 1, O: 0, U: nil, B: 8, '$' => '$'}
52
+ end
53
+
54
+ ## Development
55
+
56
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
57
+
58
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
59
+
60
+ ## Contributing
61
+
62
+ Bug reports and pull requests are welcome on GitHub at https://github.com/MacksMind/human_readable.
63
+
64
+
65
+ ## License
66
+
67
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2020 Mack Earnhardt
4
+
5
+ require 'human_readable/version'
6
+ require 'ostruct'
7
+ require 'securerandom'
8
+
9
+ # Human readable random tokens with no ambiguous characters
10
+ module HumanReadable
11
+ # +#generate+ output_size must be >= 2 due to check character
12
+ class MinSizeTwo < StandardError; end
13
+
14
+ class << self
15
+ # Yields block for configuration
16
+ #
17
+ # HumanReadable.configure do |c|
18
+ # c.substitution_hash[:B] = 8
19
+ # c.substitution_hash[:U] = nil
20
+ # c.substitution_hash['$'] = '$'
21
+ # # or equivalently
22
+ # c.substitution_hash = { I: 1, L: 1, O: 0, U: nil, B: 8, '$' => '$'}
23
+ # end
24
+ #
25
+ # DEFAULT:
26
+ # substitution_hash: { I: 1, L: 1, O: 0, U: :V }
27
+ #
28
+ # Specified keys won't be used during generation, and values will be substituted during
29
+ # validation, increasing the likelihood that a misread character can be restored. Extend
30
+ # or replace the substitutions to use a different character set. For convenience, numbers
31
+ # and symbols are allowed in the hash and are translated to characters during usage.
32
+ # Alter as needed per examples below.
33
+ #
34
+ # *CAUTION:* Changing substitution_hash keys alters the check character, invalidating previous tokens.
35
+ def configure
36
+ yield(configuration)
37
+ end
38
+
39
+ # Generates a random token of the requested size
40
+ #
41
+ # Minimum size is 2 since the last character is a check character
42
+ def generate(output_size: 10)
43
+ raise(MinSizeTwo) if output_size < 2
44
+
45
+ (token = generate_random(output_size - 1)) + check_character(token)
46
+ end
47
+
48
+ # Clean and validate a candidate token
49
+ #
50
+ # * Upcases
51
+ # * Applies substitutions
52
+ # * Remove characters not in available character set
53
+ # * Validates the check character
54
+ #
55
+ # Return value: Valid token or nil
56
+ def valid_token?(input)
57
+ return unless input.is_a?(String)
58
+
59
+ codepoints = input.upcase.tr(trans_from, trans_to).chars.map! { |c| charset.index(c) }
60
+ codepoints.compact!
61
+
62
+ return if codepoints.size < 2
63
+
64
+ array =
65
+ codepoints.reverse.each_with_index.map do |codepoint, i|
66
+ codepoint *= 2 if i.odd?
67
+ codepoint / charset_size + codepoint % charset_size
68
+ end
69
+
70
+ codepoints.map { |codepoint| charset[codepoint] }.join if (array.sum % charset_size).zero?
71
+ end
72
+
73
+ # Characters available for token generation
74
+ #
75
+ # Manipulate by configuring +substitution_hash+
76
+ #
77
+ # DEFAULT: All number and uppercase letters except for ILOU
78
+ def charset
79
+ @charset ||= (('0'..'9').to_a + ('A'..'Z').to_a - trans_from.chars + trans_to.chars - nil_substitutions).uniq
80
+ end
81
+
82
+ private
83
+
84
+ def configuration
85
+ @configuration ||= OpenStruct.new(
86
+ substitution_hash: { I: 1, L: 1, O: 0, U: :V }
87
+ )
88
+ end
89
+
90
+ # Generates a random string of the requested length from the charset
91
+ #
92
+ # We could use one of the below routines in +#generate+, but the first
93
+ # increases the chances of token collisions and the second is too slow.
94
+ #
95
+ # Array.new(random_size) { charset.sample }
96
+ # # or
97
+ # Array.new(random_size) { charset.sample(random: SecureRandom) }
98
+ #
99
+ # Instead we attempt to optimize the number of bytes generated with each
100
+ # call to SecureRandom.
101
+ def generate_random(random_size)
102
+ return '' unless random_size.positive?
103
+
104
+ codepoints = []
105
+
106
+ while codepoints.size < random_size
107
+ bytes_needed = ((random_size - codepoints.size) * byte_multiplier).ceil
108
+
109
+ codepoints +=
110
+ begin
111
+ array =
112
+ SecureRandom
113
+ .random_bytes(bytes_needed)
114
+ .unpack1('B*')
115
+ .scan(scan_regexp)
116
+ .map! { |bin_string| bin_string.to_i(2) }
117
+ array.select { |codepoint| codepoint < charset_size }
118
+ end
119
+ end
120
+
121
+ codepoints[0, random_size].map { |codepoint| charset[codepoint] }.join
122
+ end
123
+
124
+ # Compute check character using Luhn mod N algorithm
125
+ #
126
+ # *CAUTION:* Changing substitution_hash keys alters the output
127
+ def check_character(input)
128
+ array =
129
+ input.chars.reverse.each_with_index.map do |c, i|
130
+ d = charset.index(c)
131
+ d *= 2 if i.even?
132
+ d / charset_size + d % charset_size
133
+ end
134
+
135
+ mod = (charset_size - array.sum % charset_size) % charset_size
136
+
137
+ charset[mod]
138
+ end
139
+
140
+ def char_bits
141
+ @char_bits ||= (charset_size - 1).to_s(2).size
142
+ end
143
+
144
+ def scan_regexp
145
+ @scan_regexp ||= /.{#{char_bits}}/
146
+ end
147
+
148
+ def byte_multiplier
149
+ @byte_multiplier ||=
150
+ begin
151
+ bit_multiplier = char_bits / 8.0
152
+ # Then extra 1.1 helps performance due to randomness of misses
153
+ miss_percentage = 2**char_bits * 1.0 / charset_size * 1.1
154
+ bit_multiplier * miss_percentage
155
+ end
156
+ end
157
+
158
+ def charset_size
159
+ @charset_size ||= charset.size
160
+ end
161
+
162
+ def nil_substitutions
163
+ @nil_substitutions ||=
164
+ begin
165
+ array = configuration.substitution_hash.each.map { |k, v| k if v.nil? }
166
+ array.compact!
167
+ array.map!(&:to_s)
168
+ array.map!(&:upcase)
169
+ end
170
+ end
171
+
172
+ def trans_from
173
+ @trans_from ||=
174
+ begin
175
+ array = configuration.substitution_hash.each.map { |k, v| k unless v.nil? }
176
+ array.compact!
177
+ array.map!(&:to_s)
178
+ array.map!(&:upcase)
179
+ array.join
180
+ end
181
+ end
182
+
183
+ def trans_to
184
+ @trans_to ||=
185
+ begin
186
+ array = configuration.substitution_hash.values
187
+ array.compact!
188
+ array.map!(&:to_s)
189
+ array.map!(&:upcase)
190
+ array.join
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2020 Mack Earnhardt
4
+
5
+ module HumanReadable
6
+ # Gem version
7
+ VERSION = '0.8.0'
8
+ public_constant :VERSION
9
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: human_readable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ platform: ruby
6
+ authors:
7
+ - Mack Earnhardt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-08-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ Human readable random tokens with no ambiguous characters
15
+
16
+ Tranlates invalid characters to their most likely original value
17
+ and validates using a checksum.
18
+ email:
19
+ - mack@agilereasoning.com
20
+ executables: []
21
+ extensions: []
22
+ extra_rdoc_files: []
23
+ files:
24
+ - "./CHANGELOG.md"
25
+ - "./LICENSE.txt"
26
+ - "./README.md"
27
+ - lib/human_readable.rb
28
+ - lib/human_readable/version.rb
29
+ homepage: https://github.com/MacksMind/human_readable
30
+ licenses:
31
+ - MIT
32
+ metadata:
33
+ homepage_uri: https://github.com/MacksMind/human_readable
34
+ source_code_uri: https://github.com/MacksMind/human_readable
35
+ changelog_uri: https://github.com/MacksMind/human_readable/blob/master/CHANGELOG.md
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: 2.4.1
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubygems_version: 3.1.2
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: Human readable random tokens with no ambiguous characters
55
+ test_files: []