gfc64 0.0.2
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/README.md +46 -0
- data/lib/gfc64.rb +57 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f93c96115de165e7cec9824e893e7254b6612c5d254c19d19c14899c7aa68ef1
|
4
|
+
data.tar.gz: 00fc60e41ebe8a82bd7de95386aaa097df8a468ecb13e1ad98c7a4b5a311f59c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2b7b53943801d158fbc1e44c2a9bd6038aa4445047ebfc7de941f3dc583bb55120e9353dbea6d12de4253d1bc67f88c44e4a1c2326a549f4dd13a6a0842e2041
|
7
|
+
data.tar.gz: 6674b80a4457530858aa15b57d89b421431e81313066356d95013292257b160936efde74b27349f410d5cc002dc64bf5dfe9fb00f08af830d72c5dfb2268a617
|
data/README.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# GFC64
|
2
|
+
|
3
|
+
Format-preserving Generalized Feistel Cipher for 64-bit integers. Encrypts 64-bit
|
4
|
+
integers into... 64-bit integers!
|
5
|
+
|
6
|
+
Very useful for if you have sequential 64-bit integer database primary keys that
|
7
|
+
you want to expose in an app, but you don't want to leak count information
|
8
|
+
(e.g. `/customers/1`, `/customers/2`, ...), and you don't want to add another
|
9
|
+
column to store something auxiliary like a UUID.
|
10
|
+
|
11
|
+
For example, with this gem, routes like
|
12
|
+
|
13
|
+
`/customers/1` can become something like `/customers/4552956331295818987`, and
|
14
|
+
|
15
|
+
`/customers/2` can become something like `/customers/3833777695217202560`
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
key = SecureRandom.hex(32) # => "ffb5e3600fc27924f97dc055440403b10ce97160261f2a87eee576584cf942e5"
|
21
|
+
gfc = GFC64.new(key)
|
22
|
+
gfc.encrypt(1) # => 4552956331295818987
|
23
|
+
gfc.decrypt(4552956331295818987) # => 1
|
24
|
+
gfc.encrypt(2) # => 3833777695217202560
|
25
|
+
gfc.decrypt(3833777695217202560) # => 2
|
26
|
+
```
|
27
|
+
|
28
|
+
## Disclaimer
|
29
|
+
|
30
|
+
This code has not been vetted by a security audit or a professional
|
31
|
+
cryptographer so may be wrong and/or insecure.
|
32
|
+
|
33
|
+
## License
|
34
|
+
|
35
|
+
This was written with AI tool assistance. If any code mirrors existing
|
36
|
+
copyrighted works, it was not my intent, and the copyright remains with the
|
37
|
+
original works. My contributions are MIT licensed.
|
38
|
+
|
39
|
+
## References
|
40
|
+
|
41
|
+
Black, John, and Phillip Rogaway. ["Ciphers with Arbitrary Finite Domains."][paper] In Topics in Cryptology — CT-RSA 2002, edited by Bart Preneel, 2271:114–30. Lecture Notes in Computer Science. Berlin, Heidelberg: Springer Berlin Heidelberg, 2002. https://doi.org/10.1007/3-540-45760-7_9.
|
42
|
+
|
43
|
+
Hat tip to [this Hacker News comment][hn] that led me to the idea and the paper.
|
44
|
+
|
45
|
+
[paper]: https://web.cs.ucdavis.edu/~rogaway/papers/subset.pdf
|
46
|
+
[hn]: https://news.ycombinator.com/item?id=27016779
|
data/lib/gfc64.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
require "securerandom"
|
5
|
+
|
6
|
+
class GFC64
|
7
|
+
VERSION = "0.0.2".freeze
|
8
|
+
|
9
|
+
attr_reader :rounds, :key, :block_size
|
10
|
+
|
11
|
+
def initialize(key, rounds: 3)
|
12
|
+
@key = key
|
13
|
+
@block_size = 8 # 64 bits
|
14
|
+
@rounds = rounds
|
15
|
+
end
|
16
|
+
|
17
|
+
def encrypt(number)
|
18
|
+
raise ArgumentError, 'Number must be a 64-bit integer' unless number.is_a?(Integer) && number.between?(0, 2**64 - 1)
|
19
|
+
|
20
|
+
# Split the number into two 32-bit halves
|
21
|
+
left, right = number >> 32, number & 0xFFFFFFFF
|
22
|
+
|
23
|
+
rounds.times do |round|
|
24
|
+
round_key = derive_round_key(key, round)
|
25
|
+
aes = OpenSSL::Cipher.new('AES-128-ECB').encrypt
|
26
|
+
aes.key = round_key
|
27
|
+
f = aes.update([right].pack('N')) + aes.final
|
28
|
+
left, right = right, left ^ f.unpack1('N')
|
29
|
+
end
|
30
|
+
|
31
|
+
# Combine the two 32-bit halves back into a 64-bit number
|
32
|
+
(left << 32) | right
|
33
|
+
end
|
34
|
+
|
35
|
+
def decrypt(number)
|
36
|
+
raise ArgumentError, 'Number must be a 64-bit integer' unless number.is_a?(Integer) && number.between?(0, 2**64 - 1)
|
37
|
+
|
38
|
+
left, right = number >> 32, number & 0xFFFFFFFF
|
39
|
+
|
40
|
+
rounds.times do |round|
|
41
|
+
round_key = derive_round_key(key, rounds - round - 1)
|
42
|
+
aes = OpenSSL::Cipher.new('AES-128-ECB').encrypt
|
43
|
+
aes.key = round_key
|
44
|
+
f = aes.update([left].pack('N')) + aes.final
|
45
|
+
left, right = right ^ f.unpack1('N'), left
|
46
|
+
end
|
47
|
+
|
48
|
+
(left << 32) | right
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def derive_round_key(key, round)
|
54
|
+
# A simple round key derivation using SHA256 and the round number
|
55
|
+
OpenSSL::Digest::SHA256.digest(key + [round].pack('L'))[0...16] # Truncate to 128 bits for AES key
|
56
|
+
end
|
57
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gfc64
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Abe Voelker
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-11-03 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Hides sequential primary key counts without resorting to UUIDs or GUIDs.
|
14
|
+
email: abe@abevoelker.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- README.md
|
20
|
+
- lib/gfc64.rb
|
21
|
+
homepage: https://github.com/abevoelker/gfc64
|
22
|
+
licenses:
|
23
|
+
- MIT
|
24
|
+
metadata:
|
25
|
+
homepage_uri: https://github.com/abevoelker/gfc64
|
26
|
+
documentation_uri: https://rubydoc.info/github/abevoelker/gfc64
|
27
|
+
source_code_uri: https://github.com/abevoelker/gfc64
|
28
|
+
bug_tracker_uri: https://github.com/abevoelker/gfc64/issues
|
29
|
+
post_install_message:
|
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: 2.1.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: 3.4.10
|
45
|
+
signing_key:
|
46
|
+
specification_version: 4
|
47
|
+
summary: Generalized Feistel Cipher for 64-bit integers
|
48
|
+
test_files: []
|