obfuskey 0.1.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/LICENSE +21 -0
- data/README.md +59 -0
- data/lib/obfuskey/alphabets.rb +25 -0
- data/lib/obfuskey/errors.rb +11 -0
- data/lib/obfuskey/math.rb +91 -0
- data/lib/obfuskey/obfusbit.rb +130 -0
- data/lib/obfuskey/obfusbit_schema.rb +112 -0
- data/lib/obfuskey/obfuskey.rb +85 -0
- data/lib/obfuskey/utils.rb +46 -0
- data/lib/obfuskey/version.rb +3 -0
- data/lib/obfuskey.rb +10 -0
- metadata +83 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9bc5f45d8820d179791885eba40fc30d7cf1145e1f62194f3354fc9bac512f90
|
|
4
|
+
data.tar.gz: ea7b2365b2c97baec0f8368a087ea461bfd47dd0e6250d94e1278c70a93b7903
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 174c3b7d704769bea2c6d10ee3f25c557c395932bcc3d66f5d6a0a3c0b139b429c81856810d11ce29aa17be6f3f361b6a2258fec0394b9123e90a0796f813bb4
|
|
7
|
+
data.tar.gz: 221acb4d5cf61847afb3874ab4dd81a743c08f4e133fa48b6264d4bcbdca5bbd08d64159fa7f1f8e05739c3934f809b32219630c91632a076cdb8d194592e517
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Nathan Lucas
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# obfuskey-rb
|
|
2
|
+
|
|
3
|
+
A Ruby port of [Obfuskey](https://github.com/bnlucas/obfuskey). Generates deterministic, reversible, fixed-length keys from integer values using a custom alphabet. Cross-compatible with the Python, JavaScript, and Rust implementations — the same `(alphabet, key_length, value)` triple produces the same key in every language.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Gemfile
|
|
9
|
+
gem "obfuskey"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
require "obfuskey"
|
|
16
|
+
|
|
17
|
+
obf = Obfuskey.new(Obfuskey::Alphabets::BASE62)
|
|
18
|
+
key = obf.get_key(12345) # => "d2Aasl"
|
|
19
|
+
obf.get_value(key) # => 12345
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Custom key length or multiplier
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
Obfuskey.new(Obfuskey::Alphabets::BASE62, key_length: 8)
|
|
26
|
+
Obfuskey.new(Obfuskey::Alphabets::BASE62, multiplier: 123) # odd integer
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Obfusbit (bit-packed structured keys)
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
schema = [
|
|
33
|
+
{ name: "id", bits: 10 },
|
|
34
|
+
{ name: "type", bits: 2 },
|
|
35
|
+
{ name: "flag", bits: 1 }
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
key_maker = Obfuskey.new(Obfuskey::Alphabets::BASE62, key_length: 3)
|
|
39
|
+
ob = Obfusbit.new(schema, obfuskey: key_maker)
|
|
40
|
+
|
|
41
|
+
encoded = ob.pack({ "id" => 100, "type" => 2, "flag" => 1 }, obfuscate: true)
|
|
42
|
+
ob.unpack(encoded, obfuscated: true)
|
|
43
|
+
# => { "id" => 100, "type" => 2, "flag" => 1 }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Alphabets
|
|
47
|
+
|
|
48
|
+
`Obfuskey::Alphabets` provides `BASE16`, `BASE32`, `BASE36`, `BASE52`, `BASE56`, `BASE58`, `BASE62`, `BASE64`, `BASE94`, `CROCKFORD_BASE32`, `ZBASE32`, `BASE64_URL_SAFE`.
|
|
49
|
+
|
|
50
|
+
## Tests
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
bundle install
|
|
54
|
+
bundle exec rspec
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class Obfuskey
|
|
2
|
+
module Alphabets
|
|
3
|
+
BASE16 = "0123456789ABCDEF".freeze
|
|
4
|
+
BASE32 = "234567ABCDEFGHIJKLMNOPQRSTUVWXYZ".freeze
|
|
5
|
+
BASE36 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".freeze
|
|
6
|
+
BASE52 = "0123456789BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz".freeze
|
|
7
|
+
BASE56 = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz".freeze
|
|
8
|
+
BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".freeze
|
|
9
|
+
BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".freeze
|
|
10
|
+
BASE64 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/".freeze
|
|
11
|
+
BASE94 = (
|
|
12
|
+
'!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ' \
|
|
13
|
+
'[\]^_`abcdefghijklmnopqrstuvwxyz{|}~'
|
|
14
|
+
).freeze
|
|
15
|
+
|
|
16
|
+
CROCKFORD_BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".freeze
|
|
17
|
+
ZBASE32 = "ybndrfg8ejkmcpqxot1uwisza345h769".freeze
|
|
18
|
+
BASE64_URL_SAFE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_".freeze
|
|
19
|
+
|
|
20
|
+
ALL = [
|
|
21
|
+
BASE16, BASE32, BASE36, BASE52, BASE56, BASE58,
|
|
22
|
+
BASE62, BASE64, BASE94, CROCKFORD_BASE32, ZBASE32, BASE64_URL_SAFE
|
|
23
|
+
].freeze
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class Obfuskey
|
|
2
|
+
class Error < StandardError; end
|
|
3
|
+
class BitOverflowError < Error; end
|
|
4
|
+
class DuplicateError < Error; end
|
|
5
|
+
class KeyLengthError < Error; end
|
|
6
|
+
class MaximumValueError < Error; end
|
|
7
|
+
class MultiplierError < Error; end
|
|
8
|
+
class NegativeValueError < Error; end
|
|
9
|
+
class SchemaValidationError < Error; end
|
|
10
|
+
class UnknownKeyError < Error; end
|
|
11
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
class Obfuskey
|
|
2
|
+
module Math
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def factor(n)
|
|
6
|
+
s = 0
|
|
7
|
+
d = n - 1
|
|
8
|
+
while d.even?
|
|
9
|
+
s += 1
|
|
10
|
+
d /= 2
|
|
11
|
+
end
|
|
12
|
+
[s, d]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def trial_division(n)
|
|
16
|
+
return false if n <= 1
|
|
17
|
+
return true if n == 2
|
|
18
|
+
return false if n.even?
|
|
19
|
+
|
|
20
|
+
i = 3
|
|
21
|
+
limit = Integer.sqrt(n)
|
|
22
|
+
while i <= limit
|
|
23
|
+
return false if (n % i).zero?
|
|
24
|
+
i += 2
|
|
25
|
+
end
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def strong_pseudoprime(n, base = 2)
|
|
30
|
+
return false if n.even?
|
|
31
|
+
return false if n == 1
|
|
32
|
+
|
|
33
|
+
s, d = factor(n)
|
|
34
|
+
x = base.pow(d, n)
|
|
35
|
+
return true if x == 1 || x == n - 1
|
|
36
|
+
|
|
37
|
+
(s - 1).times do
|
|
38
|
+
x = x.pow(2, n)
|
|
39
|
+
return true if x == n - 1
|
|
40
|
+
return false if x == 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def small_strong_pseudoprime(n)
|
|
47
|
+
[2, 13, 23, 1_662_803].all? { |base| strong_pseudoprime(n, base) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def prime?(n)
|
|
51
|
+
return true if n == 2
|
|
52
|
+
return false if n < 2 || n.even?
|
|
53
|
+
return [3, 5, 7, 11, 13, 17].include?(n) if n.gcd(510_510) > 1
|
|
54
|
+
return trial_division(n) if n < 2_000_000
|
|
55
|
+
|
|
56
|
+
small_strong_pseudoprime(n)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def next_prime(n)
|
|
60
|
+
if n.bit_length > 512
|
|
61
|
+
raise MaximumValueError,
|
|
62
|
+
"For integers larger than 512-bit, a more advanced prime generator is required."
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
return 2 if n < 2
|
|
66
|
+
return [3, 5, 5][n - 2] if n < 5
|
|
67
|
+
|
|
68
|
+
gap = [
|
|
69
|
+
1, 6, 5, 4, 3, 2, 1, 4, 3, 2, 1, 2, 1, 4, 3,
|
|
70
|
+
2, 1, 2, 1, 4, 3, 2, 1, 6, 5, 4, 3, 2, 1, 2
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
n += 1 + (n & 1)
|
|
74
|
+
n += gap[n % 30] if (n % 3).zero? || (n % 5).zero?
|
|
75
|
+
n += gap[n % 30] until prime?(n)
|
|
76
|
+
n
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def mod_inv(base, mod)
|
|
80
|
+
g, x, _ = extended_gcd(base % mod, mod)
|
|
81
|
+
raise ArgumentError, "base and mod are not coprime" unless g == 1
|
|
82
|
+
x % mod
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def extended_gcd(a, b)
|
|
86
|
+
return [a, 1, 0] if b.zero?
|
|
87
|
+
g, x1, y1 = extended_gcd(b, a % b)
|
|
88
|
+
[g, y1, x1 - (a / b) * y1]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
class Obfusbit
|
|
2
|
+
Error = Obfuskey::Error
|
|
3
|
+
MaximumValueError = Obfuskey::MaximumValueError
|
|
4
|
+
SchemaValidationError = Obfuskey::SchemaValidationError
|
|
5
|
+
BitOverflowError = Obfuskey::BitOverflowError
|
|
6
|
+
|
|
7
|
+
attr_reader :obfuskey
|
|
8
|
+
|
|
9
|
+
def initialize(schema, obfuskey: nil)
|
|
10
|
+
@schema = schema.is_a?(Obfuskey::ObfusbitSchema) ? schema : Obfuskey::ObfusbitSchema.new(schema)
|
|
11
|
+
@obfuskey = obfuskey
|
|
12
|
+
@total_bits = @schema.total_bits
|
|
13
|
+
@max_bits = @schema.max_bits
|
|
14
|
+
|
|
15
|
+
if @obfuskey && @max_bits > @obfuskey.maximum_value
|
|
16
|
+
raise MaximumValueError,
|
|
17
|
+
"The provided schema requires a maximum packed integer value of #{@max_bits} " \
|
|
18
|
+
"(which needs #{@total_bits} bits to represent), but the provided Obfuskey instance " \
|
|
19
|
+
"can only handle up to a maximum value of #{@obfuskey.maximum_value} " \
|
|
20
|
+
"(which covers #{@obfuskey.maximum_value.bit_length} bits)."
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def total_bits
|
|
25
|
+
@total_bits
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def max_bits
|
|
29
|
+
@max_bits
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def schema
|
|
33
|
+
@schema.definition
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def pack(values, obfuscate: false)
|
|
37
|
+
@schema.validate_values!(values)
|
|
38
|
+
|
|
39
|
+
normalized = values.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
|
|
40
|
+
packed_int = 0
|
|
41
|
+
|
|
42
|
+
@schema.field_info.each do |name, info|
|
|
43
|
+
packed_int |= normalized[name] << info[:shift]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if obfuscate
|
|
47
|
+
raise ArgumentError, "An Obfuskey instance was not provided during initialization." unless @obfuskey
|
|
48
|
+
return @obfuskey.get_key(packed_int)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
packed_int
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def unpack(packed_data, obfuscated: false)
|
|
55
|
+
if obfuscated
|
|
56
|
+
raise ArgumentError, "An Obfuskey instance was not provided during initialization." unless @obfuskey
|
|
57
|
+
raise TypeError, "packed_data must be a String when obfuscated is true." unless packed_data.is_a?(String)
|
|
58
|
+
packed_int = @obfuskey.get_value(packed_data)
|
|
59
|
+
else
|
|
60
|
+
raise TypeError, "packed_data must be an Integer when obfuscated is false." unless packed_data.is_a?(Integer)
|
|
61
|
+
packed_int = packed_data
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
result = {}
|
|
65
|
+
@schema.field_info.each do |name, info|
|
|
66
|
+
mask = (1 << info[:bits]) - 1
|
|
67
|
+
result[name] = (packed_int >> info[:shift]) & mask
|
|
68
|
+
end
|
|
69
|
+
result
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def pack_bytes(values, byteorder: :big)
|
|
73
|
+
packed_int = pack(values, obfuscate: false)
|
|
74
|
+
int_to_bytes(packed_int, byteorder)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def unpack_bytes(byte_data, byteorder: :big)
|
|
78
|
+
packed_int = bytes_to_int(byte_data, byteorder)
|
|
79
|
+
unpack(packed_int, obfuscated: false)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def to_s
|
|
83
|
+
key_repr = @obfuskey ? "obfuskey=#{@obfuskey}" : "no obfuskey"
|
|
84
|
+
"Obfusbit(schema=#{@schema}, #{key_repr})"
|
|
85
|
+
end
|
|
86
|
+
alias inspect to_s
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def required_byte_length
|
|
91
|
+
(@total_bits + 7) / 8
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def int_to_bytes(packed_int, byteorder)
|
|
95
|
+
unless packed_int >= 0 && packed_int <= @max_bits
|
|
96
|
+
raise ArgumentError,
|
|
97
|
+
"Packed integer #{packed_int} is out of range (0 to #{@max_bits}) " \
|
|
98
|
+
"for the schema's total bit capacity."
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
byte_length = required_byte_length
|
|
102
|
+
bytes = Array.new(byte_length)
|
|
103
|
+
value = packed_int
|
|
104
|
+
(byte_length - 1).downto(0) do |i|
|
|
105
|
+
bytes[i] = value & 0xff
|
|
106
|
+
value >>= 8
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
bytes = bytes.reverse if byteorder.to_sym == :little
|
|
110
|
+
bytes.pack("C*")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def bytes_to_int(byte_data, byteorder)
|
|
114
|
+
raise TypeError, "byte_data must be a String." unless byte_data.is_a?(String)
|
|
115
|
+
|
|
116
|
+
expected = required_byte_length
|
|
117
|
+
if byte_data.bytesize != expected
|
|
118
|
+
raise ArgumentError,
|
|
119
|
+
"Byte data length (#{byte_data.bytesize}) does not match expected length " \
|
|
120
|
+
"for this schema (#{expected} bytes based on #{@total_bits} bits)."
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
bytes = byte_data.bytes
|
|
124
|
+
bytes = bytes.reverse if byteorder.to_sym == :little
|
|
125
|
+
|
|
126
|
+
result = 0
|
|
127
|
+
bytes.each { |b| result = (result << 8) | b }
|
|
128
|
+
result
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
class Obfuskey
|
|
2
|
+
class ObfusbitSchema
|
|
3
|
+
attr_reader :definition, :total_bits, :max_bits, :field_names
|
|
4
|
+
|
|
5
|
+
def initialize(raw_schema)
|
|
6
|
+
validate_schema!(raw_schema)
|
|
7
|
+
|
|
8
|
+
@definition = raw_schema
|
|
9
|
+
@total_bits = raw_schema.sum { |item| item[:bits] || item["bits"] }
|
|
10
|
+
@max_bits = (1 << @total_bits) - 1
|
|
11
|
+
@field_info = calculate_field_info(raw_schema)
|
|
12
|
+
@field_names = raw_schema.map { |item| (item[:name] || item["name"]).to_s }.to_set
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def field_info
|
|
16
|
+
# Return a deep copy so callers can't mutate internal state.
|
|
17
|
+
@field_info.each_with_object({}) { |(k, v), h| h[k] = v.dup }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get_field_info(name)
|
|
21
|
+
info = @field_info[name.to_s]
|
|
22
|
+
raise ArgumentError, "Field '#{name}' not found in schema." unless info
|
|
23
|
+
info
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def validate_values!(values)
|
|
27
|
+
input_names = values.keys.map(&:to_s).to_set
|
|
28
|
+
|
|
29
|
+
missing = @field_names - input_names
|
|
30
|
+
unless missing.empty?
|
|
31
|
+
raise ArgumentError,
|
|
32
|
+
"Required values for the following fields are missing: #{missing.sort.join(', ')}."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
extra = input_names - @field_names
|
|
36
|
+
unless extra.empty?
|
|
37
|
+
raise ArgumentError,
|
|
38
|
+
"Unexpected fields provided in input values: #{extra.sort.join(', ')}."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
values.each do |name, value|
|
|
42
|
+
info = get_field_info(name)
|
|
43
|
+
bits = info[:bits]
|
|
44
|
+
unless value.is_a?(Integer) && value >= 0 && value < (1 << bits)
|
|
45
|
+
raise BitOverflowError,
|
|
46
|
+
"Value '#{name}' (#{value}) exceeds its allocated #{bits} bits " \
|
|
47
|
+
"(maximum allowed: #{(1 << bits) - 1})."
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_s
|
|
53
|
+
"ObfusbitSchema(total_bits=#{@total_bits}, fields=#{@definition.length})"
|
|
54
|
+
end
|
|
55
|
+
alias inspect to_s
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def calculate_field_info(raw_schema)
|
|
60
|
+
info = {}
|
|
61
|
+
shift = 0
|
|
62
|
+
raw_schema.reverse_each do |item|
|
|
63
|
+
name = (item[:name] || item["name"]).to_s
|
|
64
|
+
bits = item[:bits] || item["bits"]
|
|
65
|
+
info[name] = { bits: bits, shift: shift }
|
|
66
|
+
shift += bits
|
|
67
|
+
end
|
|
68
|
+
info
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def validate_schema!(schema)
|
|
72
|
+
raise SchemaValidationError, "Schema must be a list of dictionaries." unless schema.is_a?(Array)
|
|
73
|
+
raise SchemaValidationError, "Schema cannot be empty." if schema.empty?
|
|
74
|
+
|
|
75
|
+
seen = Set.new
|
|
76
|
+
schema.each_with_index do |item, i|
|
|
77
|
+
unless item.is_a?(Hash)
|
|
78
|
+
raise SchemaValidationError,
|
|
79
|
+
"Schema item at index #{i} must be a hash, got #{item.class.name}."
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
name = item[:name] || item["name"]
|
|
83
|
+
bits = item[:bits] || item["bits"]
|
|
84
|
+
|
|
85
|
+
raise SchemaValidationError, "Schema item at index #{i} is missing the 'name' key." if name.nil?
|
|
86
|
+
raise SchemaValidationError, "Schema item at index #{i} is missing the 'bits' key." if bits.nil?
|
|
87
|
+
|
|
88
|
+
unless name.is_a?(String) || name.is_a?(Symbol)
|
|
89
|
+
raise SchemaValidationError,
|
|
90
|
+
"Schema item (index #{i}): 'name' must be a string, got #{name.class.name}."
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
unless bits.is_a?(Integer)
|
|
94
|
+
raise SchemaValidationError,
|
|
95
|
+
"Schema item '#{name}' (index #{i}): 'bits' must be an integer, got #{bits.class.name}."
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if bits <= 0
|
|
99
|
+
raise SchemaValidationError,
|
|
100
|
+
"Schema item '#{name}' (index #{i}): 'bits' must be a positive integer, got #{bits}."
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
name_str = name.to_s
|
|
104
|
+
if seen.include?(name_str)
|
|
105
|
+
raise SchemaValidationError,
|
|
106
|
+
"Schema contains duplicate name: '#{name_str}'. Names must be unique."
|
|
107
|
+
end
|
|
108
|
+
seen << name_str
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
class Obfuskey
|
|
2
|
+
KEY_LENGTH = 6
|
|
3
|
+
|
|
4
|
+
attr_reader :alphabet, :key_length, :maximum_value
|
|
5
|
+
|
|
6
|
+
def initialize(alphabet, key_length: KEY_LENGTH, multiplier: nil)
|
|
7
|
+
if alphabet.chars.uniq.length != alphabet.length
|
|
8
|
+
raise DuplicateError, "The alphabet contains duplicate characters."
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
if !multiplier.nil? && (!multiplier.is_a?(Integer) || multiplier.even?)
|
|
12
|
+
raise MultiplierError, "The multiplier must be an odd integer."
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
@alphabet = alphabet
|
|
16
|
+
@key_length = key_length
|
|
17
|
+
@maximum_value = alphabet.length**key_length - 1
|
|
18
|
+
@multiplier = multiplier
|
|
19
|
+
@prime_multiplier = PRIME_MULTIPLIER
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def multiplier
|
|
23
|
+
@multiplier ||= generate_multiplier
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def set_prime_multiplier(prime_multiplier)
|
|
27
|
+
@prime_multiplier = prime_multiplier
|
|
28
|
+
@multiplier = nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def get_key(value)
|
|
32
|
+
raise NegativeValueError, "The value must be greater than or equal to zero." if value < 0
|
|
33
|
+
|
|
34
|
+
if value > @maximum_value
|
|
35
|
+
raise MaximumValueError, "The maximum value possible is #{@maximum_value}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
return @alphabet[0] * @key_length if value.zero?
|
|
39
|
+
|
|
40
|
+
encoded = Utils.encode((value * multiplier) % (@maximum_value + 1), @alphabet)
|
|
41
|
+
encoded.rjust(@key_length, @alphabet[0])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def get_value(key)
|
|
45
|
+
unless key.chars.all? { |c| @alphabet.include?(c) }
|
|
46
|
+
raise UnknownKeyError,
|
|
47
|
+
"The key contains characters not found in the current alphabet."
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if key.length != @key_length
|
|
51
|
+
raise KeyLengthError, "The key length does not match the set length."
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
return 0 if key == @alphabet[0] * @key_length
|
|
55
|
+
|
|
56
|
+
decoded = Utils.decode(key, @alphabet)
|
|
57
|
+
max_p1 = @maximum_value + 1
|
|
58
|
+
decoded * Math.mod_inv(multiplier, max_p1) % max_p1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_s
|
|
62
|
+
multiplier_info =
|
|
63
|
+
if @multiplier.nil?
|
|
64
|
+
", multiplier=auto (prime_mult=#{@prime_multiplier})"
|
|
65
|
+
else
|
|
66
|
+
", multiplier=#{@multiplier}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
display_alphabet =
|
|
70
|
+
if @alphabet.length > 20
|
|
71
|
+
"'#{@alphabet[0, 10]}...#{@alphabet[-5, 5]}'"
|
|
72
|
+
else
|
|
73
|
+
"'#{@alphabet}'"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
"Obfuskey(alphabet=#{display_alphabet}, key_length=#{@key_length}#{multiplier_info})"
|
|
77
|
+
end
|
|
78
|
+
alias inspect to_s
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def generate_multiplier
|
|
83
|
+
Utils.generate_prime(@alphabet, @key_length, @prime_multiplier)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
class Obfuskey
|
|
2
|
+
PRIME_MULTIPLIER = 1.618033988749894848
|
|
3
|
+
|
|
4
|
+
module Utils
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def decode(value, alphabet)
|
|
8
|
+
chars = value.chars
|
|
9
|
+
unless chars.all? { |c| alphabet.include?(c) }
|
|
10
|
+
raise UnknownKeyError,
|
|
11
|
+
"The value contains characters not found in the current alphabet."
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
return alphabet.index(value) if chars.length == 1
|
|
15
|
+
|
|
16
|
+
base = alphabet.length
|
|
17
|
+
result = 0
|
|
18
|
+
chars.each do |c|
|
|
19
|
+
result = result * base + alphabet.index(c)
|
|
20
|
+
end
|
|
21
|
+
result
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def encode(value, alphabet)
|
|
25
|
+
raise NegativeValueError, "The value must be greater than or equal to zero." if value < 0
|
|
26
|
+
|
|
27
|
+
return alphabet[value] if value < alphabet.length
|
|
28
|
+
|
|
29
|
+
base = alphabet.length
|
|
30
|
+
key = +""
|
|
31
|
+
while value > 0
|
|
32
|
+
value, i = value.divmod(base)
|
|
33
|
+
key << alphabet[i]
|
|
34
|
+
end
|
|
35
|
+
key.reverse
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def generate_prime(alphabet, key_length, prime_multiplier = PRIME_MULTIPLIER)
|
|
39
|
+
# Mirror Python's Decimal(float) conversion: Rational(float) preserves
|
|
40
|
+
# the exact binary representation of a Float, matching Python's behavior.
|
|
41
|
+
factor = prime_multiplier.is_a?(Float) ? Rational(prime_multiplier) : prime_multiplier
|
|
42
|
+
target = ((alphabet.length**key_length - 1) * factor).to_i
|
|
43
|
+
Math.next_prime(target)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/obfuskey.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: obfuskey
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nathan Lucas
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rspec
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.12'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.12'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
description: A Ruby port of obfuskey that produces deterministic, reversible keys
|
|
41
|
+
from integers using a custom alphabet. Cross-compatible with the Python, JavaScript,
|
|
42
|
+
and Rust implementations.
|
|
43
|
+
email:
|
|
44
|
+
- nathan@bnlucas.com
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- LICENSE
|
|
50
|
+
- README.md
|
|
51
|
+
- lib/obfuskey.rb
|
|
52
|
+
- lib/obfuskey/alphabets.rb
|
|
53
|
+
- lib/obfuskey/errors.rb
|
|
54
|
+
- lib/obfuskey/math.rb
|
|
55
|
+
- lib/obfuskey/obfusbit.rb
|
|
56
|
+
- lib/obfuskey/obfusbit_schema.rb
|
|
57
|
+
- lib/obfuskey/obfuskey.rb
|
|
58
|
+
- lib/obfuskey/utils.rb
|
|
59
|
+
- lib/obfuskey/version.rb
|
|
60
|
+
homepage: https://github.com/bnlucas/obfuskey-rb
|
|
61
|
+
licenses:
|
|
62
|
+
- MIT
|
|
63
|
+
metadata:
|
|
64
|
+
homepage_uri: https://github.com/bnlucas/obfuskey-rb
|
|
65
|
+
source_code_uri: https://github.com/bnlucas/obfuskey-rb
|
|
66
|
+
rdoc_options: []
|
|
67
|
+
require_paths:
|
|
68
|
+
- lib
|
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 2.7.0
|
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '0'
|
|
79
|
+
requirements: []
|
|
80
|
+
rubygems_version: 3.6.9
|
|
81
|
+
specification_version: 4
|
|
82
|
+
summary: Generate obfuscated, fixed-length keys for integer values.
|
|
83
|
+
test_files: []
|