human_readable 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|