ipcrypt2 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 +10 -0
- data/LICENSE +21 -0
- data/README.md +353 -0
- data/lib/ipcrypt/deterministic.rb +64 -0
- data/lib/ipcrypt/nd.rb +307 -0
- data/lib/ipcrypt/ndx.rb +127 -0
- data/lib/ipcrypt/version.rb +5 -0
- data/lib/ipcrypt.rb +9 -0
- metadata +97 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 417ba3249b6a04ea6f104538af68b7d393f73c3b4b12ca1ca88082b7788b7617
|
4
|
+
data.tar.gz: e03947c5f01df8906342b8730a96f9f3280210951a6829206e9a37c28b5c6e70
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 258447b2adc9eb4df871cbbf69f30da0e327ecdf4e01be1ccd82150f7da58f4af816509109c3c3d054f7c67364f27829c4bee888d86999c0dec8cbac690751ab
|
7
|
+
data.tar.gz: fca823b4fd42a7c09c5833d524bc38df494e30f0a623bb5b74e9f748bc9633612d1f3089a0bb42d88190991c1bb0e940219cf2fc0d2ede7391ee589164876245
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [1.0.0] - 2025-08-30
|
9
|
+
|
10
|
+
Initial public release.
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Frank Denis
|
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,353 @@
|
|
1
|
+
# IPCrypt2 Ruby Implementation
|
2
|
+
|
3
|
+
A Ruby implementation of the IPCrypt specification for encrypting and obfuscating IP addresses, as defined in the [IPCrypt IETF draft](https://datatracker.ietf.org/doc/draft-denis-ipcrypt/).
|
4
|
+
|
5
|
+
This gem provides privacy-preserving methods for storing, logging, and analyzing IP addresses while maintaining the ability to decrypt them when necessary.
|
6
|
+
|
7
|
+
## Features
|
8
|
+
|
9
|
+
- **Three encryption modes:**
|
10
|
+
- `ipcrypt-deterministic`: Deterministic encryption using AES-128 (same input always produces same output)
|
11
|
+
- `ipcrypt-nd`: Non-deterministic encryption using KIASU-BC with 8-byte tweak
|
12
|
+
- `ipcrypt-ndx`: Non-deterministic encryption using AES-XTS with 16-byte tweak
|
13
|
+
- **Full IPv4 and IPv6 support** with automatic conversion to unified 16-byte format
|
14
|
+
- **Secure implementations** using OpenSSL for cryptographic operations
|
15
|
+
- **Comprehensive test suite** with official test vectors from the specification
|
16
|
+
- **Ruby 2.6+ compatibility**
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Add this line to your application's Gemfile:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'ipcrypt2'
|
24
|
+
```
|
25
|
+
|
26
|
+
And then execute:
|
27
|
+
|
28
|
+
```bash
|
29
|
+
$ bundle install
|
30
|
+
```
|
31
|
+
|
32
|
+
Or install it yourself as:
|
33
|
+
|
34
|
+
```bash
|
35
|
+
$ gem install ipcrypt2
|
36
|
+
```
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
### Deterministic Encryption (ipcrypt-deterministic)
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
require 'ipcrypt/deterministic'
|
44
|
+
|
45
|
+
# 16-byte key
|
46
|
+
key = "0123456789abcdeffedcba9876543210".scan(/../).map { |x| x.hex }.pack("C*")
|
47
|
+
|
48
|
+
# Encrypt an IP address
|
49
|
+
ip = "192.0.2.1"
|
50
|
+
encrypted_ip = IPCrypt::Deterministic.encrypt(ip, key)
|
51
|
+
decrypted_ip = IPCrypt::Deterministic.decrypt(encrypted_ip, key)
|
52
|
+
```
|
53
|
+
|
54
|
+
### Non-Deterministic Encryption with KIASU-BC (ipcrypt-nd)
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
require 'ipcrypt/nd'
|
58
|
+
|
59
|
+
# 16-byte key
|
60
|
+
key = "0123456789abcdeffedcba9876543210".scan(/../).map { |x| x.hex }.pack("C*")
|
61
|
+
|
62
|
+
# Encrypt an IP address (with optional tweak)
|
63
|
+
ip = "192.0.2.1"
|
64
|
+
encrypted_data = IPCrypt::ND.encrypt(ip, key) # Random tweak
|
65
|
+
# or
|
66
|
+
tweak = "08e0c289bff23b7c".scan(/../).map { |x| x.hex }.pack("C*")
|
67
|
+
encrypted_data = IPCrypt::ND.encrypt(ip, key, tweak) # Specific tweak
|
68
|
+
|
69
|
+
# Decrypt
|
70
|
+
decrypted_ip = IPCrypt::ND.decrypt(encrypted_data, key)
|
71
|
+
```
|
72
|
+
|
73
|
+
### Non-Deterministic Encryption with AES-XTS (ipcrypt-ndx)
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
require 'ipcrypt/ndx'
|
77
|
+
|
78
|
+
# 32-byte key
|
79
|
+
key = "0123456789abcdeffedcba98765432101032547698badcfeefcdab8967452301".scan(/../).map { |x| x.hex }.pack("C*")
|
80
|
+
|
81
|
+
# Encrypt an IP address
|
82
|
+
ip = "192.0.2.1"
|
83
|
+
encrypted_data = IPCrypt::NDX.encrypt(ip, key)
|
84
|
+
|
85
|
+
# Decrypt
|
86
|
+
decrypted_ip = IPCrypt::NDX.decrypt(encrypted_data, key)
|
87
|
+
```
|
88
|
+
|
89
|
+
## Framework Integration
|
90
|
+
|
91
|
+
### Ruby on Rails
|
92
|
+
|
93
|
+
#### 1. Basic Setup
|
94
|
+
|
95
|
+
Add to your `Gemfile`:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
gem 'ipcrypt2'
|
99
|
+
```
|
100
|
+
|
101
|
+
Create an initializer `config/initializers/ipcrypt.rb`:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
# Store your key securely using Rails credentials
|
105
|
+
Rails.application.config.ipcrypt_key = Rails.application.credentials.ipcrypt_key
|
106
|
+
|
107
|
+
# Or use environment variables
|
108
|
+
Rails.application.config.ipcrypt_key = ENV['IPCRYPT_KEY'].scan(/../).map { |x| x.hex }.pack("C*")
|
109
|
+
```
|
110
|
+
|
111
|
+
#### 2. ActiveRecord Model Integration
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
class User < ApplicationRecord
|
115
|
+
# Store encrypted IP addresses in the database
|
116
|
+
|
117
|
+
def ip_address=(ip)
|
118
|
+
key = Rails.application.config.ipcrypt_key
|
119
|
+
self.encrypted_ip = IPCrypt::Deterministic.encrypt(ip, key)
|
120
|
+
end
|
121
|
+
|
122
|
+
def ip_address
|
123
|
+
return nil unless encrypted_ip.present?
|
124
|
+
key = Rails.application.config.ipcrypt_key
|
125
|
+
IPCrypt::Deterministic.decrypt(encrypted_ip, key)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
#### 3. Request Logging
|
131
|
+
|
132
|
+
Create a concern for controllers:
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
# app/controllers/concerns/ip_encryption.rb
|
136
|
+
module IpEncryption
|
137
|
+
extend ActiveSupport::Concern
|
138
|
+
|
139
|
+
included do
|
140
|
+
before_action :store_encrypted_ip
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def store_encrypted_ip
|
146
|
+
return unless request.remote_ip.present?
|
147
|
+
|
148
|
+
key = Rails.application.config.ipcrypt_key
|
149
|
+
encrypted_ip = IPCrypt::Deterministic.encrypt(request.remote_ip, key)
|
150
|
+
|
151
|
+
# Store in session, database, or logs
|
152
|
+
session[:encrypted_ip] = encrypted_ip
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Use in controllers
|
157
|
+
class ApplicationController < ActionController::Base
|
158
|
+
include IpEncryption
|
159
|
+
end
|
160
|
+
```
|
161
|
+
|
162
|
+
#### 4. Custom Logger
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
# config/application.rb
|
166
|
+
class EncryptedIpLogger < ActiveSupport::Logger
|
167
|
+
def add_ip(severity, ip_address, progname = nil)
|
168
|
+
key = Rails.application.config.ipcrypt_key
|
169
|
+
encrypted_ip = IPCrypt::Deterministic.encrypt(ip_address, key)
|
170
|
+
add(severity, "IP: #{encrypted_ip}", progname)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Usage
|
175
|
+
Rails.logger.add_ip(Logger::INFO, request.remote_ip)
|
176
|
+
```
|
177
|
+
|
178
|
+
### Sinatra
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
require 'sinatra'
|
182
|
+
require 'ipcrypt/deterministic'
|
183
|
+
|
184
|
+
configure do
|
185
|
+
set :ipcrypt_key, ENV['IPCRYPT_KEY'].scan(/../).map { |x| x.hex }.pack("C*")
|
186
|
+
end
|
187
|
+
|
188
|
+
helpers do
|
189
|
+
def encrypt_ip(ip)
|
190
|
+
IPCrypt::Deterministic.encrypt(ip, settings.ipcrypt_key)
|
191
|
+
end
|
192
|
+
|
193
|
+
def decrypt_ip(encrypted_ip)
|
194
|
+
IPCrypt::Deterministic.decrypt(encrypted_ip, settings.ipcrypt_key)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
before do
|
199
|
+
@encrypted_client_ip = encrypt_ip(request.ip)
|
200
|
+
end
|
201
|
+
|
202
|
+
get '/' do
|
203
|
+
"Your encrypted IP: #{@encrypted_client_ip}"
|
204
|
+
end
|
205
|
+
```
|
206
|
+
|
207
|
+
### Rack Middleware
|
208
|
+
|
209
|
+
Create middleware for any Rack-based application:
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
# lib/rack/ip_encryptor.rb
|
213
|
+
module Rack
|
214
|
+
class IpEncryptor
|
215
|
+
def initialize(app, key)
|
216
|
+
@app = app
|
217
|
+
@key = key
|
218
|
+
end
|
219
|
+
|
220
|
+
def call(env)
|
221
|
+
# Encrypt the client IP
|
222
|
+
if env['REMOTE_ADDR']
|
223
|
+
env['rack.encrypted_ip'] = IPCrypt::Deterministic.encrypt(env['REMOTE_ADDR'], @key)
|
224
|
+
end
|
225
|
+
|
226
|
+
@app.call(env)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Usage in config.ru
|
232
|
+
require 'ipcrypt/deterministic'
|
233
|
+
|
234
|
+
key = ENV['IPCRYPT_KEY'].scan(/../).map { |x| x.hex }.pack("C*")
|
235
|
+
use Rack::IpEncryptor, key
|
236
|
+
run YourApp
|
237
|
+
```
|
238
|
+
|
239
|
+
### Hanami
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
# config/environment.rb
|
243
|
+
require 'ipcrypt/deterministic'
|
244
|
+
|
245
|
+
Hanami.configure do
|
246
|
+
# Store key in settings
|
247
|
+
settings.ipcrypt_key = ENV['IPCRYPT_KEY'].scan(/../).map { |x| x.hex }.pack("C*")
|
248
|
+
end
|
249
|
+
|
250
|
+
# app/actions/application.rb
|
251
|
+
module YourApp
|
252
|
+
module Actions
|
253
|
+
class Application < Hanami::Action
|
254
|
+
before :encrypt_client_ip
|
255
|
+
|
256
|
+
private
|
257
|
+
|
258
|
+
def encrypt_client_ip
|
259
|
+
return unless request.ip
|
260
|
+
|
261
|
+
key = Hanami.app.settings.ipcrypt_key
|
262
|
+
@encrypted_ip = IPCrypt::Deterministic.encrypt(request.ip, key)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
```
|
268
|
+
|
269
|
+
### Grape API
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
require 'grape'
|
273
|
+
require 'ipcrypt/deterministic'
|
274
|
+
|
275
|
+
class API < Grape::API
|
276
|
+
helpers do
|
277
|
+
def ipcrypt_key
|
278
|
+
@key ||= ENV['IPCRYPT_KEY'].scan(/../).map { |x| x.hex }.pack("C*")
|
279
|
+
end
|
280
|
+
|
281
|
+
def encrypted_client_ip
|
282
|
+
IPCrypt::Deterministic.encrypt(request.ip, ipcrypt_key)
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
before do
|
287
|
+
# Log encrypted IP for each request
|
288
|
+
logger.info "Request from: #{encrypted_client_ip}"
|
289
|
+
end
|
290
|
+
|
291
|
+
get '/my-ip' do
|
292
|
+
{ encrypted_ip: encrypted_client_ip }
|
293
|
+
end
|
294
|
+
end
|
295
|
+
```
|
296
|
+
|
297
|
+
## Best Practices
|
298
|
+
|
299
|
+
1. **Key Management**: Never hardcode encryption keys. Use environment variables or secure credential stores like Rails credentials.
|
300
|
+
|
301
|
+
2. **Choose the Right Mode**:
|
302
|
+
- Use `deterministic` for logs and analytics where you need to correlate multiple requests from the same IP
|
303
|
+
- Use `nd` or `ndx` for storage where each encryption should be unique
|
304
|
+
|
305
|
+
3. **Performance**: For high-traffic applications, consider caching the key parsing:
|
306
|
+
```ruby
|
307
|
+
class IpEncryptor
|
308
|
+
def self.key
|
309
|
+
@key ||= ENV['IPCRYPT_KEY'].scan(/../).map { |x| x.hex }.pack("C*")
|
310
|
+
end
|
311
|
+
end
|
312
|
+
```
|
313
|
+
|
314
|
+
4. **Database Storage**: Store encrypted IPs as binary or base64-encoded strings:
|
315
|
+
```ruby
|
316
|
+
# Migration
|
317
|
+
add_column :users, :encrypted_ip, :binary
|
318
|
+
|
319
|
+
# Model
|
320
|
+
def ip_address=(ip)
|
321
|
+
self.encrypted_ip = IPCrypt::Deterministic.encrypt(ip, key)
|
322
|
+
end
|
323
|
+
```
|
324
|
+
|
325
|
+
## Development
|
326
|
+
|
327
|
+
After checking out the repo, run `bundle install` to install dependencies.
|
328
|
+
|
329
|
+
### Running Tests
|
330
|
+
|
331
|
+
```bash
|
332
|
+
# Run all tests
|
333
|
+
bundle exec rake test
|
334
|
+
|
335
|
+
# Run tests with verbose output
|
336
|
+
bundle exec rake test_verbose
|
337
|
+
|
338
|
+
# Run RuboCop linting
|
339
|
+
bundle exec rubocop
|
340
|
+
|
341
|
+
# Run both tests and linting (default task)
|
342
|
+
bundle exec rake
|
343
|
+
```
|
344
|
+
|
345
|
+
### Building the Gem
|
346
|
+
|
347
|
+
```bash
|
348
|
+
# Build the gem
|
349
|
+
gem build ipcrypt2.gemspec
|
350
|
+
|
351
|
+
# Install locally for testing
|
352
|
+
gem install ./ipcrypt2-*.gem
|
353
|
+
```
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ipaddr'
|
4
|
+
require 'openssl'
|
5
|
+
|
6
|
+
module IPCrypt
|
7
|
+
# Implementation of ipcrypt-deterministic using AES-128
|
8
|
+
class Deterministic
|
9
|
+
# Convert an IP address to its 16-byte representation
|
10
|
+
def self.ip_to_bytes(ip)
|
11
|
+
ip_addr = ip.is_a?(String) ? IPAddr.new(ip) : ip
|
12
|
+
if ip_addr.ipv4?
|
13
|
+
# Convert IPv4 to IPv4-mapped IPv6 format (::ffff:0:0/96)
|
14
|
+
bytes = [0] * 10 + [0xff, 0xff] + ip_addr.hton.bytes
|
15
|
+
bytes.pack('C*').force_encoding('BINARY')
|
16
|
+
else
|
17
|
+
ip_addr.hton.force_encoding('BINARY')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Convert a 16-byte representation back to an IP address
|
22
|
+
def self.bytes_to_ip(bytes16)
|
23
|
+
raise InvalidDataError, 'Input must be 16 bytes' unless bytes16.length == 16
|
24
|
+
|
25
|
+
# Check for IPv4-mapped IPv6 format
|
26
|
+
zero_bytes = [0] * 10
|
27
|
+
ff_bytes = [255, 255]
|
28
|
+
|
29
|
+
if bytes16[0, 10].bytes == zero_bytes && bytes16[10, 2].bytes == ff_bytes
|
30
|
+
IPAddr.new_ntoh(bytes16[12, 4])
|
31
|
+
else
|
32
|
+
IPAddr.new_ntoh(bytes16)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Encrypt an IP address using AES-128
|
37
|
+
def self.encrypt(ip, key)
|
38
|
+
raise InvalidKeyError, 'Key must be 16 bytes' unless key.length == 16
|
39
|
+
|
40
|
+
plaintext = ip_to_bytes(ip)
|
41
|
+
cipher = OpenSSL::Cipher.new('AES-128-ECB')
|
42
|
+
cipher.encrypt
|
43
|
+
cipher.padding = 0 # Disable padding for exact 16-byte blocks
|
44
|
+
cipher.key = key
|
45
|
+
ciphertext = cipher.update(plaintext) + cipher.final
|
46
|
+
|
47
|
+
bytes_to_ip(ciphertext)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Decrypt an IP address using AES-128
|
51
|
+
def self.decrypt(ip, key)
|
52
|
+
raise InvalidKeyError, 'Key must be 16 bytes' unless key.length == 16
|
53
|
+
|
54
|
+
ciphertext = ip_to_bytes(ip)
|
55
|
+
cipher = OpenSSL::Cipher.new('AES-128-ECB')
|
56
|
+
cipher.decrypt
|
57
|
+
cipher.padding = 0 # Disable padding for exact 16-byte blocks
|
58
|
+
cipher.key = key
|
59
|
+
plaintext = cipher.update(ciphertext) + cipher.final
|
60
|
+
|
61
|
+
bytes_to_ip(plaintext)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/ipcrypt/nd.rb
ADDED
@@ -0,0 +1,307 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ipaddr'
|
4
|
+
require 'openssl'
|
5
|
+
|
6
|
+
module IPCrypt
|
7
|
+
# Implementation of ipcrypt-nd using KIASU-BC
|
8
|
+
class ND
|
9
|
+
# AES S-box and inverse S-box
|
10
|
+
SBOX = [
|
11
|
+
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
|
12
|
+
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
|
13
|
+
0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
|
14
|
+
0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
|
15
|
+
0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
|
16
|
+
0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
|
17
|
+
0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
|
18
|
+
0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
|
19
|
+
0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
|
20
|
+
0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
|
21
|
+
0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
|
22
|
+
0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
|
23
|
+
0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
|
24
|
+
0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
|
25
|
+
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
|
26
|
+
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16
|
27
|
+
].freeze
|
28
|
+
|
29
|
+
INV_SBOX = [
|
30
|
+
0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
|
31
|
+
0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
|
32
|
+
0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
|
33
|
+
0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
|
34
|
+
0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
|
35
|
+
0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
|
36
|
+
0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
|
37
|
+
0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
|
38
|
+
0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
|
39
|
+
0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
|
40
|
+
0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
|
41
|
+
0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
|
42
|
+
0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
|
43
|
+
0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
|
44
|
+
0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
|
45
|
+
0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d
|
46
|
+
].freeze
|
47
|
+
|
48
|
+
# AES round constants
|
49
|
+
RCON = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36].freeze
|
50
|
+
|
51
|
+
# Precomputed multiplication tables for AES operations
|
52
|
+
MUL2 = (0..255).map do |x|
|
53
|
+
((x << 1) & 0xFF) ^ (x & 0x80 != 0 ? 0x1B : 0)
|
54
|
+
end.freeze
|
55
|
+
|
56
|
+
MUL3 = (0..255).map do |x|
|
57
|
+
MUL2[x] ^ x
|
58
|
+
end.freeze
|
59
|
+
|
60
|
+
# Specialized GF multiplication by 0x09 (x^3 + 1)
|
61
|
+
def self.gf_mul_09(a)
|
62
|
+
# 0x09 = x^3 + 1 = MUL2[MUL2[MUL2[a]]] ^ a
|
63
|
+
MUL2[MUL2[MUL2[a]]] ^ a
|
64
|
+
end
|
65
|
+
|
66
|
+
# Specialized GF multiplication by 0x0B (x^3 + x + 1)
|
67
|
+
def self.gf_mul_0b(a)
|
68
|
+
# 0x0B = x^3 + x + 1 = MUL2[MUL2[MUL2[a]]] ^ MUL2[a] ^ a
|
69
|
+
MUL2[MUL2[MUL2[a]]] ^ MUL2[a] ^ a
|
70
|
+
end
|
71
|
+
|
72
|
+
# Specialized GF multiplication by 0x0D (x^3 + x^2 + 1)
|
73
|
+
def self.gf_mul_0d(a)
|
74
|
+
# 0x0D = x^3 + x^2 + 1 = MUL2[MUL2[MUL2[a]]] ^ MUL2[MUL2[a]] ^ a
|
75
|
+
MUL2[MUL2[MUL2[a]]] ^ MUL2[MUL2[a]] ^ a
|
76
|
+
end
|
77
|
+
|
78
|
+
# Specialized GF multiplication by 0x0E (x^3 + x^2 + x)
|
79
|
+
def self.gf_mul_0e(a)
|
80
|
+
# 0x0E = x^3 + x^2 + x = MUL2[MUL2[MUL2[a]]] ^ MUL2[MUL2[a]] ^ MUL2[a]
|
81
|
+
MUL2[MUL2[MUL2[a]]] ^ MUL2[MUL2[a]] ^ MUL2[a]
|
82
|
+
end
|
83
|
+
|
84
|
+
# Convert an IP address to its 16-byte representation
|
85
|
+
def self.ip_to_bytes(ip)
|
86
|
+
ip_addr = ip.is_a?(String) ? IPAddr.new(ip) : ip
|
87
|
+
if ip_addr.ipv4?
|
88
|
+
# Convert IPv4 to IPv4-mapped IPv6 format (::ffff:0:0/96)
|
89
|
+
bytes = [0] * 10 + [0xff, 0xff] + ip_addr.hton.bytes
|
90
|
+
bytes.pack('C*').force_encoding('BINARY')
|
91
|
+
else
|
92
|
+
ip_addr.hton.force_encoding('BINARY')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Convert a 16-byte representation back to an IP address
|
97
|
+
def self.bytes_to_ip(bytes16)
|
98
|
+
raise InvalidDataError, 'Input must be 16 bytes' unless bytes16.length == 16
|
99
|
+
|
100
|
+
# Check for IPv4-mapped IPv6 format
|
101
|
+
zero_bytes = [0] * 10
|
102
|
+
ff_bytes = [255, 255]
|
103
|
+
|
104
|
+
if bytes16[0, 10].bytes == zero_bytes && bytes16[10, 2].bytes == ff_bytes
|
105
|
+
IPAddr.new_ntoh(bytes16[12, 4])
|
106
|
+
else
|
107
|
+
IPAddr.new_ntoh(bytes16)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Rotate a 4-byte word
|
112
|
+
def self.rot_word(word)
|
113
|
+
word[1..] + word[0]
|
114
|
+
end
|
115
|
+
|
116
|
+
# Generate AES round keys
|
117
|
+
def self.expand_key(key)
|
118
|
+
raise InvalidKeyError, 'Key must be 16 bytes' unless key.length == 16
|
119
|
+
|
120
|
+
round_keys = [key]
|
121
|
+
10.times do |i|
|
122
|
+
prev_key = round_keys.last
|
123
|
+
temp = prev_key[-4..]
|
124
|
+
temp = rot_word(temp)
|
125
|
+
temp = temp.bytes.map { |b| SBOX[b] }.pack('C*')
|
126
|
+
temp[0] = (temp[0].ord ^ RCON[i]).chr
|
127
|
+
|
128
|
+
new_key = ''
|
129
|
+
4.times do |j|
|
130
|
+
word = prev_key[j * 4, 4]
|
131
|
+
word = if j.zero?
|
132
|
+
word.bytes.zip(temp.bytes).map { |a, b| a ^ b }.pack('C*')
|
133
|
+
else
|
134
|
+
word.bytes.zip(new_key[(j - 1) * 4, 4].bytes).map { |a, b| a ^ b }.pack('C*')
|
135
|
+
end
|
136
|
+
new_key += word
|
137
|
+
end
|
138
|
+
round_keys << new_key
|
139
|
+
end
|
140
|
+
|
141
|
+
round_keys
|
142
|
+
end
|
143
|
+
|
144
|
+
# Pad an 8-byte tweak to 16 bytes by placing each 2-byte pair at the start of each 4-byte group
|
145
|
+
def self.pad_tweak(tweak)
|
146
|
+
raise InvalidTweakError, 'Tweak must be 8 bytes' unless tweak.length == 8
|
147
|
+
|
148
|
+
padded_bytes = [0] * 16
|
149
|
+
4.times do |i|
|
150
|
+
padded_bytes[i * 4] = tweak[i * 2].ord
|
151
|
+
padded_bytes[i * 4 + 1] = tweak[i * 2 + 1].ord
|
152
|
+
# padded_bytes[i * 4 + 2] and padded_bytes[i * 4 + 3] are already 0
|
153
|
+
end
|
154
|
+
padded_bytes.pack('C*').force_encoding('BINARY')
|
155
|
+
end
|
156
|
+
|
157
|
+
# Perform AES SubBytes operation
|
158
|
+
def self.sub_bytes(state)
|
159
|
+
state.bytes.map { |b| SBOX[b] }.pack('C*')
|
160
|
+
end
|
161
|
+
|
162
|
+
# Perform inverse AES SubBytes operation
|
163
|
+
def self.inv_sub_bytes(state)
|
164
|
+
state.bytes.map { |b| INV_SBOX[b] }.pack('C*')
|
165
|
+
end
|
166
|
+
|
167
|
+
# Perform AES ShiftRows operation
|
168
|
+
def self.shift_rows(state)
|
169
|
+
bytes = state.bytes
|
170
|
+
[
|
171
|
+
bytes[0], bytes[5], bytes[10], bytes[15], bytes[4], bytes[9], bytes[14], bytes[3],
|
172
|
+
bytes[8], bytes[13], bytes[2], bytes[7], bytes[12], bytes[1], bytes[6], bytes[11]
|
173
|
+
].pack('C*')
|
174
|
+
end
|
175
|
+
|
176
|
+
# Perform inverse AES ShiftRows operation
|
177
|
+
def self.inv_shift_rows(state)
|
178
|
+
bytes = state.bytes
|
179
|
+
[
|
180
|
+
bytes[0], bytes[13], bytes[10], bytes[7], bytes[4], bytes[1], bytes[14], bytes[11],
|
181
|
+
bytes[8], bytes[5], bytes[2], bytes[15], bytes[12], bytes[9], bytes[6], bytes[3]
|
182
|
+
].pack('C*')
|
183
|
+
end
|
184
|
+
|
185
|
+
# Perform AES MixColumns operation
|
186
|
+
def self.mix_columns(state)
|
187
|
+
new_state = []
|
188
|
+
4.times do |i|
|
189
|
+
s = state[i * 4, 4].bytes
|
190
|
+
s0, s1, s2, s3 = s
|
191
|
+
new_state[i * 4] = MUL2[s0] ^ MUL3[s1] ^ s2 ^ s3
|
192
|
+
new_state[i * 4 + 1] = s0 ^ MUL2[s1] ^ MUL3[s2] ^ s3
|
193
|
+
new_state[i * 4 + 2] = s0 ^ s1 ^ MUL2[s2] ^ MUL3[s3]
|
194
|
+
new_state[i * 4 + 3] = MUL3[s0] ^ s1 ^ s2 ^ MUL2[s3]
|
195
|
+
end
|
196
|
+
new_state.pack('C*')
|
197
|
+
end
|
198
|
+
|
199
|
+
# Perform inverse AES MixColumns operation
|
200
|
+
def self.inv_mix_columns(state)
|
201
|
+
new_state = []
|
202
|
+
4.times do |i|
|
203
|
+
col = state[i * 4, 4].bytes
|
204
|
+
result = [
|
205
|
+
gf_mul_0e(col[0]) ^ gf_mul_0b(col[1]) ^ gf_mul_0d(col[2]) ^ gf_mul_09(col[3]),
|
206
|
+
gf_mul_09(col[0]) ^ gf_mul_0e(col[1]) ^ gf_mul_0b(col[2]) ^ gf_mul_0d(col[3]),
|
207
|
+
gf_mul_0d(col[0]) ^ gf_mul_09(col[1]) ^ gf_mul_0e(col[2]) ^ gf_mul_0b(col[3]),
|
208
|
+
gf_mul_0b(col[0]) ^ gf_mul_0d(col[1]) ^ gf_mul_09(col[2]) ^ gf_mul_0e(col[3])
|
209
|
+
]
|
210
|
+
new_state += result
|
211
|
+
end
|
212
|
+
new_state.pack('C*')
|
213
|
+
end
|
214
|
+
|
215
|
+
# Encrypt using KIASU-BC construction
|
216
|
+
def self.kiasu_bc_encrypt(key, tweak, plaintext)
|
217
|
+
raise InvalidKeyError, 'Key must be 16 bytes' unless key.length == 16
|
218
|
+
raise InvalidTweakError, 'Tweak must be 8 bytes' unless tweak.length == 8
|
219
|
+
raise InvalidDataError, 'Plaintext must be 16 bytes' unless plaintext.length == 16
|
220
|
+
|
221
|
+
round_keys = expand_key(key)
|
222
|
+
padded_tweak = pad_tweak(tweak)
|
223
|
+
|
224
|
+
# XOR plaintext with round key and padded tweak
|
225
|
+
state = plaintext.bytes.zip(round_keys[0].bytes, padded_tweak.bytes)
|
226
|
+
.map { |p, k, t| p ^ k ^ t }.pack('C*')
|
227
|
+
|
228
|
+
9.times do |i|
|
229
|
+
state = sub_bytes(state)
|
230
|
+
state = shift_rows(state)
|
231
|
+
state = mix_columns(state)
|
232
|
+
# XOR with round key and padded tweak
|
233
|
+
state = state.bytes.zip(round_keys[i + 1].bytes, padded_tweak.bytes)
|
234
|
+
.map { |s, k, t| s ^ k ^ t }.pack('C*')
|
235
|
+
end
|
236
|
+
|
237
|
+
state = sub_bytes(state)
|
238
|
+
state = shift_rows(state)
|
239
|
+
# Final round - XOR with round key and padded tweak
|
240
|
+
state.bytes.zip(round_keys[10].bytes, padded_tweak.bytes)
|
241
|
+
.map { |s, k, t| s ^ k ^ t }.pack('C*')
|
242
|
+
end
|
243
|
+
|
244
|
+
# Decrypt using KIASU-BC construction
|
245
|
+
def self.kiasu_bc_decrypt(key, tweak, ciphertext)
|
246
|
+
raise InvalidKeyError, 'Key must be 16 bytes' unless key.length == 16
|
247
|
+
raise InvalidTweakError, 'Tweak must be 8 bytes' unless tweak.length == 8
|
248
|
+
raise InvalidDataError, 'Ciphertext must be 16 bytes' unless ciphertext.length == 16
|
249
|
+
|
250
|
+
round_keys = expand_key(key)
|
251
|
+
padded_tweak = pad_tweak(tweak)
|
252
|
+
|
253
|
+
# Initial operations
|
254
|
+
state = ciphertext.bytes.zip(round_keys[10].bytes, padded_tweak.bytes)
|
255
|
+
.map { |c, k, t| c ^ k ^ t }.pack('C*')
|
256
|
+
state = inv_shift_rows(state)
|
257
|
+
state = inv_sub_bytes(state)
|
258
|
+
|
259
|
+
9.downto(1) do |i|
|
260
|
+
# XOR with round key and padded tweak
|
261
|
+
state = state.bytes.zip(round_keys[i].bytes, padded_tweak.bytes)
|
262
|
+
.map { |s, k, t| s ^ k ^ t }.pack('C*')
|
263
|
+
state = inv_mix_columns(state)
|
264
|
+
state = inv_shift_rows(state)
|
265
|
+
state = inv_sub_bytes(state)
|
266
|
+
end
|
267
|
+
|
268
|
+
# Final round - XOR with round key and padded tweak
|
269
|
+
state.bytes.zip(round_keys[0].bytes, padded_tweak.bytes)
|
270
|
+
.map { |s, k, t| s ^ k ^ t }.pack('C*')
|
271
|
+
end
|
272
|
+
|
273
|
+
# Encrypt an IP address using ipcrypt-nd
|
274
|
+
def self.encrypt(ip_address, key, tweak = nil)
|
275
|
+
# Convert IP to bytes
|
276
|
+
ip_bytes = ip_to_bytes(ip_address)
|
277
|
+
|
278
|
+
# Use provided tweak or generate random 8-byte tweak
|
279
|
+
if tweak.nil?
|
280
|
+
tweak = OpenSSL::Random.random_bytes(8)
|
281
|
+
elsif tweak.length != 8
|
282
|
+
raise InvalidTweakError, 'Tweak must be 8 bytes'
|
283
|
+
end
|
284
|
+
|
285
|
+
# Encrypt using KIASU-BC
|
286
|
+
ciphertext = kiasu_bc_encrypt(key, tweak, ip_bytes)
|
287
|
+
|
288
|
+
# Return tweak || ciphertext
|
289
|
+
tweak + ciphertext
|
290
|
+
end
|
291
|
+
|
292
|
+
# Decrypt an IP address using ipcrypt-nd
|
293
|
+
def self.decrypt(encrypted_data, key)
|
294
|
+
raise InvalidDataError, 'Encrypted data must be 24 bytes' unless encrypted_data.length == 24
|
295
|
+
|
296
|
+
# Split into tweak and ciphertext
|
297
|
+
tweak = encrypted_data[0, 8]
|
298
|
+
ciphertext = encrypted_data[8, 16]
|
299
|
+
|
300
|
+
# Decrypt using KIASU-BC
|
301
|
+
ip_bytes = kiasu_bc_decrypt(key, tweak, ciphertext)
|
302
|
+
|
303
|
+
# Convert back to IP address
|
304
|
+
bytes_to_ip(ip_bytes)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
data/lib/ipcrypt/ndx.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ipaddr'
|
4
|
+
require 'openssl'
|
5
|
+
|
6
|
+
module IPCrypt
|
7
|
+
# Implementation of ipcrypt-ndx using AES-XTS with a 16-byte tweak
|
8
|
+
class NDX
|
9
|
+
# Convert an IP address to its 16-byte representation
|
10
|
+
def self.ip_to_bytes(ip)
|
11
|
+
ip_addr = ip.is_a?(String) ? IPAddr.new(ip) : ip
|
12
|
+
if ip_addr.ipv4?
|
13
|
+
# Convert IPv4 to IPv4-mapped IPv6 format (::ffff:0:0/96)
|
14
|
+
bytes = [0] * 10 + [0xff, 0xff] + ip_addr.hton.bytes
|
15
|
+
bytes.pack('C*').force_encoding('BINARY')
|
16
|
+
else
|
17
|
+
ip_addr.hton.force_encoding('BINARY')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Convert a 16-byte representation back to an IP address
|
22
|
+
def self.bytes_to_ip(bytes16)
|
23
|
+
raise InvalidDataError, 'Input must be 16 bytes' unless bytes16.length == 16
|
24
|
+
|
25
|
+
# Check for IPv4-mapped IPv6 format
|
26
|
+
zero_bytes = [0] * 10
|
27
|
+
ff_bytes = [255, 255]
|
28
|
+
|
29
|
+
if bytes16[0, 10].bytes == zero_bytes && bytes16[10, 2].bytes == ff_bytes
|
30
|
+
IPAddr.new_ntoh(bytes16[12, 4])
|
31
|
+
else
|
32
|
+
IPAddr.new_ntoh(bytes16)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Encrypt using AES-XTS construction
|
37
|
+
def self.aes_xts_encrypt(key, tweak, plaintext)
|
38
|
+
raise InvalidKeyError, 'Key must be 32 bytes' unless key.length == 32
|
39
|
+
raise InvalidTweakError, 'Tweak must be 16 bytes' unless tweak.length == 16
|
40
|
+
raise InvalidDataError, 'Plaintext must be 16 bytes' unless plaintext.length == 16
|
41
|
+
|
42
|
+
# Split key into two 16-byte keys
|
43
|
+
k1 = key[0, 16]
|
44
|
+
k2 = key[16, 16]
|
45
|
+
|
46
|
+
# Encrypt tweak with second key
|
47
|
+
cipher2 = OpenSSL::Cipher.new('AES-128-ECB')
|
48
|
+
cipher2.encrypt
|
49
|
+
cipher2.padding = 0 # Disable padding
|
50
|
+
cipher2.key = k2
|
51
|
+
et = cipher2.update(tweak) + cipher2.final
|
52
|
+
|
53
|
+
# XOR plaintext with encrypted tweak
|
54
|
+
xored = plaintext.bytes.zip(et.bytes).map { |a, b| a ^ b }.pack('C*')
|
55
|
+
|
56
|
+
# Encrypt with first key
|
57
|
+
cipher1 = OpenSSL::Cipher.new('AES-128-ECB')
|
58
|
+
cipher1.encrypt
|
59
|
+
cipher1.padding = 0 # Disable padding
|
60
|
+
cipher1.key = k1
|
61
|
+
encrypted = cipher1.update(xored) + cipher1.final
|
62
|
+
|
63
|
+
# XOR result with encrypted tweak
|
64
|
+
encrypted.bytes.zip(et.bytes).map { |a, b| a ^ b }.pack('C*')
|
65
|
+
end
|
66
|
+
|
67
|
+
# Decrypt using AES-XTS construction
|
68
|
+
def self.aes_xts_decrypt(key, tweak, ciphertext)
|
69
|
+
raise InvalidKeyError, 'Key must be 32 bytes' unless key.length == 32
|
70
|
+
raise InvalidTweakError, 'Tweak must be 16 bytes' unless tweak.length == 16
|
71
|
+
raise InvalidDataError, 'Ciphertext must be 16 bytes' unless ciphertext.length == 16
|
72
|
+
|
73
|
+
# Split key into two 16-byte keys
|
74
|
+
k1 = key[0, 16]
|
75
|
+
k2 = key[16, 16]
|
76
|
+
|
77
|
+
# Encrypt tweak with second key
|
78
|
+
cipher2 = OpenSSL::Cipher.new('AES-128-ECB')
|
79
|
+
cipher2.encrypt
|
80
|
+
cipher2.padding = 0 # Disable padding
|
81
|
+
cipher2.key = k2
|
82
|
+
et = cipher2.update(tweak) + cipher2.final
|
83
|
+
|
84
|
+
# XOR ciphertext with encrypted tweak
|
85
|
+
xored = ciphertext.bytes.zip(et.bytes).map { |a, b| a ^ b }.pack('C*')
|
86
|
+
|
87
|
+
# Decrypt with first key
|
88
|
+
cipher1 = OpenSSL::Cipher.new('AES-128-ECB')
|
89
|
+
cipher1.decrypt
|
90
|
+
cipher1.padding = 0 # Disable padding
|
91
|
+
cipher1.key = k1
|
92
|
+
decrypted = cipher1.update(xored) + cipher1.final
|
93
|
+
|
94
|
+
# XOR result with encrypted tweak
|
95
|
+
decrypted.bytes.zip(et.bytes).map { |a, b| a ^ b }.pack('C*')
|
96
|
+
end
|
97
|
+
|
98
|
+
# Encrypt an IP address using AES-XTS
|
99
|
+
def self.encrypt(ip, key)
|
100
|
+
raise InvalidKeyError, 'Key must be 32 bytes' unless key.length == 32
|
101
|
+
|
102
|
+
# Generate random 16-byte tweak
|
103
|
+
tweak = OpenSSL::Random.random_bytes(16)
|
104
|
+
|
105
|
+
# Convert IP to bytes and encrypt
|
106
|
+
plaintext = ip_to_bytes(ip)
|
107
|
+
ciphertext = aes_xts_encrypt(key, tweak, plaintext)
|
108
|
+
|
109
|
+
# Return tweak || ciphertext
|
110
|
+
tweak + ciphertext
|
111
|
+
end
|
112
|
+
|
113
|
+
# Decrypt a binary output using AES-XTS
|
114
|
+
def self.decrypt(binary_output, key)
|
115
|
+
raise InvalidKeyError, 'Key must be 32 bytes' unless key.length == 32
|
116
|
+
raise InvalidDataError, 'Binary output must be 32 bytes' unless binary_output.length == 32
|
117
|
+
|
118
|
+
# Split into tweak and ciphertext
|
119
|
+
tweak = binary_output[0, 16]
|
120
|
+
ciphertext = binary_output[16, 16]
|
121
|
+
|
122
|
+
# Decrypt and convert back to IP
|
123
|
+
plaintext = aes_xts_decrypt(key, tweak, ciphertext)
|
124
|
+
bytes_to_ip(plaintext)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/ipcrypt.rb
ADDED
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ipcrypt2
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Frank Denis
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: minitest
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '5.0'
|
19
|
+
type: :development
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '5.0'
|
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
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: rubocop
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.0'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.0'
|
54
|
+
description: IPCrypt provides methods for encrypting and obfuscating IP addresses
|
55
|
+
for privacy-preserving storage, logging, and analytics. Implements deterministic,
|
56
|
+
non-deterministic (KIASU-BC), and XTS-based encryption.
|
57
|
+
email:
|
58
|
+
- fde@00f.net
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- CHANGELOG.md
|
64
|
+
- LICENSE
|
65
|
+
- README.md
|
66
|
+
- lib/ipcrypt.rb
|
67
|
+
- lib/ipcrypt/deterministic.rb
|
68
|
+
- lib/ipcrypt/nd.rb
|
69
|
+
- lib/ipcrypt/ndx.rb
|
70
|
+
- lib/ipcrypt/version.rb
|
71
|
+
homepage: https://github.com/jedisct1/ipcrypt-ruby
|
72
|
+
licenses:
|
73
|
+
- MIT
|
74
|
+
metadata:
|
75
|
+
homepage_uri: https://github.com/jedisct1/ipcrypt-ruby
|
76
|
+
source_code_uri: https://github.com/jedisct1/ipcrypt-ruby
|
77
|
+
changelog_uri: https://github.com/jedisct1/ipcrypt-ruby/blob/master/CHANGELOG.md
|
78
|
+
bug_tracker_uri: https://github.com/jedisct1/ipcrypt-ruby/issues
|
79
|
+
documentation_uri: https://www.rubydoc.info/gems/ipcrypt2
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 2.6.0
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubygems_version: 3.6.9
|
95
|
+
specification_version: 4
|
96
|
+
summary: Ruby implementation of IPCrypt for encrypting and obfuscating IP addresses
|
97
|
+
test_files: []
|