ff1 1.0.0 → 1.2.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.
data/spec/ff1_spec.rb CHANGED
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  RSpec.describe FF1 do
4
- it "has a version number" do
6
+ it 'has a version number' do
5
7
  expect(FF1::VERSION).not_to be nil
6
8
  end
7
9
 
@@ -9,182 +11,321 @@ RSpec.describe FF1 do
9
11
  let(:key) { "\x2B\x7E\x15\x16\x28\xAE\xD2\xA6\xAB\xF7\x15\x88\x09\xCF\x4F\x3C" }
10
12
  let(:cipher) { FF1::Cipher.new(key, 10) }
11
13
 
12
- describe "#initialize" do
13
- it "accepts a valid 128-bit key" do
14
+ describe '#initialize' do
15
+ it 'accepts a valid 128-bit key' do
14
16
  expect { FF1::Cipher.new(key, 10) }.not_to raise_error
15
17
  end
16
18
 
17
- it "accepts a valid 192-bit key" do
19
+ it 'accepts a valid 192-bit key' do
18
20
  key_192 = "\x2B" * 24
19
21
  expect { FF1::Cipher.new(key_192, 10) }.not_to raise_error
20
22
  end
21
23
 
22
- it "accepts a valid 256-bit key" do
24
+ it 'accepts a valid 256-bit key' do
23
25
  key_256 = "\x2B" * 32
24
26
  expect { FF1::Cipher.new(key_256, 10) }.not_to raise_error
25
27
  end
26
28
 
27
- it "rejects invalid key lengths" do
29
+ it 'rejects invalid key lengths' do
28
30
  invalid_key = "\x2B" * 15
29
- expect { FF1::Cipher.new(invalid_key, 10) }.to raise_error(FF1::Error, "Invalid key length")
31
+ expect do
32
+ FF1::Cipher.new(invalid_key, 10)
33
+ end.to raise_error(FF1::Error, 'Invalid key length - must be 16, 24, or 32 bytes')
30
34
  end
31
35
 
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")
36
+ it 'rejects invalid radix values' do
37
+ expect { FF1::Cipher.new(key, 1) }.to raise_error(FF1::Error, 'Invalid radix - must be between 2 and 65536')
38
+ expect do
39
+ FF1::Cipher.new(key, 65_537)
40
+ end.to raise_error(FF1::Error, 'Invalid radix - must be between 2 and 65536')
35
41
  end
36
42
 
37
- it "accepts valid radix values" do
43
+ it 'accepts valid radix values' do
38
44
  expect { FF1::Cipher.new(key, 2) }.not_to raise_error
39
45
  expect { FF1::Cipher.new(key, 16) }.not_to raise_error
40
- expect { FF1::Cipher.new(key, 65536) }.not_to raise_error
46
+ expect { FF1::Cipher.new(key, 65_536) }.not_to raise_error
41
47
  end
42
48
  end
43
49
 
44
- describe "#encrypt and #decrypt" do
45
- it "encrypts and decrypts correctly for numeric data" do
46
- plaintext = "1234567890"
50
+ describe '#encrypt and #decrypt' do
51
+ it 'encrypts and decrypts correctly for numeric data' do
52
+ plaintext = '1234567890'
47
53
  ciphertext = cipher.encrypt(plaintext)
48
-
54
+
49
55
  expect(ciphertext).not_to eq(plaintext)
50
56
  expect(ciphertext.length).to eq(plaintext.length)
51
57
  expect(cipher.decrypt(ciphertext)).to eq(plaintext)
52
58
  end
53
59
 
54
- it "preserves the format of the data" do
55
- plaintext = "9876543210"
60
+ it 'preserves the format of the data' do
61
+ plaintext = '9876543210'
56
62
  ciphertext = cipher.encrypt(plaintext)
57
-
63
+
58
64
  expect(ciphertext.length).to eq(plaintext.length)
59
65
  ciphertext.each_char do |char|
60
66
  expect(char).to match(/[0-9]/)
61
67
  end
62
68
  end
63
69
 
64
- it "produces different ciphertext for different plaintexts" do
65
- plaintext1 = "1234567890"
66
- plaintext2 = "0987654321"
67
-
70
+ it 'produces different ciphertext for different plaintexts' do
71
+ plaintext1 = '1234567890'
72
+ plaintext2 = '0987654321'
73
+
68
74
  ciphertext1 = cipher.encrypt(plaintext1)
69
75
  ciphertext2 = cipher.encrypt(plaintext2)
70
-
76
+
71
77
  expect(ciphertext1).not_to eq(ciphertext2)
72
78
  end
73
79
 
74
- it "works with tweaks" do
75
- plaintext = "1234567890"
76
- tweak = "test"
77
-
80
+ it 'works with tweaks' do
81
+ plaintext = '1234567890'
82
+ tweak = 'test'
83
+
78
84
  ciphertext_with_tweak = cipher.encrypt(plaintext, tweak)
79
85
  ciphertext_without_tweak = cipher.encrypt(plaintext)
80
-
86
+
81
87
  expect(ciphertext_with_tweak).not_to eq(ciphertext_without_tweak)
82
88
  expect(cipher.decrypt(ciphertext_with_tweak, tweak)).to eq(plaintext)
83
89
  end
84
90
 
85
- it "handles hexadecimal data with radix 16" do
91
+ it 'handles hexadecimal data with radix 16' do
86
92
  hex_cipher = FF1::Cipher.new(key, 16)
87
- plaintext = "ABCDEF123456"
88
-
93
+ plaintext = 'ABCDEF123456'
94
+
89
95
  ciphertext = hex_cipher.encrypt(plaintext)
90
96
  expect(hex_cipher.decrypt(ciphertext)).to eq(plaintext)
91
-
97
+
92
98
  ciphertext.each_char do |char|
93
99
  expect(char).to match(/[0-9A-F]/)
94
100
  end
95
101
  end
96
102
 
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")
103
+ it 'raises error for input too short' do
104
+ expect { cipher.encrypt('1') }.to raise_error(FF1::Error, 'Input length must be at least 2')
99
105
  end
100
106
 
101
- it "raises error for empty input" do
102
- expect { cipher.encrypt("") }.to raise_error(FF1::Error, "Input cannot be empty")
107
+ it 'raises error for empty input' do
108
+ expect { cipher.encrypt('') }.to raise_error(FF1::Error, 'Input cannot be empty')
103
109
  end
104
110
 
105
- it "raises error for nil input" do
106
- expect { cipher.encrypt(nil) }.to raise_error(FF1::Error, "Input cannot be nil")
111
+ it 'raises error for nil input' do
112
+ expect { cipher.encrypt(nil) }.to raise_error(FF1::Error, 'Input cannot be nil')
107
113
  end
108
114
 
109
- it "raises error for invalid characters" do
110
- expect { cipher.encrypt("12A4") }.to raise_error(FF1::Error, /Invalid character/)
115
+ it 'raises error for invalid characters' do
116
+ expect { cipher.encrypt('12A4') }.to raise_error(FF1::Error, /Invalid character/)
111
117
  end
112
118
 
113
- it "raises error for domain too small" do
119
+ it 'raises error for domain too small' do
114
120
  # Create a cipher with radix 2 and input "10" -> 2^2 = 4 < 100
115
121
  binary_cipher = FF1::Cipher.new(key, 2)
116
- short_plaintext = "10"
122
+ short_plaintext = '10'
117
123
  expect { binary_cipher.encrypt(short_plaintext) }.to raise_error(FF1::Error, /Domain size too small/)
118
124
  end
119
125
 
120
- it "handles minimum valid domain size" do
121
- plaintext = "12345" # 10^5 = 100,000 > 100
126
+ it 'handles minimum valid domain size' do
127
+ plaintext = '12345' # 10^5 = 100,000 > 100
122
128
  ciphertext = cipher.encrypt(plaintext)
123
129
  expect(cipher.decrypt(ciphertext)).to eq(plaintext)
124
130
  end
125
131
  end
126
132
 
127
- describe "consistency tests" do
128
- it "encryption is deterministic" do
129
- plaintext = "1234567890"
130
-
133
+ describe 'consistency tests' do
134
+ it 'encryption is deterministic' do
135
+ plaintext = '1234567890'
136
+
131
137
  ciphertext1 = cipher.encrypt(plaintext)
132
138
  ciphertext2 = cipher.encrypt(plaintext)
133
-
139
+
134
140
  expect(ciphertext1).to eq(ciphertext2)
135
141
  end
136
142
 
137
- it "decryption is deterministic" do
138
- plaintext = "1234567890"
143
+ it 'decryption is deterministic' do
144
+ plaintext = '1234567890'
139
145
  ciphertext = cipher.encrypt(plaintext)
140
-
146
+
141
147
  decrypted1 = cipher.decrypt(ciphertext)
142
148
  decrypted2 = cipher.decrypt(ciphertext)
143
-
149
+
144
150
  expect(decrypted1).to eq(decrypted2)
145
151
  expect(decrypted1).to eq(plaintext)
146
152
  end
147
153
 
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
-
154
+ it 'handles various input lengths' do
155
+ %w[123 1234 12345 123456 1234567 12345678 123456789 1234567890].each do |plaintext|
156
+ next if plaintext.length < 3 # Skip too short inputs
157
+
152
158
  ciphertext = cipher.encrypt(plaintext)
153
159
  decrypted = cipher.decrypt(ciphertext)
154
-
160
+
155
161
  expect(decrypted).to eq(plaintext), "Failed for input: #{plaintext}"
156
162
  expect(ciphertext.length).to eq(plaintext.length), "Length mismatch for: #{plaintext}"
157
163
  end
158
164
  end
159
165
  end
160
166
 
161
- describe "different radix support" do
162
- it "works with radix 2 (binary)" do
167
+ describe 'different radix support' do
168
+ it 'works with radix 2 (binary)' do
163
169
  binary_cipher = FF1::Cipher.new(key, 2)
164
- # Note: For radix 2, we need longer inputs to meet domain size requirements
170
+ # NOTE: For radix 2, we need longer inputs to meet domain size requirements
165
171
  # 2^7 = 128 > 100
166
- plaintext = "1010101"
167
-
172
+ plaintext = '1010101'
173
+
168
174
  ciphertext = binary_cipher.encrypt(plaintext)
169
175
  expect(binary_cipher.decrypt(ciphertext)).to eq(plaintext)
170
-
176
+
171
177
  ciphertext.each_char do |char|
172
178
  expect(char).to match(/[01]/)
173
179
  end
174
180
  end
175
181
 
176
- it "works with radix 36" do
182
+ it 'works with radix 36' do
177
183
  alpha_cipher = FF1::Cipher.new(key, 36)
178
184
  # For testing, we'll use a simple numeric string that fits within radix 36
179
- plaintext = "123ABC"
180
-
185
+
181
186
  # Convert to proper format for radix 36 (0-9, then continue with character codes)
182
187
  # For simplicity in this test, we'll just use numbers
183
- plaintext = "123456"
184
-
188
+ plaintext = '123456'
189
+
185
190
  ciphertext = alpha_cipher.encrypt(plaintext)
186
191
  expect(alpha_cipher.decrypt(ciphertext)).to eq(plaintext)
187
192
  end
188
193
  end
