gfc64 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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: []