ffxcodec 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ba22c48b71bd085c4a3dce71253019c04e9d0779
4
+ data.tar.gz: 14ad93e6127517bc027501c512b108c6682b7479
5
+ SHA512:
6
+ metadata.gz: 459359ebd5bb8e2ee4aef9ff97e31731be15146faa70f511092a4a33ffb23b4e54dec54107b668f35f60e2150f4687b80f37641b04f32854b0c9979dc8e7fc54
7
+ data.tar.gz: 27b2a249060cff63d1dd17207a81e6065a0eea449c17465a19a15f5d0d8b6e5097d7002aef8ed2b183628fc331122fc16d363dab9d4764299eaeee1a4ea15471
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /test/reports/
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ffxcodec.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,10 @@
1
+ Copyright (c) 2016, J. Brandt Buckley
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+
8
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
+
10
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # FFXCodec
2
+
3
+ Encodes two unsigned integers into a single, larger (32 or 64-bit) integer.
4
+
5
+ Optionally, it can encrypt/decrypt the resulting integer using an implementation of the AES-FFX [format-preserving cipher][1].
6
+
7
+ Has no external dependencies. Everything is done with the stdlib.
8
+
9
+
10
+ ## Usage
11
+
12
+ Start by divvying up the 32 or 64 bits that will make up the resulting integer:
13
+
14
+ ffx = FFXCodec.new(16, 16) # divide 32-bit int equally
15
+ ffx = FFXCodec.new(40, 24) # divide 64-bit int into a 40 and 24-bit int
16
+
17
+ Then, encode and decode accordingly:
18
+
19
+ ffx.encode(1234567890, 4) #=> 165828720871684
20
+ ffx.decode(165828720871684) #=> [1234567890, 4]
21
+
22
+ Optionally, you can enable encryption by setting a `key` and `tweak`:
23
+
24
+ ffx.setup_encryption("2b7e151628aed2a6abf7158809cf4f3c", "9876543210")
25
+ ffx.encode(797980150281, 5427652) #=> 7692035069140451684
26
+ ffx.decode(7692035069140451684) #=> [797980150281, 5427652]
27
+
28
+
29
+ ### Putting it all together
30
+
31
+ Example **without encryption** (40 and 24-bit integers into 64-bit):
32
+
33
+ ffx = FFXCodec.new(40, 24)
34
+ ffx.encode(1234567890, 4) #=> 165828720871684
35
+ ffx.decode(165828720871684) #=> [1234567890, 4]
36
+
37
+ Example **with encryption** (40 and 24-bit integers into 64-bit):
38
+
39
+ ffx = FFXCodec.new(40, 24)
40
+ ffx.setup_encryption("2b7e151628aed2a6abf7158809cf4f3c", "9876543210")
41
+ ffx.encode(797980150281, 5427652) #=> 7692035069140451684
42
+ ffx.decode(7692035069140451684) #=> [797980150281, 5427652]
43
+
44
+
45
+ ## Installation
46
+
47
+ To install:
48
+
49
+ gem install ffxcodec
50
+
51
+ Add this line to your application's Gemfile:
52
+
53
+ ```ruby
54
+ gem 'ffxcodec'
55
+ ```
56
+
57
+
58
+ ## FAQ
59
+
60
+ Q. Does this only work with unsigned integers?
61
+
62
+ A. It could be made to work with signed integers, but it wasn't built or tested with that use case in mind.
63
+
64
+
65
+ Q. What is a tweak?
66
+
67
+ A. It's kind of like a salt. The [initial FFX spec][2] has a good description.
68
+
69
+
70
+ ## Alternatives
71
+
72
+ Encoding:
73
+ - "Mortonizing" / Z-Order Curve
74
+
75
+ Encryption:
76
+ - [BPS][3] (PDF)
77
+ - [Hasty Pudding cipher][4]
78
+
79
+ ## Warning
80
+
81
+ The AES-FFX implementation is experimental. It was cooked up for this proof of concept.
82
+
83
+ The tests included are based on the NIST reference vectors, but the published vectors only cover radix 10 and 36.
84
+
85
+ Additionally, FFX is still a DRAFT specification. Thus, it cannot yet be considered cryptographically secure.
86
+
87
+ Don't use this for anything beyond basic obfuscation.
88
+
89
+
90
+ ## Known Issues
91
+
92
+ - Assumes little-endian.
93
+ - Assumes 64-bit capable.
94
+
95
+
96
+ ## Author
97
+
98
+ - J. Brandt Buckley <brandt@runlevel1.com>
99
+
100
+
101
+ [1]: https://en.wikipedia.org/wiki/Format-preserving_encryption
102
+ [2]: http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/ffx/ffx-spec.pdf
103
+ [3]: http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/bps/bps-spec.pdf
104
+ [4]: https://en.wikipedia.org/wiki/Hasty_Pudding_cipher
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ end
7
+
8
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ffxcodec"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/exe/ffxcodec ADDED
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "optparse"
4
+ require "ffxcodec"
5
+
6
+ examples = <<-EOF
7
+ Examples:
8
+
9
+ - Encode:
10
+ ffxcodec --alloc=40:24 1234567890 4
11
+
12
+ - Decode:
13
+ ffxcodec --alloc=40:24 20712612157194244
14
+
15
+ - Encrypted encode:
16
+ ffxcodec --alloc=40:24 --key=2b7e151628aed2a6abf7158809cf4f3c --tweak=FZNT4F22E5QA5QUM 1234567890 4
17
+
18
+ - Encrypted decode:
19
+ ffxcodec --alloc=40:24 --key=2b7e151628aed2a6abf7158809cf4f3c --tweak=FZNT4F22E5QA5QUM 5539580373534012574
20
+ EOF
21
+
22
+ options = {}
23
+ OptionParser.new do |opts|
24
+ opts.banner = "Usage: ffxcodec [options] [args]"
25
+
26
+ opts.on("-aLEFT:RIGHT", "--alloc LEFT:RIGHT", "Bit allocation - must add up to 32/64 (REQUIRED)") do |b|
27
+ left, right = b.split(":").map(&:to_i)
28
+ options[:l_alloc] = left
29
+ options[:r_alloc] = right
30
+ end
31
+
32
+ opts.on("-kKEY", "--key KEY", "Hex key to encrypt with (use with --tweak)") do |k|
33
+ options[:key] = k
34
+ end
35
+
36
+ opts.on("-tTWEAK", "--tweak TWEAK", "Tweak to encrypt with (use with --key)") do |t|
37
+ options[:tweak] = t
38
+ end
39
+
40
+ opts.on("-m", "--maximums", "Show max integers for the given allocation and exit") do
41
+ options[:maximum] = true
42
+ end
43
+
44
+ opts.on_tail("-h", "--help", "Show this message") do
45
+ puts opts
46
+ puts ""
47
+ puts examples
48
+ exit
49
+ end
50
+ end.parse!
51
+
52
+ if options[:l_alloc].nil? || options[:r_alloc].nil?
53
+ warn "Must provide left and right bit allocation like: --alloc=40:24"
54
+ exit 2
55
+ end
56
+
57
+ ffx = FFXCodec.new(options[:l_alloc], options[:r_alloc])
58
+
59
+ if options[:maximum]
60
+ l_max, r_max = ffx.maximums
61
+ puts "Left max: #{l_max}"
62
+ puts "Right max: #{r_max}"
63
+ exit
64
+ end
65
+
66
+ if options[:key] || options[:tweak]
67
+ if [options[:key], options[:tweak]].any?(&:nil?)
68
+ warn "Must use --key with --tweak to enable encryption"
69
+ exit 2
70
+ end
71
+ ffx.setup_encryption(options[:key], options[:tweak])
72
+ end
73
+
74
+ case ARGV.count
75
+ when 1
76
+ encoded = ARGV[0].to_i
77
+ userid, listid = ffx.decode(encoded)
78
+ puts "User ID: #{userid}"
79
+ puts "List ID: #{listid}"
80
+ when 2
81
+ userid = ARGV[0].to_i
82
+ listid = ARGV[1].to_i
83
+ encoded = ffx.encode(userid, listid)
84
+ puts "Encoded: #{encoded}"
85
+ else
86
+ warn "Wrong number of values provided to encode or decode."
87
+ exit 2
88
+ end
data/exe/ffxcodec-demo ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "ffxcodec"
4
+ require "pp"
5
+
6
+ a_val = 1234567890
7
+ b_val = 4
8
+ a_size = 40
9
+ b_size = 24
10
+ key = "2b7e151628aed2a6abf7158809cf4f3c"
11
+ tweak = "9876543210"
12
+
13
+ ffx = FFXCodec.new(a_size, b_size) # divide 64-bit int into a 40 and 24-bit int
14
+
15
+ unencrypted_encoded = ffx.encode(a_val, b_val)
16
+ unencrypted_decoded = ffx.decode(unencrypted_encoded)
17
+
18
+ ffx.setup_encryption(key, tweak)
19
+
20
+ encrypted_encoded = ffx.encode(a_val, b_val)
21
+ encrypted_decoded = ffx.decode(encrypted_encoded)
22
+
23
+ puts "Input A: #{a_val}"
24
+ puts "Input B: #{b_val}"
25
+ puts "Input A bit allocation: #{a_size}"
26
+ puts "Input B bit allocation: #{b_size}"
27
+ puts "Encoded Size (Type): #{ffx.size} bytes (#{ffx.bit_length}-bit unsigned integer)"
28
+ puts ""
29
+ puts "Without Encryption:"
30
+ puts "- Encoded: #{unencrypted_encoded}"
31
+ puts "- Decoded: #{unencrypted_decoded.inspect}"
32
+ puts ""
33
+ puts "With Encryption: (Key: #{key}, Tweak: #{tweak})"
34
+ puts "- Encoded: #{encrypted_encoded}"
35
+ puts "- Decoded: #{encrypted_decoded.inspect}"
data/ffxcodec.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ffxcodec/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'ffxcodec'
8
+ spec.version = FFXCodec::VERSION
9
+ spec.authors = ['J. Brandt Buckley']
10
+ spec.email = ['brandt@runlevel1.com']
11
+ spec.license = 'BSD-2-Clause'
12
+
13
+ spec.summary = 'Encodes two integers into one with optional encryption'
14
+ spec.description = 'Encodes two unsigned integers into a single, larger (32 or 64-bit) integer with optional AES-FFX encryption.'
15
+ spec.homepage = 'https://github.com/brandt/ffxcodec'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.8"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "minitest"
25
+ end
@@ -0,0 +1,40 @@
1
+ class String
2
+ # XOR operation on String
3
+ #
4
+ # @param [String] other string to XOR with
5
+ # @raises [ArgumentError] if other string isn't the same length
6
+ # @return [String] result of XOR operation
7
+ # rubocop:disable AbcSize
8
+ def ^(other)
9
+ # rubocop:disable RedundantSelf, SignalException
10
+ b1 = self.unpack("C*")
11
+ b2 = other.unpack("C*")
12
+ raise ArgumentError, "Strings must be the same length" unless b1.size == b2.size
13
+ longest = [b1.length, b2.length].max
14
+ b1 = [0] * (longest - b1.length) + b1
15
+ b2 = [0] * (longest - b2.length) + b2
16
+ b1.zip(b2).map { |a, b| a ^ b }.pack("C*")
17
+ end
18
+
19
+ # Split down the middle into two parts (right-biased)
20
+ #
21
+ # @return [Array<String>] the original string split into two. If length was
22
+ # odd, then the second string will have an extra character.
23
+ def bisect
24
+ n = self.size
25
+ l = n / 2
26
+ [self[0...l], self[l...n]]
27
+ end
28
+
29
+ # Prepend zeroes until string is of the given length
30
+ #
31
+ # @note if string was already longer than the given length, no action taken
32
+ #
33
+ # @param [Integer] length we want the resulting string to be
34
+ # @return [String] prepended with '0's until the given length is reached
35
+ def prepad_zeros(length)
36
+ str = self
37
+ str.insert(0, '0') while str.length < length
38
+ str
39
+ end
40
+ end
@@ -0,0 +1,120 @@
1
+ class FFXCodec
2
+ # Encode two integers into one larger integer and decode back
3
+ class Encoder
4
+ # @return [Fixnum] size of encoded integer in bits (32 or 64)
5
+ attr_reader :size
6
+
7
+ # @return [Fixnum] maximum unsigned value representable by left integer
8
+ attr_reader :a_max
9
+
10
+ # @return [Fixnum] maximum unsigned value representable by right integer
11
+ attr_reader :b_max
12
+
13
+ # @param [Fixnum] a_size the number of bits allocated to the left integer
14
+ # @param [Fixnum] b_size the number of bits allocated to the right integer
15
+ def initialize(a_size = 32, b_size = 32)
16
+ @a_size = a_size
17
+ @b_size = b_size
18
+ @a_max, @b_max = maximums(a_size, b_size)
19
+ @size = a_size + b_size
20
+ check_size
21
+ end
22
+
23
+ # Combine two unsigned integers into a single, larger unsigned integer
24
+ #
25
+ # @param [Fixnum] a value to encode
26
+ # @param [Fixnum] b value to encode
27
+ #
28
+ # @example Encode 40 and 24-bit integers into a single 64-bit integer
29
+ # i = Encoder.new(40, 24)
30
+ # i.encode(1234567890, 4) #=> 20712612157194244
31
+ #
32
+ # @return [Fixnum, Bignum] encoded integer
33
+ def encode(a, b)
34
+ check_ab_bounds(a, b)
35
+ i = (a << @b_size) ^ b
36
+ interlace(i)
37
+ end
38
+
39
+ # Separate an unsigned integer into two smaller unsigned integers
40
+ #
41
+ # @example Decode an encoded 64-bit integer into 40 and 24-bit integers
42
+ # i = Encoder.new(40, 24)
43
+ # i.decode(20712612157194244) #=> [1234567890, 4]
44
+ #
45
+ # @param [Fixnum, Bignum] c encoded value to decode
46
+ # @return [Array<Fixnum>] decoded integers
47
+ def decode(c)
48
+ i = interlace(c)
49
+ a = i >> @b_size
50
+ b = (i ^ (a << @b_size))
51
+ [a, b]
52
+ end
53
+
54
+ private
55
+
56
+ # Interlace / deinterlace bytes
57
+ #
58
+ # Running this on a number toggles interlacing (a reorder of the bits).
59
+ #
60
+ # This reorders the bytes in an integer. We do this to avoid a possible
61
+ # reduction in the effectiveness of our encryption resulting from the fact
62
+ # that we encode input by concatenating the bits of two potentially equally
63
+ # sized integers and the way a maximally-balanced Feistel splits the input
64
+ # in half.
65
+ #
66
+ # Technically we only need to do this when encryption is enabled and the
67
+ # integer is evenly divided. However, this imparts almost no performance
68
+ # penalty. Ruby can perform ~1 million of these operations in 2 sec for
69
+ # random 64-bit values and 0.5 sec for random 32-bit values.
70
+ #
71
+ # @param [Fixnum, Bignum] n interlaced or uninterlaced value
72
+ # @return [Fixnum, Bignum] interlaced value if input wasn't interlaced
73
+ # @return [Fixnum, Bignum] deinterlaced value if input was interlaced
74
+ # rubocop:disable AbcSize
75
+ def interlace(n)
76
+ # rubocop:disable SpaceAroundOperators, MultilineOperationIndentation
77
+ if @size == 32
78
+ n & 0xFF0000FF | # 0, 3
79
+ (n >> 8) & 0x0000FF00 | # 1
80
+ (n << 8) & 0x00FF0000 # 2
81
+ else
82
+ n & 0xFF00FF0000FF00FF | # 0, 2, 5, 7
83
+ (n >> 40) & 0x000000000000FF00 | # 1
84
+ (n >> 8) & 0x00000000FF000000 | # 3
85
+ (n << 8) & 0x000000FF00000000 | # 4
86
+ (n << 40) & 0x00FF000000000000 # 6
87
+ end
88
+ end
89
+
90
+ # Calculate the maximum values representable in the given number of bits
91
+ #
92
+ # @param [Fixnum] a_size number of bits allocated to left integer
93
+ # @param [Fixnum] b_size number of bits allocated to right integer
94
+ # @return [Array<Fixnum>] maximum representable values for each integer
95
+ def maximums(a_size, b_size)
96
+ a_max = (1 << a_size) - 1
97
+ b_max = (1 << b_size) - 1
98
+ [a_max, b_max]
99
+ end
100
+
101
+ # @param [Fixnum] a left integer to be encoded
102
+ # @param [Fixnum] b right integer to be encoded
103
+ # @raise [ArgumentError] if the given values fall outside our maximums
104
+ # @return [void]
105
+ def check_ab_bounds(a, b)
106
+ if a > @a_max || a < 0
107
+ fail ArgumentError, "LHS #{@a_size}-bit value out of bounds: #{a}"
108
+ elsif b > @b_max || b < 0
109
+ fail ArgumentError, "RHS #{@b_size}-bit value out of bounds: #{b}"
110
+ end
111
+ end
112
+
113
+ # @raise [ArgumentError] if the combined bit count isn't 32 or 64 bits
114
+ # @return [void]
115
+ def check_size
116
+ return if @size == 32 || @size == 64
117
+ fail ArgumentError, "Combined size must be 32 or 64 bits"
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,223 @@
1
+ require "openssl"
2
+
3
+ class FFXCodec
4
+ # Implementation of AES-FFX mode format-preserving encryption
5
+ #
6
+ # Cipher device encrypts integers where the resulting ciphertext has the same
7
+ # number of digits in the given base (radix).
8
+ #
9
+ # @note WARNING: This was cooked up as an experimental proof of concept.
10
+ # It hasn't been tested thoroughly and shouldn't be considered secure.
11
+ #
12
+ # @note Format-preserving != integer-size-preserving in base 10 (see below)
13
+ #
14
+ # The format-preserving characteristic of this cipher is best thought of as
15
+ # preserving the number of digits, not the integer size. For instance, in
16
+ # base 10, 4294967295 and 4294967296 would be considered to have the same
17
+ # format, but the first is a 32-bit unsigned integer and the second is 64.
18
+ #
19
+ # So given base 10 input that fits within a 32 or 64-bit integer, it's
20
+ # possible for the AES-FFX cipher to return a number that contains the same
21
+ # number of base 10 digits but exceeds the largest number that can be
22
+ # represented in 32 or 64 bits respectively.
23
+ #
24
+ # You can work around this by using radix 2 so that the cipher returns an
25
+ # equal number of bits. As with all modes, you must supply input as a
26
+ # stringified integer in the base you've specified.
27
+ #
28
+ # Be aware that when you convert between bases, leading zeros are sometimes
29
+ # dropped by the converter. You must supply the same number of digits to the
30
+ # decrypter as you did to the encrypter or you'll get a different value. The
31
+ # encrypt and decrypt methods prepend zeros until the input is is of the
32
+ # length specified during initialization.
33
+ #
34
+ class Encrypt
35
+ # @param [Fixnum] length of input
36
+ attr_accessor :length
37
+
38
+ # @note This is set to 10 by the spec. Don't change it unless you know
39
+ # what you're doing.
40
+ # @param [Fixnum] rounds of encryption / decryption to run input through
41
+ attr_accessor :rounds
42
+
43
+ # @return [Fixnum] radix of the input
44
+ attr_reader :radix
45
+
46
+ # @param [String] key for AES as a hexadecimal string
47
+ # @param [String] tweak for AES
48
+ # @param [Fixnum] length of the input
49
+ # @param [Fixnum] radix of the input
50
+ def initialize(key, tweak, length, radix = 10)
51
+ self.key = key
52
+ self.tweak = tweak
53
+ self.radix = radix
54
+ @length = length
55
+ @rounds = 10
56
+ end
57
+
58
+ # @param [String] key for AES as a hexadecimal string
59
+ def key=(key)
60
+ hexkey = [key].pack('H*')
61
+ fail ArgumentError, "key must be a 16-byte hexidecimal" if hexkey.length != 16
62
+ @key = hexkey
63
+ end
64
+
65
+ # @param [String] tweak tweak for AES
66
+ def tweak=(tweak)
67
+ fail ArgumentError, "tweak length must be under (2^32) - 1" if tweak.length > ((1 << 32) - 1)
68
+ @tweak = tweak
69
+ end
70
+
71
+ # @param [Fixnum] num radix of the input
72
+ def radix=(num)
73
+ fail ArgumentError, "radix must be between 2 and 2^16" if num > 65536
74
+ @radix = num
75
+ end
76
+
77
+ # Encrypt
78
+ #
79
+ # @param [String] input unencrypted, stringifed integer of base @radix
80
+ #
81
+ # @example Encrypt
82
+ # e = Encrypt.new("4fb450a9c27dd07f22ef56413432c94a", "FZNT4F22E5QA5QUM")
83
+ # e.encrypt(1234567890) #=> "1224011974"
84
+ #
85
+ # @return [Fixnum, Bignum] encrypted integer
86
+ def encrypt(input)
87
+ a, b = input.prepad_zeros(@length).bisect
88
+ 0.upto(@rounds - 1) do |iter|
89
+ f = feistel_round(input.size, iter, b)
90
+ c = block_addition(a, f)
91
+ a = b
92
+ b = c
93
+ end
94
+ a + b
95
+ end
96
+
97
+ # Decrypt
98
+ #
99
+ # @param [String] input encrypted, stringifed integer of base @radix
100
+ #
101
+ # @example Decrypt
102
+ # e = Encrypt.new("4fb450a9c27dd07f22ef56413432c94a", "FZNT4F22E5QA5QUM")
103
+ # e.decrypt(1224011974) #=> "1234567890"
104
+ #
105
+ # @return [Fixnum, Bignum] unencrypted integer
106
+ def decrypt(input)
107
+ a, b = input.prepad_zeros(@length).bisect
108
+ (@rounds - 1).downto(0) do |iter|
109
+ c = b
110
+ b = a
111
+ f = feistel_round(input.size, iter, b)
112
+ lmin = [c.size, f.size].min
113
+ a = block_subtraction(lmin, c, f)
114
+ end
115
+ a + b
116
+ end
117
+
118
+ private
119
+
120
+ # Computes the block-wise radix addition of x and y
121
+ def block_addition(a, b)
122
+ sum = a.to_i(@radix) + b.to_i(@radix)
123
+ sum %= (@radix**a.size)
124
+ sum.to_s(@radix).prepad_zeros(a.size)
125
+ end
126
+
127
+ # Computes the block-wise radix subtraction of x and y
128
+ def block_subtraction(n, x, y)
129
+ diff = x.to_i(@radix) - y.to_i(@radix)
130
+ mod = @radix**n
131
+ block_diff = diff % mod
132
+ block_diff += mod if block_diff < 0
133
+ out = block_diff.to_s(@radix)
134
+ return out unless out.length < n
135
+ out.prepad_zeros(n)
136
+ end
137
+
138
+ def num_radix(str, length)
139
+ n = str.to_i(@radix)
140
+ n_bitcount = ('0' * (length * 8)) + n.to_s(2)
141
+ n_bitcount = n_bitcount[-(length * 8)..-1]
142
+ [n_bitcount].pack('B*')
143
+ end
144
+
145
+ def aes(block)
146
+ aes = OpenSSL::Cipher::Cipher.new('aes-128-ecb')
147
+ aes.encrypt
148
+ aes.key = @key
149
+ aes.update(block)
150
+ end
151
+
152
+ def cbc_mac(block)
153
+ fail "invalid block size" unless (block.size % 16 == 0)
154
+ y = "\0" * 16
155
+ i = 0
156
+ while i < block.size
157
+ x = block[i...(i + 16)]
158
+ y = aes(x ^ y)
159
+ i += 16
160
+ end
161
+ y
162
+ end
163
+
164
+ def byte_array_to_int(block)
165
+ block.bytes.inject(0) { |memo, b| (memo << 8) + b }
166
+ end
167
+
168
+ # Creates the first half of the IV
169
+ #
170
+ # Concatenated with Q in the feistel round.
171
+ #
172
+ # p <- [vers] | [method] | [addition] | [radix] | [rnds(n)] | [split(n)] | [n] | [t]
173
+ def generate_p(input_len)
174
+ vers = 1
175
+ method = 2
176
+ addition = 1
177
+ split_n = input_len / 2
178
+ [vers, method, addition].pack('CCC') +
179
+ [@radix].pack('N')[1..3] +
180
+ [@rounds].pack('C') +
181
+ [split_n].pack('C') +
182
+ [input_len].pack('N') +
183
+ [@tweak.length].pack('N')
184
+ end
185
+
186
+ # Creates the second half of the IV
187
+ #
188
+ # Concatenated with P in the feistel round.
189
+ #
190
+ # q <- tweak | [0]^((-t-b-1) mod 16) | [roundNum] | [numradix(B)]
191
+ def generate_q(b, blk_len, round)
192
+ round_num = [round].pack('C')
193
+ @tweak + "\0" * ((-@tweak.size - blk_len - 1) % 16) + round_num + num_radix(b, blk_len)
194
+ end
195
+
196
+ # Y <- first d+4 bytes of (Y | AESK(Y XOR [1]16) | AESK(Y XOR [2]16) | AESK(Y XOR [3]16)...)
197
+ def generate_y(blk_len, iv_p, iv_q)
198
+ d = 4 * (blk_len / 4.0).ceil
199
+ y = cbc_mac(iv_p + iv_q)
200
+ byte_array_to_int(y[0...(d + 4)])
201
+ end
202
+
203
+ # b <- ceil(ceil(beta * log_2(radix)) / 8)
204
+ def block_length(input_len)
205
+ beta = (input_len / 2.0).ceil
206
+ ((beta * Math.log(@radix) / Math.log(2)).ceil / 8.0).ceil
207
+ end
208
+
209
+ # Runs the given block through the modified feistel network
210
+ def feistel_round(input_len, iter, b)
211
+ blk_len = block_length(input_len)
212
+ iv_p = generate_p(input_len)
213
+ iv_q = generate_q(b, blk_len, iter)
214
+
215
+ # z = y mod r^m
216
+ y = generate_y(blk_len, iv_p, iv_q)
217
+ m = (iter % 2).zero? ? (input_len / 2) : (input_len / 2.0).ceil
218
+ z = y % (@radix**m)
219
+
220
+ z.to_s(@radix).prepad_zeros(m)
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,3 @@
1
+ class FFXCodec
2
+ VERSION = "0.1.0"
3
+ end
data/lib/ffxcodec.rb ADDED
@@ -0,0 +1,119 @@
1
+ require "ffxcodec/version"
2
+ require "ffxcodec/core_ext/string"
3
+ require "ffxcodec/encrypt"
4
+ require "ffxcodec/encoder"
5
+
6
+ # Encode / decode two integers into a single integer with optional encryption
7
+ #
8
+ # The resulting value is a single 32 or 64-bit unsigned integer (your choice),
9
+ # even when encrypted.
10
+ #
11
+ # Works by divvying up the bits of the single integer between the two component
12
+ # integers and running it through AES-FFX format-preserving cipher (optional).
13
+ class FFXCodec
14
+ # @param [Fixnum] a_size the number of bits allocated to the left integer
15
+ # @param [Fixnum] b_size the number of bits allocated to the right integer
16
+ def initialize(a_size, b_size)
17
+ @encoder = Encoder.new(a_size, b_size)
18
+ end
19
+
20
+ # Setup encryption
21
+ #
22
+ # Auto-enables encryption after encoding and decryption before decoding.
23
+ #
24
+ # @param [String] key for AES as a hexadecimal string
25
+ # @param [String] tweak for AES
26
+ # @return [void]
27
+ def setup_encryption(key, tweak)
28
+ @crypto = Encrypt.new(key, tweak, @encoder.size, 2)
29
+ end
30
+
31
+ # Turn off encryption
32
+ #
33
+ # @return [void]
34
+ def disable_encryption
35
+ @crypto = false
36
+ end
37
+
38
+ # Encode two integers into a single integer
39
+ #
40
+ # @param [Fixnum] a value to encode
41
+ # @param [Fixnum] b value to encode
42
+ #
43
+ # @example Encode 40 and 24-bit integers into an unencrypted 64-bit integer
44
+ # ffx = FFXCodec.new(40, 24)
45
+ # ffx.encode(1234567890, 4) #=> 165828720871684
46
+ #
47
+ # @example Encode 40 and 24-bit integers into an encrypted 64-bit integer
48
+ # ffx = FFXCodec.new(40, 24)
49
+ # ffx.setup_encryption("2b7e151628aed2a6abf7158809cf4f3c", "9876543210")
50
+ # ffx.encode(797980150281, 5427652) #=> 7692035069140451684
51
+ #
52
+ # @return [Fixnum, Bignum] encoded integer if encryption not setup
53
+ # @return [Fixnum, Bignum] encrypted encoded integer if encryption setup
54
+ def encode(a, b)
55
+ c = @encoder.encode(a, b)
56
+ @crypto ? encrypt(c) : c
57
+ end
58
+
59
+ # Decode an integer into its two component integers
60
+ #
61
+ # @note input will automatically be decrypted if encryption was setup
62
+ # @param [Fixnum, Bignum] c value to decode
63
+ #
64
+ # @example Decode unencrypted integer into component 40 and 24-bit integers
65
+ # ffx = FFXCodec.new(40, 24)
66
+ # ffx.decode(165828720871684) #=> [1234567890, 4]
67
+ #
68
+ # @example Decode encrypted integer into component 40 and 24-bit integers
69
+ # ffx = FFXCodec.new(40, 24)
70
+ # ffx.setup_encryption("2b7e151628aed2a6abf7158809cf4f3c", "9876543210")
71
+ # ffx.decode(7692035069140451684) #=> [797980150281, 5427652]
72
+ #
73
+ # @return [Array<Fixnum>] component integers
74
+ def decode(c)
75
+ input = @crypto ? decrypt(c) : c
76
+ @encoder.decode(input)
77
+ end
78
+
79
+ # Show maximum representable base 10 value for each field
80
+ #
81
+ # @example Maximums for a 32-bit integer split into 24 and 8-bit components
82
+ # ffx = FFXCodec.new(24, 8)
83
+ # ffx.maximums #=> [16777215, 255]
84
+ #
85
+ # @example Maximums for a 64-bit integer split into two 32-bit components
86
+ # ffx = FFXCodec.new(32, 32)
87
+ # ffx.maximums #=> [4294967295, 4294967295]
88
+ #
89
+ # @return [Array<Fixnum>] maximum representable component integers
90
+ def maximums
91
+ [@encoder.a_max, @encoder.b_max]
92
+ end
93
+
94
+ # Show size of the resulting integer in bytes
95
+ #
96
+ # @return [Fixnum] size of the combined integer in bytes
97
+ def size
98
+ @encoder.size / 8
99
+ end
100
+
101
+ # Show size of the resulting integer in bits
102
+ #
103
+ # @return [Fixnum] size of the combined integer in bits
104
+ def bit_length
105
+ @encoder.size
106
+ end
107
+
108
+ private
109
+
110
+ # @param [Fixnum, Bignum] value to encrypt
111
+ def encrypt(value)
112
+ @crypto.encrypt(value.to_s(2)).to_i(2)
113
+ end
114
+
115
+ # @param [Fixnum, Bignum] value to decrypt
116
+ def decrypt(value)
117
+ @crypto.decrypt(value.to_s(2)).to_i(2)
118
+ end
119
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ffxcodec
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - J. Brandt Buckley
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-02-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.8'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Encodes two unsigned integers into a single, larger (32 or 64-bit) integer
56
+ with optional AES-FFX encryption.
57
+ email:
58
+ - brandt@runlevel1.com
59
+ executables:
60
+ - ffxcodec
61
+ - ffxcodec-demo
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - ".gitignore"
66
+ - ".travis.yml"
67
+ - Gemfile
68
+ - LICENSE
69
+ - README.md
70
+ - Rakefile
71
+ - bin/console
72
+ - bin/setup
73
+ - exe/ffxcodec
74
+ - exe/ffxcodec-demo
75
+ - ffxcodec.gemspec
76
+ - lib/ffxcodec.rb
77
+ - lib/ffxcodec/core_ext/string.rb
78
+ - lib/ffxcodec/encoder.rb
79
+ - lib/ffxcodec/encrypt.rb
80
+ - lib/ffxcodec/version.rb
81
+ homepage: https://github.com/brandt/ffxcodec
82
+ licenses:
83
+ - BSD-2-Clause
84
+ metadata: {}
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 2.4.8
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: Encodes two integers into one with optional encryption
105
+ test_files: []
106
+ has_rdoc: