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.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +46 -0
  3. data/lib/gfc64.rb +57 -0
  4. 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: []