194
+
195
+ describe 'text encryption support' do
196
+ it 'encrypts and decrypts simple text' do
197
+ text = 'Hello, World!'
198
+ encrypted = cipher.encrypt_text(text)
199
+ decrypted = cipher.decrypt_text(encrypted)
200
+
201
+ expect(decrypted).to eq(text)
202
+ expect(encrypted).not_to eq(text)
203
+ expect(encrypted).to match(%r{\A[A-Za-z0-9+/]+=*\z}) # base64 format
204
+ end
205
+
206
+ it 'handles text with special characters' do
207
+ text = "Special chars: !@#$%^&*()_+={[}]|\\:;\"'<,>.?/~`"
208
+ encrypted = cipher.encrypt_text(text)
209
+ decrypted = cipher.decrypt_text(encrypted)
210
+
211
+ expect(decrypted).to eq(text)
212
+ end
213
+
214
+ it 'handles Unicode text including emojis' do
215
+ text = 'Unicode: Héllo Wörld! 🌍🚀💻🔐'
216
+ encrypted = cipher.encrypt_text(text)
217
+ decrypted = cipher.decrypt_text(encrypted)
218
+
219
+ expect(decrypted).to eq(text)
220
+ end
221
+
222
+ it 'handles long text' do
223
+ text = 'This is a very long piece of text that contains multiple sentences and should test the ability of the FF1 cipher to handle large text inputs while maintaining all the security properties of format-preserving encryption. ' * 10
224
+ encrypted = cipher.encrypt_text(text)
225
+ decrypted = cipher.decrypt_text(encrypted)
226
+
227
+ expect(decrypted).to eq(text)
228
+ expect(text.length).to be > 1000 # Ensure it's actually long
229
+ end
230
+
231
+ it 'works with tweaks for text' do
232
+ text = 'Secret message'
233
+ tweak = 'context_123'
234
+
235
+ encrypted_with_tweak = cipher.encrypt_text(text, tweak)
236
+ encrypted_without_tweak = cipher.encrypt_text(text)
237
+
238
+ expect(encrypted_with_tweak).not_to eq(encrypted_without_tweak)
239
+ expect(cipher.decrypt_text(encrypted_with_tweak, tweak)).to eq(text)
240
+ end
241
+
242
+ it 'handles minimum length text (single character)' do
243
+ text = 'A'
244
+ encrypted = cipher.encrypt_text(text)
245
+ decrypted = cipher.decrypt_text(encrypted)
246
+
247
+ expect(decrypted).to eq(text)
248
+ end
249
+
250
+ it 'handles empty text edge case' do
251
+ expect { cipher.encrypt_text('') }.to raise_error(FF1::Error, 'Text cannot be empty')
252
+ expect { cipher.encrypt_text(nil) }.to raise_error(FF1::Error, 'Text cannot be nil')
253
+ end
254
+
255
+ it 'handles invalid base64 in decrypt_text' do
256
+ invalid_b64 = 'not-valid-base64!'
257
+ expect { cipher.decrypt_text(invalid_b64) }.to raise_error(FF1::Error, /Invalid base64/)
258
+ end
259
+
260
+ it 'text encryption is deterministic' do
261
+ text = 'Deterministic test'
262
+
263
+ encrypted1 = cipher.encrypt_text(text)
264
+ encrypted2 = cipher.encrypt_text(text)
265
+
266
+ expect(encrypted1).to eq(encrypted2)
267
+ end
268
+
269
+ it 'produces different ciphertext for different texts' do
270
+ text1 = 'First message'
271
+ text2 = 'Second message'
272
+
273
+ encrypted1 = cipher.encrypt_text(text1)
274
+ encrypted2 = cipher.encrypt_text(text2)
275
+
276
+ expect(encrypted1).not_to eq(encrypted2)
277
+ end
278
+
279
+ context 'with irreversible mode' do
280
+ let(:irreversible_cipher) { FF1::Cipher.new(key, 10, FF1::Modes::IRREVERSIBLE) }
281
+
282
+ it 'encrypts text in irreversible mode' do
283
+ text = 'Sensitive data to be deleted'
284
+ encrypted = irreversible_cipher.encrypt_text(text)
285
+
286
+ expect(encrypted).not_to eq(text)
287
+ expect(encrypted).to match(%r{\A[A-Za-z0-9+/]+=*\z}) # base64 format
288
+ end
289
+
290
+ it 'prevents decryption in irreversible mode' do
291
+ text = 'Cannot decrypt this'
292
+ encrypted = irreversible_cipher.encrypt_text(text)
293
+
294
+ expect { irreversible_cipher.decrypt_text(encrypted) }.to raise_error(
295
+ FF1::Error, 'Cannot decrypt text in irreversible mode - data is permanently transformed'
296
+ )
297
+ end
298
+
299
+ it 'produces consistent ciphertext for same plaintext in irreversible mode' do
300
+ text = 'Consistent irreversible'
301
+
302
+ encrypted1 = irreversible_cipher.encrypt_text(text)
303
+ encrypted2 = irreversible_cipher.encrypt_text(text)
304
+
305
+ expect(encrypted1).to eq(encrypted2)
306
+ end
307
+ end
308
+
309
+ context 'with different UTF-8 encodings' do
310
+ it 'handles text with different UTF-8 byte sequences' do
311
+ # Test various UTF-8 characters
312
+ texts = [
313
+ 'ASCII only',
314
+ 'Latin-1: café résumé',
315
+ 'Cyrillic: Привет мир',
316
+ 'Asian: 你好世界 こんにちは 안녕하세요',
317
+ 'Math symbols: ∑∞≠±×÷',
318
+ 'Emojis: 👋🌟💫✨🎉',
319
+ 'Mixed: Hello 世界! 🌍'
320
+ ]
321
+
322
+ texts.each do |text|
323
+ encrypted = cipher.encrypt_text(text)
324
+ decrypted = cipher.decrypt_text(encrypted)
325
+ expect(decrypted).to eq(text), "Failed for text: #{text}"
326
+ end
327
+ end
328
+ end
329
+ end
189
330
  end
