ff1 1.0.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 +65 -0
- data/DEMO.md +198 -0
- data/IMPLEMENTATION.md +354 -0
- data/LICENSE +21 -0
- data/README.md +168 -0
- data/examples/basic_usage.rb +90 -0
- data/lib/ff1/cipher.rb +353 -0
- data/lib/ff1/modes.rb +11 -0
- data/lib/ff1/version.rb +3 -0
- data/lib/ff1.rb +7 -0
- data/spec/ff1_spec.rb +190 -0
- data/spec/spec_helper.rb +14 -0
- metadata +84 -0
data/spec/ff1_spec.rb
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe FF1 do
|
4
|
+
it "has a version number" do
|
5
|
+
expect(FF1::VERSION).not_to be nil
|
6
|
+
end
|
7
|
+
|
8
|
+
describe FF1::Cipher do
|
9
|
+
let(:key) { "\x2B\x7E\x15\x16\x28\xAE\xD2\xA6\xAB\xF7\x15\x88\x09\xCF\x4F\x3C" }
|
10
|
+
let(:cipher) { FF1::Cipher.new(key, 10) }
|
11
|
+
|
12
|
+
describe "#initialize" do
|
13
|
+
it "accepts a valid 128-bit key" do
|
14
|
+
expect { FF1::Cipher.new(key, 10) }.not_to raise_error
|
15
|
+
end
|
16
|
+
|
17
|
+
it "accepts a valid 192-bit key" do
|
18
|
+
key_192 = "\x2B" * 24
|
19
|
+
expect { FF1::Cipher.new(key_192, 10) }.not_to raise_error
|
20
|
+
end
|
21
|
+
|
22
|
+
it "accepts a valid 256-bit key" do
|
23
|
+
key_256 = "\x2B" * 32
|
24
|
+
expect { FF1::Cipher.new(key_256, 10) }.not_to raise_error
|
25
|
+
end
|
26
|
+
|
27
|
+
it "rejects invalid key lengths" do
|
28
|
+
invalid_key = "\x2B" * 15
|
29
|
+
expect { FF1::Cipher.new(invalid_key, 10) }.to raise_error(FF1::Error, "Invalid key length")
|
30
|
+
end
|
31
|
+
|
32
|
+
it "rejects invalid radix values" do
|
33
|
+
expect { FF1::Cipher.new(key, 1) }.to raise_error(FF1::Error, "Invalid radix")
|
34
|
+
expect { FF1::Cipher.new(key, 65537) }.to raise_error(FF1::Error, "Invalid radix")
|
35
|
+
end
|
36
|
+
|
37
|
+
it "accepts valid radix values" do
|
38
|
+
expect { FF1::Cipher.new(key, 2) }.not_to raise_error
|
39
|
+
expect { FF1::Cipher.new(key, 16) }.not_to raise_error
|
40
|
+
expect { FF1::Cipher.new(key, 65536) }.not_to raise_error
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#encrypt and #decrypt" do
|
45
|
+
it "encrypts and decrypts correctly for numeric data" do
|
46
|
+
plaintext = "1234567890"
|
47
|
+
ciphertext = cipher.encrypt(plaintext)
|
48
|
+
|
49
|
+
expect(ciphertext).not_to eq(plaintext)
|
50
|
+
expect(ciphertext.length).to eq(plaintext.length)
|
51
|
+
expect(cipher.decrypt(ciphertext)).to eq(plaintext)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "preserves the format of the data" do
|
55
|
+
plaintext = "9876543210"
|
56
|
+
ciphertext = cipher.encrypt(plaintext)
|
57
|
+
|
58
|
+
expect(ciphertext.length).to eq(plaintext.length)
|
59
|
+
ciphertext.each_char do |char|
|
60
|
+
expect(char).to match(/[0-9]/)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it "produces different ciphertext for different plaintexts" do
|
65
|
+
plaintext1 = "1234567890"
|
66
|
+
plaintext2 = "0987654321"
|
67
|
+
|
68
|
+
ciphertext1 = cipher.encrypt(plaintext1)
|
69
|
+
ciphertext2 = cipher.encrypt(plaintext2)
|
70
|
+
|
71
|
+
expect(ciphertext1).not_to eq(ciphertext2)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "works with tweaks" do
|
75
|
+
plaintext = "1234567890"
|
76
|
+
tweak = "test"
|
77
|
+
|
78
|
+
ciphertext_with_tweak = cipher.encrypt(plaintext, tweak)
|
79
|
+
ciphertext_without_tweak = cipher.encrypt(plaintext)
|
80
|
+
|
81
|
+
expect(ciphertext_with_tweak).not_to eq(ciphertext_without_tweak)
|
82
|
+
expect(cipher.decrypt(ciphertext_with_tweak, tweak)).to eq(plaintext)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "handles hexadecimal data with radix 16" do
|
86
|
+
hex_cipher = FF1::Cipher.new(key, 16)
|
87
|
+
plaintext = "ABCDEF123456"
|
88
|
+
|
89
|
+
ciphertext = hex_cipher.encrypt(plaintext)
|
90
|
+
expect(hex_cipher.decrypt(ciphertext)).to eq(plaintext)
|
91
|
+
|
92
|
+
ciphertext.each_char do |char|
|
93
|
+
expect(char).to match(/[0-9A-F]/)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
it "raises error for input too short" do
|
98
|
+
expect { cipher.encrypt("1") }.to raise_error(FF1::Error, "Input length must be at least 2")
|
99
|
+
end
|
100
|
+
|
101
|
+
it "raises error for empty input" do
|
102
|
+
expect { cipher.encrypt("") }.to raise_error(FF1::Error, "Input cannot be empty")
|
103
|
+
end
|
104
|
+
|
105
|
+
it "raises error for nil input" do
|
106
|
+
expect { cipher.encrypt(nil) }.to raise_error(FF1::Error, "Input cannot be nil")
|
107
|
+
end
|
108
|
+
|
109
|
+
it "raises error for invalid characters" do
|
110
|
+
expect { cipher.encrypt("12A4") }.to raise_error(FF1::Error, /Invalid character/)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "raises error for domain too small" do
|
114
|
+
# Create a cipher with radix 2 and input "10" -> 2^2 = 4 < 100
|
115
|
+
binary_cipher = FF1::Cipher.new(key, 2)
|
116
|
+
short_plaintext = "10"
|
117
|
+
expect { binary_cipher.encrypt(short_plaintext) }.to raise_error(FF1::Error, /Domain size too small/)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "handles minimum valid domain size" do
|
121
|
+
plaintext = "12345" # 10^5 = 100,000 > 100
|
122
|
+
ciphertext = cipher.encrypt(plaintext)
|
123
|
+
expect(cipher.decrypt(ciphertext)).to eq(plaintext)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe "consistency tests" do
|
128
|
+
it "encryption is deterministic" do
|
129
|
+
plaintext = "1234567890"
|
130
|
+
|
131
|
+
ciphertext1 = cipher.encrypt(plaintext)
|
132
|
+
ciphertext2 = cipher.encrypt(plaintext)
|
133
|
+
|
134
|
+
expect(ciphertext1).to eq(ciphertext2)
|
135
|
+
end
|
136
|
+
|
137
|
+
it "decryption is deterministic" do
|
138
|
+
plaintext = "1234567890"
|
139
|
+
ciphertext = cipher.encrypt(plaintext)
|
140
|
+
|
141
|
+
decrypted1 = cipher.decrypt(ciphertext)
|
142
|
+
decrypted2 = cipher.decrypt(ciphertext)
|
143
|
+
|
144
|
+
expect(decrypted1).to eq(decrypted2)
|
145
|
+
expect(decrypted1).to eq(plaintext)
|
146
|
+
end
|
147
|
+
|
148
|
+
it "handles various input lengths" do
|
149
|
+
["123", "1234", "12345", "123456", "1234567", "12345678", "123456789", "1234567890"].each do |plaintext|
|
150
|
+
next if plaintext.length < 3 # Skip too short inputs
|
151
|
+
|
152
|
+
ciphertext = cipher.encrypt(plaintext)
|
153
|
+
decrypted = cipher.decrypt(ciphertext)
|
154
|
+
|
155
|
+
expect(decrypted).to eq(plaintext), "Failed for input: #{plaintext}"
|
156
|
+
expect(ciphertext.length).to eq(plaintext.length), "Length mismatch for: #{plaintext}"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe "different radix support" do
|
162
|
+
it "works with radix 2 (binary)" do
|
163
|
+
binary_cipher = FF1::Cipher.new(key, 2)
|
164
|
+
# Note: For radix 2, we need longer inputs to meet domain size requirements
|
165
|
+
# 2^7 = 128 > 100
|
166
|
+
plaintext = "1010101"
|
167
|
+
|
168
|
+
ciphertext = binary_cipher.encrypt(plaintext)
|
169
|
+
expect(binary_cipher.decrypt(ciphertext)).to eq(plaintext)
|
170
|
+
|
171
|
+
ciphertext.each_char do |char|
|
172
|
+
expect(char).to match(/[01]/)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
it "works with radix 36" do
|
177
|
+
alpha_cipher = FF1::Cipher.new(key, 36)
|
178
|
+
# For testing, we'll use a simple numeric string that fits within radix 36
|
179
|
+
plaintext = "123ABC"
|
180
|
+
|
181
|
+
# Convert to proper format for radix 36 (0-9, then continue with character codes)
|
182
|
+
# For simplicity in this test, we'll just use numbers
|
183
|
+
plaintext = "123456"
|
184
|
+
|
185
|
+
ciphertext = alpha_cipher.encrypt(plaintext)
|
186
|
+
expect(alpha_cipher.decrypt(ciphertext)).to eq(plaintext)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require_relative '../lib/ff1'
|
3
|
+
|
4
|
+
RSpec.configure do |config|
|
5
|
+
config.expect_with :rspec do |expectations|
|
6
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
7
|
+
end
|
8
|
+
|
9
|
+
config.mock_with :rspec do |mocks|
|
10
|
+
mocks.verify_partial_doubles = true
|
11
|
+
end
|
12
|
+
|
13
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ff1
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- FF1 Gem
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-09-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '13.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '13.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
description: A Ruby implementation of the FF1 Format Preserving Encryption algorithm
|
42
|
+
from NIST SP 800-38G
|
43
|
+
email:
|
44
|
+
- ahmed.abdelatife@gmail.com
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- CHANGELOG.md
|
50
|
+
- DEMO.md
|
51
|
+
- IMPLEMENTATION.md
|
52
|
+
- LICENSE
|
53
|
+
- README.md
|
54
|
+
- examples/basic_usage.rb
|
55
|
+
- lib/ff1.rb
|
56
|
+
- lib/ff1/cipher.rb
|
57
|
+
- lib/ff1/modes.rb
|
58
|
+
- lib/ff1/version.rb
|
59
|
+
- spec/ff1_spec.rb
|
60
|
+
- spec/spec_helper.rb
|
61
|
+
homepage: https://github.com/a-abdellatif98/ff1
|
62
|
+
licenses:
|
63
|
+
- MIT
|
64
|
+
metadata: {}
|
65
|
+
post_install_message:
|
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.4.1
|
81
|
+
signing_key:
|
82
|
+
specification_version: 4
|
83
|
+
summary: FF1 Format Preserving Encryption implementation
|
84
|
+
test_files: []
|