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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +67 -0
- data/lib/human_readable.rb +194 -0
- data/lib/human_readable/version.rb +9 -0
- metadata +55 -0
checksums.yaml
ADDED
@@ -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
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
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: []
|