190
- end
331
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rspec'
2
4
  require_relative '../lib/ff1'
3
5
 
@@ -11,4 +13,4 @@ RSpec.configure do |config|
11
13
  end
12
14
 
13
15
  config.shared_context_metadata_behavior = :apply_to_host_groups
14
- end
16
+ end
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ff1
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
- - FF1 Gem
7
+ - Ahmed Abdellatif
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
@@ -38,8 +38,11 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '3.0'
41
- description: A Ruby implementation of the FF1 Format Preserving Encryption algorithm
42
- from NIST SP 800-38G
41
+ description: "A Ruby implementation of the FF1 Format Preserving Encryption algorithm
42
+ from NIST SP 800-38G.\nFeatures dual-mode operation: reversible encryption for active
43
+ data and irreversible encryption \nfor GDPR compliance and secure data deletion
44
+ while maintaining format and relationships.\nNow includes full UTF-8 text encryption
45
+ support for arbitrary strings while preserving FF1 security properties.\n"
43
46
  email:
44
47
  - ahmed.abdelatife@gmail.com
45
48
  executables: []
@@ -48,6 +51,7 @@ extra_rdoc_files: []
48
51
  files:
49
52
  - CHANGELOG.md
50
53
  - DEMO.md
54
+ - DUAL_MODE.md
51
55
  - IMPLEMENTATION.md
52
56
  - LICENSE
53
57
  - README.md
@@ -61,7 +65,12 @@ files:
61
65
  homepage: https://github.com/a-abdellatif98/ff1
62
66
  licenses:
63
67
  - MIT
64
- metadata: {}
68
+ metadata:
69
+ bug_tracker_uri: https://github.com/a-abdellatif98/ff1/issues
70
+ changelog_uri: https://github.com/a-abdellatif98/ff1/blob/main/CHANGELOG.md
71
+ documentation_uri: https://github.com/a-abdellatif98/ff1/blob/main/README.md
72
+ homepage_uri: https://github.com/a-abdellatif98/ff1
73
+ source_code_uri: https://github.com/a-abdellatif98/ff1
65
74
  post_install_message:
66
75
  rdoc_options: []
67
76
  require_paths:
@@ -80,5 +89,6 @@ requirements: []
80
89
  rubygems_version: 3.4.1
81
90
  signing_key:
82
91
  specification_version: 4
83
- summary: FF1 Format Preserving Encryption implementation
92
+ summary: NIST-compliant FF1 Format Preserving Encryption with dual-mode operation
93
+ and full text support
84
94
  test_files: []