cyphera 0.0.1.alpha.1
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/lib/cyphera/cyphera.rb +212 -0
- data/lib/cyphera/ff1.rb +173 -0
- data/lib/cyphera/ff3.rb +148 -0
- data/lib/cyphera.rb +6 -0
- metadata +48 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: '092f5a2db55a53433ed97db72c86fb9f77a56dc15be2f96c343c36c06fb07ac2'
|
|
4
|
+
data.tar.gz: 289ca8aa1a02f678cc6ad267a103d0aa5a26c0ead54edce4d28bc8d30dbeb4db
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 75c5fe10c28c3d07559c084a9f73e5a2b8276afd700b00ecac5f3c408257ed48989fb16af488fa2e031701b4e30c4d956a9b9657e374c8a72d6db9d2b17ca3af
|
|
7
|
+
data.tar.gz: d5a63dd12e2751ac8741cc90628f2e28da9a4ca0438df0763a4e02d1c210e69b326b59213a14367d237798340c66c78b368eaa7e76ddae2c092acfe01c66be24
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'openssl'
|
|
3
|
+
|
|
4
|
+
module Cyphera
|
|
5
|
+
ALPHABETS = {
|
|
6
|
+
'digits' => '0123456789',
|
|
7
|
+
'alpha_lower' => 'abcdefghijklmnopqrstuvwxyz',
|
|
8
|
+
'alpha_upper' => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
9
|
+
'alpha' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
10
|
+
'alphanumeric' => '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
class Client
|
|
14
|
+
def self.load
|
|
15
|
+
env = ENV['CYPHERA_POLICY_FILE']
|
|
16
|
+
return from_file(env) if env && File.exist?(env)
|
|
17
|
+
return from_file('cyphera.json') if File.exist?('cyphera.json')
|
|
18
|
+
return from_file('/etc/cyphera/cyphera.json') if File.exist?('/etc/cyphera/cyphera.json')
|
|
19
|
+
raise 'No policy file found. Checked: CYPHERA_POLICY_FILE env, ./cyphera.json, /etc/cyphera/cyphera.json'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.from_file(path)
|
|
23
|
+
config = JSON.parse(File.read(path))
|
|
24
|
+
from_config(config)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.from_config(config)
|
|
28
|
+
new(config)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def protect(value, policy_name)
|
|
32
|
+
policy = get_policy(policy_name)
|
|
33
|
+
case policy['engine']
|
|
34
|
+
when 'ff1' then protect_fpe(value, policy, false)
|
|
35
|
+
when 'ff3' then protect_fpe(value, policy, true)
|
|
36
|
+
when 'mask' then protect_mask(value, policy)
|
|
37
|
+
when 'hash' then protect_hash(value, policy)
|
|
38
|
+
else raise ArgumentError, "Unknown engine: #{policy['engine']}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def access(protected_value, policy_name = nil)
|
|
43
|
+
if policy_name
|
|
44
|
+
policy = get_policy(policy_name)
|
|
45
|
+
return access_fpe(protected_value, policy)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
@tag_index.keys.sort_by { |t| -t.length }.each do |tag|
|
|
49
|
+
if protected_value.length > tag.length && protected_value.start_with?(tag)
|
|
50
|
+
policy = get_policy(@tag_index[tag])
|
|
51
|
+
return access_fpe(protected_value, policy)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
raise ArgumentError, 'No matching tag found. Use access(value, policy_name) for untagged values.'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def initialize(config)
|
|
61
|
+
@policies = {}
|
|
62
|
+
@tag_index = {}
|
|
63
|
+
@keys = {}
|
|
64
|
+
|
|
65
|
+
(config['keys'] || {}).each do |name, val|
|
|
66
|
+
material = val.is_a?(String) ? val : val['material']
|
|
67
|
+
@keys[name] = [material].pack('H*')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
(config['policies'] || {}).each do |name, pol|
|
|
71
|
+
tag_enabled = pol.fetch('tag_enabled', true)
|
|
72
|
+
tag = pol['tag']
|
|
73
|
+
|
|
74
|
+
if tag_enabled && (tag.nil? || tag.empty?)
|
|
75
|
+
raise ArgumentError, "Policy '#{name}' has tag_enabled=true but no tag specified"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if tag_enabled && tag
|
|
79
|
+
if @tag_index.key?(tag)
|
|
80
|
+
raise ArgumentError, "Tag collision: '#{tag}' used by both '#{@tag_index[tag]}' and '#{name}'"
|
|
81
|
+
end
|
|
82
|
+
@tag_index[tag] = name
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
@policies[name] = {
|
|
86
|
+
'engine' => pol.fetch('engine', 'ff1'),
|
|
87
|
+
'alphabet' => resolve_alphabet(pol['alphabet']),
|
|
88
|
+
'key_ref' => pol['key_ref'],
|
|
89
|
+
'tag' => tag,
|
|
90
|
+
'tag_enabled' => tag_enabled,
|
|
91
|
+
'pattern' => pol['pattern'],
|
|
92
|
+
'algorithm' => pol.fetch('algorithm', 'sha256')
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def get_policy(name)
|
|
98
|
+
@policies.fetch(name) { raise ArgumentError, "Unknown policy: #{name}" }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def resolve_key(key_ref)
|
|
102
|
+
raise ArgumentError, 'No key_ref in policy' if key_ref.nil? || key_ref.empty?
|
|
103
|
+
@keys.fetch(key_ref) { raise ArgumentError, "Unknown key: #{key_ref}" }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def resolve_alphabet(name)
|
|
107
|
+
return ALPHABETS['alphanumeric'] if name.nil? || name.empty?
|
|
108
|
+
ALPHABETS[name] || name
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def protect_fpe(value, policy, is_ff3)
|
|
112
|
+
key = resolve_key(policy['key_ref'])
|
|
113
|
+
alphabet = policy['alphabet']
|
|
114
|
+
encryptable, positions, chars = extract_passthroughs(value, alphabet)
|
|
115
|
+
raise ArgumentError, 'No encryptable characters in input' if encryptable.empty?
|
|
116
|
+
|
|
117
|
+
encrypted = if is_ff3
|
|
118
|
+
FF3.new(key, "\x00" * 8, alphabet).encrypt(encryptable)
|
|
119
|
+
else
|
|
120
|
+
FF1.new(key, '', alphabet).encrypt(encryptable)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
result = reinsert_passthroughs(encrypted, positions, chars)
|
|
124
|
+
if policy['tag_enabled'] && policy['tag']
|
|
125
|
+
policy['tag'] + result
|
|
126
|
+
else
|
|
127
|
+
result
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def access_fpe(protected_value, policy)
|
|
132
|
+
unless %w[ff1 ff3].include?(policy['engine'])
|
|
133
|
+
raise ArgumentError, "Cannot reverse '#{policy['engine']}' — not reversible"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
key = resolve_key(policy['key_ref'])
|
|
137
|
+
alphabet = policy['alphabet']
|
|
138
|
+
|
|
139
|
+
without_tag = protected_value
|
|
140
|
+
if policy['tag_enabled'] && policy['tag']
|
|
141
|
+
without_tag = protected_value[policy['tag'].length..]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
encryptable, positions, chars = extract_passthroughs(without_tag, alphabet)
|
|
145
|
+
|
|
146
|
+
decrypted = if policy['engine'] == 'ff3'
|
|
147
|
+
FF3.new(key, "\x00" * 8, alphabet).decrypt(encryptable)
|
|
148
|
+
else
|
|
149
|
+
FF1.new(key, '', alphabet).decrypt(encryptable)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
reinsert_passthroughs(decrypted, positions, chars)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def protect_mask(value, policy)
|
|
156
|
+
pattern = policy['pattern']
|
|
157
|
+
raise ArgumentError, "Mask policy requires 'pattern'" if pattern.nil? || pattern.empty?
|
|
158
|
+
len = value.length
|
|
159
|
+
case pattern
|
|
160
|
+
when 'last4', 'last_4' then ('*' * [0, len - 4].max) + value[[0, len - 4].max..]
|
|
161
|
+
when 'last2', 'last_2' then ('*' * [0, len - 2].max) + value[[0, len - 2].max..]
|
|
162
|
+
when 'first1', 'first_1' then value[0, [1, len].min] + ('*' * [0, len - 1].max)
|
|
163
|
+
when 'first3', 'first_3' then value[0, [3, len].min] + ('*' * [0, len - 3].max)
|
|
164
|
+
else '*' * len
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def protect_hash(value, policy)
|
|
169
|
+
algo = policy['algorithm'].downcase.delete('-')
|
|
170
|
+
digest = case algo
|
|
171
|
+
when 'sha256' then 'SHA256'
|
|
172
|
+
when 'sha384' then 'SHA384'
|
|
173
|
+
when 'sha512' then 'SHA512'
|
|
174
|
+
else raise ArgumentError, "Unsupported hash algorithm: #{policy['algorithm']}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
if policy['key_ref'] && !policy['key_ref'].empty?
|
|
178
|
+
key = resolve_key(policy['key_ref'])
|
|
179
|
+
OpenSSL::HMAC.hexdigest(digest, key, value)
|
|
180
|
+
else
|
|
181
|
+
OpenSSL::Digest.hexdigest(digest, value)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def extract_passthroughs(value, alphabet)
|
|
186
|
+
encryptable = ''
|
|
187
|
+
positions = []
|
|
188
|
+
chars = []
|
|
189
|
+
value.each_char.with_index do |c, i|
|
|
190
|
+
if alphabet.include?(c)
|
|
191
|
+
encryptable << c
|
|
192
|
+
else
|
|
193
|
+
positions << i
|
|
194
|
+
chars << c
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
[encryptable, positions, chars]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def reinsert_passthroughs(encrypted, positions, chars)
|
|
201
|
+
result = encrypted.chars
|
|
202
|
+
positions.each_with_index do |pos, i|
|
|
203
|
+
if pos <= result.length
|
|
204
|
+
result.insert(pos, chars[i])
|
|
205
|
+
else
|
|
206
|
+
result << chars[i]
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
result.join
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
data/lib/cyphera/ff1.rb
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
require 'openssl'
|
|
2
|
+
|
|
3
|
+
module Cyphera
|
|
4
|
+
class FF1
|
|
5
|
+
def initialize(key, tweak, alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
|
|
6
|
+
raise ArgumentError, "Key must be 16, 24, or 32 bytes" unless [16, 24, 32].include?(key.bytesize)
|
|
7
|
+
raise ArgumentError, "Alphabet must have >= 2 characters" if alphabet.length < 2
|
|
8
|
+
|
|
9
|
+
@key = key
|
|
10
|
+
@tweak = tweak
|
|
11
|
+
@alphabet = alphabet
|
|
12
|
+
@radix = alphabet.length
|
|
13
|
+
@char_map = {}
|
|
14
|
+
alphabet.each_char.with_index { |c, i| @char_map[c] = i }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def encrypt(plaintext)
|
|
18
|
+
digits = to_digits(plaintext)
|
|
19
|
+
result = ff1_encrypt(digits, @tweak)
|
|
20
|
+
from_digits(result)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def decrypt(ciphertext)
|
|
24
|
+
digits = to_digits(ciphertext)
|
|
25
|
+
result = ff1_decrypt(digits, @tweak)
|
|
26
|
+
from_digits(result)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def to_digits(s)
|
|
32
|
+
s.each_char.map { |c| @char_map.fetch(c) { raise ArgumentError, "Character '#{c}' not in alphabet" } }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def from_digits(d)
|
|
36
|
+
d.map { |i| @alphabet[i] }.join
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def aes_ecb(block)
|
|
40
|
+
cipher = OpenSSL::Cipher::AES.new(@key.bytesize * 8, :ECB)
|
|
41
|
+
cipher.encrypt
|
|
42
|
+
cipher.padding = 0
|
|
43
|
+
cipher.key = @key
|
|
44
|
+
cipher.update(block) + cipher.final
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def prf(data)
|
|
48
|
+
y = "\x00" * 16
|
|
49
|
+
(0...data.bytesize).step(16) do |off|
|
|
50
|
+
block = y.bytes.zip(data.byteslice(off, 16).bytes).map { |a, b| a ^ b }.pack('C*')
|
|
51
|
+
y = aes_ecb(block)
|
|
52
|
+
end
|
|
53
|
+
y
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def expand_s(r, d)
|
|
57
|
+
blocks = (d + 15) / 16
|
|
58
|
+
out = r.dup
|
|
59
|
+
(1...blocks).each do |j|
|
|
60
|
+
x = ([0] * 12 + [(j >> 24) & 0xFF, (j >> 16) & 0xFF, (j >> 8) & 0xFF, j & 0xFF]).pack('C*')
|
|
61
|
+
# XOR with R (not previous block) per NIST SP 800-38G
|
|
62
|
+
x = x.bytes.zip(r.bytes).map { |a, b| a ^ b }.pack('C*')
|
|
63
|
+
out << aes_ecb(x)
|
|
64
|
+
end
|
|
65
|
+
out.byteslice(0, d)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def num(digits)
|
|
69
|
+
r = 0
|
|
70
|
+
digits.each { |d| r = r * @radix + d }
|
|
71
|
+
r
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def str(n, len)
|
|
75
|
+
result = Array.new(len, 0)
|
|
76
|
+
(len - 1).downto(0) do |i|
|
|
77
|
+
result[i] = n % @radix
|
|
78
|
+
n /= @radix
|
|
79
|
+
end
|
|
80
|
+
result
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def compute_b(v)
|
|
84
|
+
(Math.log2(@radix) * v / 8.0).ceil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_p(u, n, t)
|
|
88
|
+
[1, 2, 1, (@radix >> 16) & 0xFF, (@radix >> 8) & 0xFF, @radix & 0xFF, 10, u,
|
|
89
|
+
(n >> 24) & 0xFF, (n >> 16) & 0xFF, (n >> 8) & 0xFF, n & 0xFF,
|
|
90
|
+
(t >> 24) & 0xFF, (t >> 16) & 0xFF, (t >> 8) & 0xFF, t & 0xFF].pack('C*')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_q(t, i, num_bytes, b)
|
|
94
|
+
pad = (16 - ((t.bytesize + 1 + b) % 16)) % 16
|
|
95
|
+
q = t.dup
|
|
96
|
+
q << ("\x00" * pad)
|
|
97
|
+
q << [i].pack('C')
|
|
98
|
+
if num_bytes.bytesize < b
|
|
99
|
+
q << ("\x00" * (b - num_bytes.bytesize))
|
|
100
|
+
end
|
|
101
|
+
start = [0, num_bytes.bytesize - b].max
|
|
102
|
+
q << num_bytes.byteslice(start..)
|
|
103
|
+
q
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def bigint_to_bytes(val, len)
|
|
107
|
+
hex = val.to_s(16)
|
|
108
|
+
hex = "0#{hex}" if hex.length.odd?
|
|
109
|
+
bytes = [hex].pack('H*')
|
|
110
|
+
if bytes.bytesize < len
|
|
111
|
+
bytes = ("\x00" * (len - bytes.bytesize)) + bytes
|
|
112
|
+
elsif bytes.bytesize > len
|
|
113
|
+
bytes = bytes.byteslice(-len, len)
|
|
114
|
+
end
|
|
115
|
+
bytes
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def ff1_encrypt(pt, t)
|
|
119
|
+
n = pt.length
|
|
120
|
+
u = n / 2
|
|
121
|
+
v = n - u
|
|
122
|
+
a = pt[0...u]
|
|
123
|
+
b = pt[u..]
|
|
124
|
+
|
|
125
|
+
bval = compute_b(v)
|
|
126
|
+
d = 4 * ((bval + 3) / 4) + 4
|
|
127
|
+
p = build_p(u, n, t.bytesize)
|
|
128
|
+
|
|
129
|
+
10.times do |i|
|
|
130
|
+
num_b = bigint_to_bytes(num(b), [bval, 1].max)
|
|
131
|
+
q = build_q(t, i, num_b, bval)
|
|
132
|
+
r = prf(p + q)
|
|
133
|
+
s = expand_s(r, d)
|
|
134
|
+
y = s.unpack1('H*').to_i(16)
|
|
135
|
+
|
|
136
|
+
m = i.even? ? u : v
|
|
137
|
+
c = (num(a) + y) % (@radix ** m)
|
|
138
|
+
a = b
|
|
139
|
+
b = str(c, m)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
a + b
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def ff1_decrypt(ct, t)
|
|
146
|
+
n = ct.length
|
|
147
|
+
u = n / 2
|
|
148
|
+
v = n - u
|
|
149
|
+
a = ct[0...u]
|
|
150
|
+
b = ct[u..]
|
|
151
|
+
|
|
152
|
+
bval = compute_b(v)
|
|
153
|
+
d = 4 * ((bval + 3) / 4) + 4
|
|
154
|
+
p = build_p(u, n, t.bytesize)
|
|
155
|
+
|
|
156
|
+
9.downto(0) do |i|
|
|
157
|
+
num_a = bigint_to_bytes(num(a), [bval, 1].max)
|
|
158
|
+
q = build_q(t, i, num_a, bval)
|
|
159
|
+
r = prf(p + q)
|
|
160
|
+
s = expand_s(r, d)
|
|
161
|
+
y = s.unpack1('H*').to_i(16)
|
|
162
|
+
|
|
163
|
+
m = i.even? ? u : v
|
|
164
|
+
mod = @radix ** m
|
|
165
|
+
c = (num(b) - y) % mod
|
|
166
|
+
b = a
|
|
167
|
+
a = str(c, m)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
a + b
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
data/lib/cyphera/ff3.rb
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
require 'openssl'
|
|
2
|
+
|
|
3
|
+
module Cyphera
|
|
4
|
+
class FF3
|
|
5
|
+
def initialize(key, tweak, alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
|
|
6
|
+
raise ArgumentError, "Key must be 16, 24, or 32 bytes" unless [16, 24, 32].include?(key.bytesize)
|
|
7
|
+
raise ArgumentError, "Tweak must be exactly 8 bytes" unless tweak.bytesize == 8
|
|
8
|
+
raise ArgumentError, "Alphabet must have >= 2 characters" if alphabet.length < 2
|
|
9
|
+
|
|
10
|
+
@key = key.reverse
|
|
11
|
+
@tweak = tweak
|
|
12
|
+
@alphabet = alphabet
|
|
13
|
+
@radix = alphabet.length
|
|
14
|
+
@char_map = {}
|
|
15
|
+
alphabet.each_char.with_index { |c, i| @char_map[c] = i }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def encrypt(plaintext)
|
|
19
|
+
digits = to_digits(plaintext)
|
|
20
|
+
result = ff3_encrypt(digits)
|
|
21
|
+
from_digits(result)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def decrypt(ciphertext)
|
|
25
|
+
digits = to_digits(ciphertext)
|
|
26
|
+
result = ff3_decrypt(digits)
|
|
27
|
+
from_digits(result)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def to_digits(s)
|
|
33
|
+
s.each_char.map { |c| @char_map.fetch(c) { raise ArgumentError, "Character '#{c}' not in alphabet" } }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def from_digits(d)
|
|
37
|
+
d.map { |i| @alphabet[i] }.join
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def aes_ecb(block)
|
|
41
|
+
cipher = OpenSSL::Cipher::AES.new(@key.bytesize * 8, :ECB)
|
|
42
|
+
cipher.encrypt
|
|
43
|
+
cipher.padding = 0
|
|
44
|
+
cipher.key = @key
|
|
45
|
+
cipher.update(block) + cipher.final
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def num(digits)
|
|
49
|
+
r = 0
|
|
50
|
+
digits.each { |d| r = r * @radix + d }
|
|
51
|
+
r
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def str(n, len)
|
|
55
|
+
result = Array.new(len, 0)
|
|
56
|
+
(len - 1).downto(0) do |i|
|
|
57
|
+
result[i] = n % @radix
|
|
58
|
+
n /= @radix
|
|
59
|
+
end
|
|
60
|
+
result
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def calc_p(round, w, half)
|
|
64
|
+
inp = Array.new(16, 0)
|
|
65
|
+
inp[0] = w.getbyte(0)
|
|
66
|
+
inp[1] = w.getbyte(1)
|
|
67
|
+
inp[2] = w.getbyte(2)
|
|
68
|
+
inp[3] = w.getbyte(3) ^ round
|
|
69
|
+
|
|
70
|
+
rev_half = half.reverse
|
|
71
|
+
half_num = num(rev_half)
|
|
72
|
+
half_hex = half_num.to_s(16)
|
|
73
|
+
half_hex = "0#{half_hex}" if half_hex.length.odd?
|
|
74
|
+
half_bytes = [half_hex].pack('H*')
|
|
75
|
+
half_bytes = "\x00" if half_bytes.empty?
|
|
76
|
+
|
|
77
|
+
if half_bytes.bytesize <= 12
|
|
78
|
+
pos = 16 - half_bytes.bytesize
|
|
79
|
+
half_bytes.bytes.each_with_index { |b, k| inp[pos + k] = b }
|
|
80
|
+
else
|
|
81
|
+
start = half_bytes.bytesize - 12
|
|
82
|
+
12.times { |k| inp[4 + k] = half_bytes.getbyte(start + k) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
rev_inp = inp.pack('C*').reverse
|
|
86
|
+
aes_out = aes_ecb(rev_inp)
|
|
87
|
+
rev_out = aes_out.reverse
|
|
88
|
+
|
|
89
|
+
rev_out.unpack1('H*').to_i(16)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def ff3_encrypt(pt)
|
|
93
|
+
n = pt.length
|
|
94
|
+
u = (n + 1) / 2
|
|
95
|
+
v = n - u
|
|
96
|
+
a = pt[0...u].dup
|
|
97
|
+
b = pt[u..].dup
|
|
98
|
+
|
|
99
|
+
8.times do |i|
|
|
100
|
+
if i.even?
|
|
101
|
+
w = @tweak.byteslice(4, 4)
|
|
102
|
+
p = calc_p(i, w, b)
|
|
103
|
+
m = @radix ** u
|
|
104
|
+
a_num = num(a.reverse)
|
|
105
|
+
y = (a_num + p) % m
|
|
106
|
+
a = str(y, u).reverse
|
|
107
|
+
else
|
|
108
|
+
w = @tweak.byteslice(0, 4)
|
|
109
|
+
p = calc_p(i, w, a)
|
|
110
|
+
m = @radix ** v
|
|
111
|
+
b_num = num(b.reverse)
|
|
112
|
+
y = (b_num + p) % m
|
|
113
|
+
b = str(y, v).reverse
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
a + b
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def ff3_decrypt(ct)
|
|
121
|
+
n = ct.length
|
|
122
|
+
u = (n + 1) / 2
|
|
123
|
+
v = n - u
|
|
124
|
+
a = ct[0...u].dup
|
|
125
|
+
b = ct[u..].dup
|
|
126
|
+
|
|
127
|
+
7.downto(0) do |i|
|
|
128
|
+
if i.even?
|
|
129
|
+
w = @tweak.byteslice(4, 4)
|
|
130
|
+
p = calc_p(i, w, b)
|
|
131
|
+
m = @radix ** u
|
|
132
|
+
a_num = num(a.reverse)
|
|
133
|
+
y = (a_num - p) % m
|
|
134
|
+
a = str(y, u).reverse
|
|
135
|
+
else
|
|
136
|
+
w = @tweak.byteslice(0, 4)
|
|
137
|
+
p = calc_p(i, w, a)
|
|
138
|
+
m = @radix ** v
|
|
139
|
+
b_num = num(b.reverse)
|
|
140
|
+
y = (b_num - p) % m
|
|
141
|
+
b = str(y, v).reverse
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
a + b
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
data/lib/cyphera.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: cyphera
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1.alpha.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Horizon Digital Engineering
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Cyphera is an open-source data protection SDK for Ruby. Format-preserving
|
|
13
|
+
encryption (FF1/FF3), data masking, and hashing. Policy-driven protect/access API.
|
|
14
|
+
Cross-language compatible.
|
|
15
|
+
email: leslie.gutschow@horizondigital.dev
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- lib/cyphera.rb
|
|
21
|
+
- lib/cyphera/cyphera.rb
|
|
22
|
+
- lib/cyphera/ff1.rb
|
|
23
|
+
- lib/cyphera/ff3.rb
|
|
24
|
+
homepage: https://cyphera.io
|
|
25
|
+
licenses:
|
|
26
|
+
- Apache-2.0
|
|
27
|
+
metadata:
|
|
28
|
+
source_code_uri: https://github.com/cyphera-labs/cyphera-ruby
|
|
29
|
+
homepage_uri: https://cyphera.io
|
|
30
|
+
rdoc_options: []
|
|
31
|
+
require_paths:
|
|
32
|
+
- lib
|
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
34
|
+
requirements:
|
|
35
|
+
- - ">="
|
|
36
|
+
- !ruby/object:Gem::Version
|
|
37
|
+
version: '3.0'
|
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '0'
|
|
43
|
+
requirements: []
|
|
44
|
+
rubygems_version: 4.0.6
|
|
45
|
+
specification_version: 4
|
|
46
|
+
summary: Data protection SDK — format-preserving encryption (FF1/FF3), data masking,
|
|
47
|
+
and hashing.
|
|
48
|
+
test_files: []
|