miscreant 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 23230bd423fada7ae6bfceeafeb1bff7133e8e75
4
- data.tar.gz: 3dc5b9db54803f64f9e58a87a4ac89370e785b2e
3
+ metadata.gz: 96a35e90aee3d3f35dcbe9a273e57d9685b7d537
4
+ data.tar.gz: dc9ab108d1d8e69daccc528bc6f06f009ab584ed
5
5
  SHA512:
6
- metadata.gz: c53969bd291002daf380591ee6547c49fa24e86e7b3e9c3fdb5c4575cbfa049a1db141af95f447dc8e4bf5a35a9acd053138a4cea1ee940c635cb44e2b0d50a9
7
- data.tar.gz: 45871b8499fbf30d1090a4f45a31eaf6d54583f7e458013c701597a7ec28c2d7df8aac80b94adf717baf1b2b64a9233dc77fd7294595e744b948ae08df7c92c7
6
+ metadata.gz: 8463583471437d1be6ef3885bbcee2ef6d78c58db500d2893a2ab3c91e6864eb5669fae96377d59d6a45e4711b6a2b7bbc57bb8d41f1a8538b0475a10ceeeb7e
7
+ data.tar.gz: f9d0be51f4255fed217290c9d07e90dea204e3009836bf5088eb861881d3b5f84b0d05804004c48d6b50e21f6367a0b8927177dd7cda138173fd1ef82d4a7039
@@ -17,6 +17,9 @@ Metrics/CyclomaticComplexity:
17
17
  Metrics/PerceivedComplexity:
18
18
  Enabled: false
19
19
 
20
+ Metrics/BlockLength:
21
+ Max: 100
22
+
20
23
  Metrics/ClassLength:
21
24
  Max: 100
22
25
 
@@ -30,5 +33,17 @@ Metrics/MethodLength:
30
33
  # Style
31
34
  #
32
35
 
36
+ Style/AsciiComments:
37
+ Enabled: false
38
+
39
+ Style/ClassAndModuleChildren:
40
+ Enabled: false
41
+
42
+ Style/ConditionalAssignment:
43
+ Enabled: false
44
+
45
+ Style/NumericPredicate:
46
+ Enabled: false
47
+
33
48
  Style/StringLiterals:
34
49
  EnforcedStyle: double_quotes
data/README.md CHANGED
@@ -81,27 +81,24 @@ Or install it yourself as:
81
81
 
82
82
  ## API
83
83
 
84
- ### Miscreant::AES::SIV
84
+ ### Miscreant::AEAD
85
85
 
86
- The `Miscreant::AES::SIV` class provides the main interface to the **AES-SIV**
86
+ The `Miscreant::AEAD` class provides the main interface to the **AES-SIV**
87
87
  misuse resistant authenticated encryption function.
88
88
 
89
- To make a new instance, pass in a 32-byte or 64-byte key. Note that these
90
- options are twice the size of what you might be expecting (AES-SIV uses two
91
- AES keys).
92
-
93
- You can generate a random key using the `generate_key` method (default 32 bytes):
89
+ To make a new instance, pass in a binary-encoded 32-byte or 64-byte key.
90
+ Note that these options are twice the size of what you might be expecting
91
+ (AES-SIV uses two AES keys).
94
92
 
95
93
  ```ruby
96
- key_bytes = Miscreant::AES::SIV.generate_key
97
- key = Miscreant::AES::SIV.new(key_bytes)
98
- # => #<Miscreant::AES::SIV:0x007fe0109e85e8>
94
+ secret_key = Miscreant::AEAD.generate_key
95
+ encryptor = Miscreant::AEAD.new("AES-SIV", secret_key)
99
96
  ```
100
97
 
101
98
  #### Encryption (#seal)
102
99
 
103
- The `Miscreant::AES::SIV#seal` method encrypts a message along with a set of
104
- *associated data* message headers.
100
+ The `Miscreant::AEAD#seal` method encrypts a binary-encoded message along with
101
+ a set of *associated data* message headers.
105
102
 
106
103
  It's recommended to include a unique "nonce" value with each message. This
107
104
  prevents those who may be observing your ciphertexts from being able to tell
@@ -114,23 +111,33 @@ Example:
114
111
 
115
112
  ```ruby
116
113
  message = "Hello, world!"
117
- nonce = SecureRandom.random_bytes(16)
114
+ nonce = Miscreant::AEAD.generate_nonce
118
115
  ciphertext = key.seal(message, nonce)
119
116
  ```
120
117
 
121
118
  #### Decryption (#open)
122
119
 
123
- The `Miscreant::AES::SIV#open` method decrypts a ciphertext with the given key.
120
+ The `Miscreant::AEAD#open` method decrypts a binary-encoded ciphertext with the
121
+ given key.
124
122
 
125
123
  Example:
126
124
 
127
125
  ```ruby
128
126
  message = "Hello, world!"
129
- nonce = SecureRandom.random_bytes(16)
127
+ nonce = Miscreant::AEAD.generate_nonce
130
128
  ciphertext = key.seal(message, nonce)
131
129
  plaintext = key.open(ciphertext, nonce)
132
130
  ```
133
131
 
132
+ ## Code of Conduct
133
+
134
+ We abide by the [Contributor Covenant][cc] and ask that you do as well.
135
+
136
+ For more information, please see [CODE_OF_CONDUCT.md].
137
+
138
+ [cc]: https://contributor-covenant.org
139
+ [CODE_OF_CONDUCT.md]: https://github.com/miscreant/miscreant/blob/master/CODE_OF_CONDUCT.md
140
+
134
141
  ## Contributing
135
142
 
136
143
  Bug reports and pull requests are welcome on GitHub at https://github.com/miscreant/miscreant
@@ -1,9 +1,14 @@
1
+ # encoding: binary
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require "openssl"
4
5
  require "securerandom"
5
6
 
6
7
  require "miscreant/version"
8
+ require "miscreant/aead"
9
+ require "miscreant/aes/cmac"
10
+ require "miscreant/aes/pmac"
11
+ require "miscreant/aes/siv"
7
12
  require "miscreant/internals"
8
13
 
9
14
  # Miscreant: A misuse-resistant symmetric encryption library
@@ -13,4 +18,7 @@ module Miscreant
13
18
 
14
19
  # Ciphertext failed to verify as authentic
15
20
  IntegrityError = Class.new(CryptoError)
21
+
22
+ # Hide internals from the outside world
23
+ private_constant :Internals
16
24
  end
@@ -0,0 +1,93 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ # The AEAD class provides Authenticated Encryption with Associated Data
6
+ #
7
+ # If you're looking for the API to encrypt something, congratulations!
8
+ # This is the one you probably want to use. This class provides a high-level
9
+ # interface to Miscreant's misuse-resistant encryption.
10
+ class AEAD
11
+ # Generate a new random AES-SIV key of the given size
12
+ #
13
+ # @param size [Integer] size of key in bytes (32 or 64)
14
+ #
15
+ # @return [String] newly generated AES-SIV key
16
+ def self.generate_key(size = 32)
17
+ raise ArgumentError, "key size must be 32 or 64 bytes" unless [32, 64].include?(size)
18
+ AES::SIV.generate_key(size)
19
+ end
20
+
21
+ # Generate a random "nonce" (i.e. number used once) value
22
+ #
23
+ # @param size [Integer] size of nonce in bytes (default 16)
24
+ #
25
+ # @return [String] newly generated nonce value
26
+ def self.generate_nonce(size = 16)
27
+ SecureRandom.random_bytes(size)
28
+ end
29
+
30
+ # Create a new AEAD encryptor instance.
31
+ #
32
+ # You will need to select an algorithm to use, passed as a string:
33
+ #
34
+ # * "AES-SIV" (RFC 5297): the original AES-SIV function, based on CMAC
35
+ # * "AES-PMAC-SIV": a parallelizable AES-SIV alternative
36
+ #
37
+ # Choose AES-PMAC-SIV if you'd like better performance.
38
+ # Choose AES-SIV if you'd like wider compatibility: AES-PMAC-SIV is
39
+ # presently implemented in the Miscreant libraries.
40
+ #
41
+ # @param alg ["AES-SIV", "AES-PMAC-SIV"] cryptographic algorithm to use
42
+ # @param key [String] 32-byte or 64-byte random Encoding::BINARY secret key
43
+ def initialize(alg, key)
44
+ Internals::Util.validate_bytestring("key", key, length: [32, 64])
45
+
46
+ case alg
47
+ when "AES-SIV", "AES-CMAC-SIV"
48
+ mac = :CMAC
49
+ when "AES-PMAC-SIV"
50
+ mac = :PMAC
51
+ else raise ArgumentError, "unsupported algorithm: #{alg.inspect}"
52
+ end
53
+
54
+ @siv = AES::SIV.new(key, mac)
55
+ end
56
+
57
+ # Inspect this AES-SIV instance
58
+ #
59
+ # @return [String] description of this instance
60
+ def inspect
61
+ to_s
62
+ end
63
+
64
+ # Encrypt a message, authenticating it along with the associated data
65
+ #
66
+ # @param plaintext [String] an Encoding::BINARY string to encrypt
67
+ # @param nonce [String] a unique-per-message value
68
+ # @param ad [String] optional data to authenticate along with the message
69
+ #
70
+ # @return [String] encrypted ciphertext
71
+ def seal(plaintext, nonce:, ad: "")
72
+ raise TypeError, "expected String, got #{nonce.class}" unless nonce.is_a?(String)
73
+ raise TypeError, "expected String, got #{ad.class}" unless ad.is_a?(String)
74
+
75
+ @siv.seal(plaintext, [ad, nonce])
76
+ end
77
+
78
+ # Verify and decrypt a ciphertext, authenticating it along with the associated data
79
+ #
80
+ # @param ciphertext [String] an Encoding::BINARY string to decrypt
81
+ # @param nonce [String] a unique-per-message value
82
+ # @param associated_data [String] optional data to authenticate along with the message
83
+ #
84
+ # @raise [Miscreant::IntegrityError] ciphertext and/or associated data are corrupt or tampered with
85
+ # @return [String] decrypted plaintext
86
+ def open(ciphertext, nonce:, ad: "")
87
+ raise TypeError, "expected nonce to be String, got #{nonce.class}" unless nonce.is_a?(String)
88
+ raise TypeError, "expected ad to be String, got #{ad.class}" unless ad.is_a?(String)
89
+
90
+ @siv.open(ciphertext, [ad, nonce])
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,63 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ # The Advanced Encryption Standard
6
+ module AES
7
+ # The AES-CMAC message authentication code
8
+ class CMAC
9
+ # Create a new AES-CMAC instance
10
+ #
11
+ # @param key [String] 16-byte or 32-byte Encoding::BINARY cryptographic key
12
+ def initialize(key)
13
+ @cipher = Internals::AES::BlockCipher.new(key)
14
+
15
+ @subkey1 = Internals::Block.new
16
+ @subkey1.encrypt(@cipher)
17
+ @subkey1.dbl
18
+
19
+ @subkey2 = @subkey1.dup
20
+ @subkey2.dbl
21
+ end
22
+
23
+ # Inspect this AES-CMAC instance
24
+ #
25
+ # @return [String] description of this instance
26
+ def inspect
27
+ to_s
28
+ end
29
+
30
+ # Compute the AES-CMAC of the given input message in a single shot,
31
+ # outputting the MAC tag.
32
+ #
33
+ # Unlike other AES-CMAC implementations, this one does not support
34
+ # incremental processing/IUF operation. (Though that would enable
35
+ # slightly more efficient decryption for AES-SIV)
36
+ #
37
+ # @param message [String] an Encoding::BINARY string to authenticate
38
+ #
39
+ # @return [String] CMAC tag
40
+ def digest(message)
41
+ Internals::Util.validate_bytestring("message", message)
42
+
43
+ if message.empty? || message.length % Internals::Block::SIZE != 0
44
+ message = Internals::Util.pad(message, Internals::Block::SIZE)
45
+ subkey = @subkey2
46
+ else
47
+ subkey = @subkey1
48
+ end
49
+
50
+ count = message.length / Internals::Block::SIZE
51
+ digest = Internals::Block.new
52
+
53
+ count.times do |i|
54
+ digest.xor_in_place(message[Internals::Block::SIZE * i, Internals::Block::SIZE])
55
+ digest.xor_in_place(subkey) if i == count - 1
56
+ digest.encrypt(@cipher)
57
+ end
58
+
59
+ digest.data
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,128 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ # The Advanced Encryption Standard
6
+ module AES
7
+ # The Parallel Message Authentication Code
8
+ class PMAC
9
+ # Number of L blocks to precompute (i.e. µ in the PMAC paper)
10
+ # TODO: dynamically compute these as needed
11
+ PRECOMPUTED_BLOCKS = 31
12
+
13
+ # Create a new PMAC instance
14
+ #
15
+ # @param key [String] 16-byte or 32-byte Encoding::BINARY cryptographic key
16
+ def initialize(key)
17
+ @cipher = Internals::AES::BlockCipher.new(key)
18
+
19
+ # L is defined as follows (quoted from the PMAC paper):
20
+ #
21
+ # Equation 1:
22
+ #
23
+ # a · x =
24
+ # a<<1 if firstbit(a)=0
25
+ # (a<<1) ⊕ 0¹²⁰10000111 if firstbit(a)=1
26
+ #
27
+ # Equation 2:
28
+ #
29
+ # a · x⁻¹ =
30
+ # a>>1 if lastbit(a)=0
31
+ # (a>>1) ⊕ 10¹²⁰1000011 if lastbit(a)=1
32
+ #
33
+ # Let L(0) ← L. For i ∈ [1..µ], compute L(i) ← L(i − 1) · x by
34
+ # Equation (1) using a shift and a conditional xor.
35
+ #
36
+ # Compute L(−1) ← L · x⁻¹ by Equation (2), using a shift and a
37
+ # conditional xor.
38
+ #
39
+ # Save the values L(−1), L(0), L(1), L(2), ..., L(µ) in a table.
40
+ # (Alternatively, [ed: as we have done in this codebase] defer computing
41
+ # some or all of these L(i) values until the value is actually needed.)
42
+ @l = []
43
+ tmp = Internals::Block.new
44
+ tmp.encrypt(@cipher)
45
+
46
+ PRECOMPUTED_BLOCKS.times.each do
47
+ block = Internals::Block.new(tmp.data.dup)
48
+ block.data.freeze
49
+ block.freeze
50
+
51
+ @l << block
52
+ tmp.dbl
53
+ end
54
+
55
+ @l.freeze
56
+
57
+ # Compute L(−1) ← L · x⁻¹:
58
+ #
59
+ # a>>1 if lastbit(a)=0
60
+ # (a>>1) ⊕ 10¹²⁰1000011 if lastbit(a)=1
61
+ #
62
+ @l_inv = Internals::Block.new(@l[0].data.dup)
63
+ last_bit = @l_inv[Internals::Block::SIZE - 1] & 0x01
64
+
65
+ (Internals::Block::SIZE - 1).downto(1) do |i|
66
+ carry = Internals::Util.ct_select(@l_inv[i - 1] & 1, 0x80, 0)
67
+ @l_inv[i] = (@l_inv[i] >> 1) | carry
68
+ end
69
+
70
+ @l_inv[0] >>= 1
71
+ @l_inv[0] ^= Internals::Util.ct_select(last_bit, 0x80, 0)
72
+ @l_inv[Internals::Block::SIZE - 1] ^= Internals::Util.ct_select(last_bit, Internals::Block::R >> 1, 0)
73
+ @l_inv.freeze
74
+ @l_inv.data.freeze
75
+ end
76
+
77
+ # Inspect this PMAC instance
78
+ #
79
+ # @return [String] description of this instance
80
+ def inspect
81
+ to_s
82
+ end
83
+
84
+ # Compute the PMAC of the given input message in a single shot,
85
+ # outputting the MAC tag.
86
+ #
87
+ # Unlike other PMAC implementations, this one does not support
88
+ # incremental processing/IUF operation. (Though that would enable
89
+ # slightly more efficient decryption for AES-SIV)
90
+ #
91
+ # @param message [String] an Encoding::BINARY string to authenticate
92
+ #
93
+ # @return [String] PMAC tag
94
+ def digest(message)
95
+ Internals::Util.validate_bytestring("message", message)
96
+
97
+ offset = Internals::Block.new
98
+ tmp = Internals::Block.new
99
+ tag = Internals::Block.new
100
+ ctr = 0
101
+ remaining = message.bytesize
102
+
103
+ while remaining > Internals::Block::SIZE
104
+ offset.xor_in_place(@l.fetch(Internals::Util.ctz(ctr + 1)))
105
+
106
+ tmp.copy(offset)
107
+ tmp.xor_in_place(message[ctr * Internals::Block::SIZE, Internals::Block::SIZE])
108
+ tmp.encrypt(@cipher)
109
+ tag.xor_in_place(tmp)
110
+
111
+ ctr += 1
112
+ remaining -= Internals::Block::SIZE
113
+ end
114
+
115
+ if remaining == Internals::Block::SIZE
116
+ tag.xor_in_place(message[(ctr * Internals::Block::SIZE)..-1])
117
+ tag.xor_in_place(@l_inv)
118
+ else
119
+ remaining.times { |i| tag[i] ^= message.getbyte((ctr * Internals::Block::SIZE) + i) }
120
+ tag[remaining] ^= 0x80
121
+ end
122
+
123
+ tag.encrypt(@cipher)
124
+ tag.data
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,123 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ # The Advanced Encryption Standard
6
+ module AES
7
+ # The SIV misuse resistant authenticated encryption mode
8
+ #
9
+ # This class is intended for power users. If you're uncertain if you
10
+ # should be using it, you probably want the `Miscreant::AEAD` API.
11
+ class SIV
12
+ # Generate a new random AES-SIV key of the given size
13
+ #
14
+ # @param size [Integer] size of key in bytes (32 or 64)
15
+ #
16
+ # @return [String] newly generated AES-SIV key
17
+ def self.generate_key(size = 32)
18
+ raise ArgumentError, "key size must be 32 or 64 bytes" unless [32, 64].include?(size)
19
+ SecureRandom.random_bytes(size)
20
+ end
21
+
22
+ # Create a new AES-SIV instance
23
+ #
24
+ # @param key [String] 32-byte or 64-byte Encoding::BINARY cryptographic key
25
+ # @param mac [:CMAC, :PMAC] (optional) MAC function to use (default CMAC)
26
+ def initialize(key, mac_class = :CMAC)
27
+ Internals::Util.validate_bytestring("key", key, length: [32, 64])
28
+ length = key.length / 2
29
+
30
+ case mac_class
31
+ when :CMAC
32
+ @mac = CMAC.new(key[0, length])
33
+ when :PMAC
34
+ @mac = PMAC.new(key[0, length])
35
+ else raise ArgumentError, "bad MAC class: #{mac_class} (expected :CMAC or :PMAC)"
36
+ end
37
+
38
+ @ctr = Internals::AES::CTR.new(key[length..-1])
39
+ end
40
+
41
+ # Inspect this AES-SIV instance
42
+ #
43
+ # @return [String] description of this instance
44
+ def inspect
45
+ to_s
46
+ end
47
+
48
+ # Encrypt a message using AES-SIV, authenticating it along with the associated data
49
+ #
50
+ # @param plaintext [String] an Encoding::BINARY string to encrypt
51
+ # @param associated_data [Array<String>] optional array of message headers to authenticate
52
+ #
53
+ # @return [String] encrypted ciphertext
54
+ def seal(plaintext, associated_data = [])
55
+ raise TypeError, "expected String, got #{plaintext.class}" unless plaintext.is_a?(String)
56
+ v = _s2v(associated_data, plaintext)
57
+ ciphertext = @ctr.encrypt(_zero_iv_bits(v), plaintext)
58
+ v + ciphertext
59
+ end
60
+
61
+ # Verify and decrypt an AES-SIV ciphertext, authenticating it along with the associated data
62
+ #
63
+ # @param ciphertext [String] an Encoding::BINARY string to decrypt
64
+ # @param associated_data [Array<String>] optional array of message headers to authenticate
65
+ #
66
+ # @raise [Miscreant::IntegrityError] ciphertext and/or associated data are corrupt or tampered with
67
+ # @return [String] decrypted plaintext
68
+ def open(ciphertext, associated_data = [])
69
+ raise TypeError, "expected String, got #{ciphertext.class}" unless ciphertext.is_a?(String)
70
+ v = ciphertext[0, Internals::Block::SIZE]
71
+ plaintext = @ctr.encrypt(_zero_iv_bits(v), ciphertext[Internals::Block::SIZE..-1])
72
+ t = _s2v(associated_data, plaintext)
73
+ raise IntegrityError, "ciphertext verification failure!" unless Internals::Util.ct_equal(t, v)
74
+
75
+ plaintext
76
+ end
77
+
78
+ private
79
+
80
+ # The S2V operation consists of the doubling and XORing of the outputs
81
+ # of the pseudo-random function CMAC.
82
+ #
83
+ # See Section 2.4 of RFC 5297 for more information
84
+ def _s2v(associated_data, plaintext)
85
+ # Note: the standalone S2V returns CMAC(1) if the number of passed
86
+ # vectors is zero, however in SIV construction this case is never
87
+ # triggered, since we always pass plaintext as the last vector (even
88
+ # if it's zero-length), so we omit this case.
89
+ d = Internals::Block.new
90
+ d.xor_in_place(@mac.digest(d.data))
91
+
92
+ associated_data.each do |ad|
93
+ d.dbl
94
+ d.xor_in_place(@mac.digest(ad))
95
+ end
96
+
97
+ if plaintext.bytesize >= Internals::Block::SIZE
98
+ # TODO: implement this more efficiently by adding IUF support to CMAC
99
+ difference = plaintext.length - Internals::Block::SIZE
100
+ beginning = plaintext[0, difference]
101
+ d.xor_in_place(plaintext[difference..-1])
102
+ msg = beginning + d.data
103
+ else
104
+ d.dbl
105
+ d.xor_in_place(Internals::Util.pad(plaintext, Internals::Block::SIZE))
106
+ msg = d.data
107
+ end
108
+
109
+ @mac.digest(msg)
110
+ end
111
+
112
+ # "We zero-out the top bit in each of the last two 32-bit words
113
+ # of the IV before assigning it to Ctr"
114
+ # -- http://web.cs.ucdavis.edu/~rogaway/papers/siv.pdf
115
+ def _zero_iv_bits(iv)
116
+ iv = iv.dup
117
+ iv.setbyte(8, iv.getbyte(8) & 0x7f)
118
+ iv.setbyte(12, iv.getbyte(12) & 0x7f)
119
+ iv
120
+ end
121
+ end
122
+ end
123
+ end
@@ -1,9 +1,10 @@
1
+ # encoding: binary
1
2
  # frozen_string_literal: true
2
3
 
3
- require "miscreant/internals/aes"
4
- require "miscreant/internals/aes/siv"
5
- require "miscreant/internals/aes/cmac"
4
+ require "miscreant/internals/block"
6
5
  require "miscreant/internals/util"
6
+ require "miscreant/internals/aes/block_cipher"
7
+ require "miscreant/internals/aes/ctr"
7
8
 
8
9
  module Miscreant
9
10
  # Internal functionality not intended for direct consumption
@@ -0,0 +1,48 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ module Internals
6
+ module AES # :nodoc:
7
+ # The AES cipher in a raw block mode (a.k.a. ECB mode)
8
+ #
9
+ # NOTE: The only valid use of ECB mode is constructing higher-level
10
+ # cryptographic primitives. This library uses this class to implement
11
+ # the CMAC and PMAC message authentication codes.
12
+ class BlockCipher # :nodoc:
13
+ # Create a new block cipher instance
14
+ #
15
+ # @param key [String] a random 16-byte or 32-byte Encoding::BINARY encryption key
16
+ #
17
+ # @raise [TypeError] the key was not a String
18
+ # @raise [ArgumentError] the key was the wrong length or encoding
19
+ def initialize(key)
20
+ Util.validate_bytestring("key", key, length: [16, 32])
21
+
22
+ @cipher = OpenSSL::Cipher.new("AES-#{key.length * 8}-ECB")
23
+ @cipher.encrypt
24
+ @cipher.padding = 0
25
+ @cipher.key = key
26
+ end
27
+
28
+ # Inspect this AES block cipher instance
29
+ #
30
+ # @return [String] description of this instance
31
+ def inspect
32
+ to_s
33
+ end
34
+
35
+ # Encrypt the given AES block-sized message
36
+ #
37
+ # @param message [String] a 16-byte Encoding::BINARY message to encrypt
38
+ #
39
+ # @raise [TypeError] the message was not a String
40
+ # @raise [ArgumentError] the message was the wrong length
41
+ def encrypt(message)
42
+ Util.validate_bytestring("message", message, length: Block::SIZE)
43
+ @cipher.update(message) + @cipher.final
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,40 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ module Internals
6
+ module AES # :nodoc:
7
+ # The AES-CTR unauthenticated stream cipher
8
+ class CTR # :nodoc:
9
+ # Create a new AES-CTR instance
10
+ #
11
+ # @param key [String] 16-byte or 32-byte Encoding::BINARY cryptographic key
12
+ def initialize(key)
13
+ Util.validate_bytestring("key", key, length: [16, 32])
14
+ @cipher = OpenSSL::Cipher::AES.new(key.bytesize * 8, :CTR)
15
+ @cipher.encrypt
16
+ @cipher.key = key
17
+ end
18
+
19
+ # Inspect this AES-CTR instance
20
+ #
21
+ # @return [String] description of this instance
22
+ def inspect
23
+ to_s
24
+ end
25
+
26
+ # Encrypt the given message using the given counter (i.e. IV)
27
+ #
28
+ # @param iv [String] initial counter value as a 16-byte Encoding::BINARY string
29
+ # @param message [String] message to be encrypted
30
+ def encrypt(iv, message)
31
+ Util.validate_bytestring("IV", iv, length: Block::SIZE)
32
+ return "".b if message.empty?
33
+
34
+ @cipher.iv = iv
35
+ @cipher.update(message) + @cipher.final
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,103 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ module Internals
6
+ # A 128-bit block (i.e. for AES)
7
+ class Block # :nodoc:
8
+ # Size of an AES block in bytes
9
+ SIZE = 16
10
+
11
+ # Minimal irreducible polynomial for a 128-bit block size
12
+ R = 0x87
13
+
14
+ attr_reader :data
15
+
16
+ # Create a new Block, optionally from the given data
17
+ def initialize(data = nil)
18
+ if data
19
+ @data = Util.validate_bytestring("block data", data, length: SIZE)
20
+ else
21
+ @data = "\0".b * SIZE
22
+ end
23
+ end
24
+
25
+ # Inspect the contents of the block in hex
26
+ def inspect
27
+ "#<#{self.class} data:\"#{@data.unpack('H*').first}\">"
28
+ end
29
+
30
+ # Retrieve the value of the byte at the given index as an integer
31
+ def [](n)
32
+ raise IndexError, "n must be zero or greater (got #{n})" if n < 0
33
+ raise IndexError, "n must be less than #{SIZE} (got #{n})" unless n < SIZE
34
+
35
+ @data.getbyte(n)
36
+ end
37
+
38
+ # Set the value of the byte at the given index as an integer
39
+ def []=(n, byte)
40
+ @data.setbyte(n, byte)
41
+ end
42
+
43
+ # Reset the value of this block to all zeroes
44
+ def clear
45
+ SIZE.times { |n| @data[n] = 0 }
46
+ end
47
+
48
+ # Copy the contents of another block into this block
49
+ def copy(other_block)
50
+ SIZE.times { |n| @data[n] = other_block.data[n] }
51
+ end
52
+
53
+ # Double a value over GF(2^128):
54
+ #
55
+ # a<<1 if firstbit(a)=0
56
+ # (a<<1) ⊕ 0¹²⁰10000111 if firstbit(a)=1
57
+ #
58
+ def dbl
59
+ overflow = 0
60
+ words = @data.unpack("N4").reverse
61
+
62
+ words.map! do |word|
63
+ new_word = (word << 1) & 0xFFFFFFFF
64
+ new_word |= overflow
65
+ overflow = (word & 0x80000000) >= 0x80000000 ? 1 : 0
66
+ new_word
67
+ end
68
+
69
+ @data = words.reverse.pack("N4")
70
+ @data[-1] = (@data[-1].ord ^ Util.ct_select(overflow, R, 0)).chr
71
+ self
72
+ end
73
+
74
+ # Encrypt this block in-place, replacing its current contents with
75
+ # their ciphertext under the given block cipher
76
+ #
77
+ # @param cipher [Miscreant::Internals::AES::BlockCipher] block cipher to encrypt with
78
+ def encrypt(cipher)
79
+ raise TypeError, "invalid cipher: #{cipher.class}" unless cipher.is_a?(AES::BlockCipher)
80
+
81
+ # TODO: more efficient in-place encryption
82
+ @data = cipher.encrypt(@data)
83
+ end
84
+
85
+ # XOR the given data into the current block in-place
86
+ #
87
+ # @param value [AES::Block, String] a block or String to XOR into this one
88
+ def xor_in_place(value)
89
+ case value
90
+ when Block
91
+ value = value.data
92
+ when String
93
+ Util.validate_bytestring("value", value, length: SIZE)
94
+ else raise TypeError, "invalid XOR input: #{value.class}"
95
+ end
96
+
97
+ SIZE.times do |i|
98
+ self[i] ^= value.getbyte(i)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -7,76 +7,72 @@ module Miscreant
7
7
  module Util # :nodoc:
8
8
  module_function
9
9
 
10
- # Perform a doubling operation as described in the CMAC and SIV papers
11
- def dbl(value)
12
- overflow = 0
13
- words = value.unpack("N4").reverse
10
+ # :nodoc: Lookup table for the number of trailing zeroes in a byte
11
+ CTZ_TABLE = [
12
+ 8, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
13
+ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
14
+ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
15
+ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
16
+ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
17
+ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
18
+ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
19
+ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
20
+ 7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
21
+ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
22
+ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
23
+ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
24
+ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
25
+ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
26
+ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
27
+ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0
28
+ ].freeze
14
29
 
15
- words.map! do |word|
16
- new_word = (word << 1) & 0xFFFFFFFF
17
- new_word |= overflow
18
- overflow = (word & 0x80000000) >= 0x80000000 ? 1 : 0
19
- new_word
20
- end
30
+ # Perform a constant time-ish comparison of two bytestrings
31
+ def ct_equal(a, b)
32
+ return false unless a.bytesize == b.bytesize
21
33
 
22
- result = words.reverse.pack("N4")
23
- result[-1] = (result[-1].ord ^ select(overflow, 0x87, 0)).chr
24
- result
25
- end
34
+ l = a.unpack("C*")
35
+ r = 0
36
+ i = -1
26
37
 
27
- # Pad value with a 0x80 value and zeroes up to the given length
28
- def pad(message, length)
29
- padded_length = message.length + length - (message.length % length)
30
- message += "\x80"
31
- message.ljust(padded_length, "\0")
38
+ b.each_byte { |v| r |= v ^ l[i += 1] }
39
+ r.zero?
32
40
  end
33
41
 
34
42
  # Perform a constant time(-ish) branch operation
35
- def select(subject, result_if_one, result_if_zero)
43
+ def ct_select(subject, result_if_one, result_if_zero)
36
44
  (~(subject - 1) & result_if_one) | ((subject - 1) & result_if_zero)
37
45
  end
38
46
 
39
- # Perform an xor on arbitrary bytestrings
40
- def xor(a, b)
41
- length = [a.length, b.length].min
42
- output = "\0" * length
43
- length.times do |i|
44
- output[i] = (a[i].ord ^ b[i].ord).chr
45
- end
46
- output
47
- end
48
-
49
- # XOR the second value into the end of the first
50
- def xorend(a, b)
51
- difference = a.length - b.length
52
-
53
- left = a.slice(0, difference)
54
- right = a.slice(difference..-1)
55
-
56
- left + xor(right, b)
47
+ # Count the number of zeros in a given 8-bit integer
48
+ #
49
+ # @param value [Integer] an integer in the 8-bit unsigned range (0-255)
50
+ def ctz(value)
51
+ CTZ_TABLE.fetch(value)
57
52
  end
58
53
 
59
- # Zero out the top bits in the last 32-bit words of the IV
60
- def zero_iv_bits(iv)
61
- # "We zero-out the top bit in each of the last two 32-bit words
62
- # of the IV before assigning it to Ctr"
63
- # -- http://web.cs.ucdavis.edu/~rogaway/papers/siv.pdf
64
- iv = iv.dup
65
- iv[8] = (iv[8].ord & 0x7f).chr
66
- iv[12] = (iv[12].ord & 0x7f).chr
67
- iv
54
+ # Pad value with a 0x80 value and zeroes up to the given length
55
+ def pad(message, length)
56
+ padded_length = message.length + length - (message.length % length)
57
+ message += "\x80"
58
+ message.ljust(padded_length, "\0")
68
59
  end
69
60
 
70
- # Perform a constant time-ish comparison of two bytestrings
71
- def ct_equal(a, b)
72
- return false unless a.bytesize == b.bytesize
61
+ # Ensure a string is a valid bytestring (potentially of a given length)
62
+ def validate_bytestring(name, string, length: nil)
63
+ raise TypeError, "expected String for #{name}, got #{string.class}" unless string.is_a?(String)
64
+ raise ArgumentError, "#{name} must be Encoding::BINARY" unless string.encoding == Encoding::BINARY
73
65
 
74
- l = a.unpack("C*")
75
- r = 0
76
- i = -1
66
+ case length
67
+ when Array
68
+ raise ArgumentError, "#{name} must be #{length.join(' or ')} bytes long" unless length.include?(string.bytesize)
69
+ when Integer
70
+ raise ArgumentError, "#{name} must be #{length}-bytes long" unless string.bytesize == length
71
+ else
72
+ raise TypeError, "bad length parameter: #{length.inspect} (#{length.class}" unless length.nil?
73
+ end
77
74
 
78
- b.each_byte { |v| r |= v ^ l[i += 1] }
79
- r.zero?
75
+ string
80
76
  end
81
77
  end
82
78
  end
@@ -1,5 +1,6 @@
1
+ # encoding: binary
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module Miscreant
4
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
5
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: miscreant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Arcieri
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-07-31 00:00:00.000000000 Z
11
+ date: 2017-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -39,10 +39,14 @@ files:
39
39
  - README.md
40
40
  - Rakefile
41
41
  - lib/miscreant.rb
42
+ - lib/miscreant/aead.rb
43
+ - lib/miscreant/aes/cmac.rb
44
+ - lib/miscreant/aes/pmac.rb
45
+ - lib/miscreant/aes/siv.rb
42
46
  - lib/miscreant/internals.rb
43
- - lib/miscreant/internals/aes.rb
44
- - lib/miscreant/internals/aes/cmac.rb
45
- - lib/miscreant/internals/aes/siv.rb
47
+ - lib/miscreant/internals/aes/block_cipher.rb
48
+ - lib/miscreant/internals/aes/ctr.rb
49
+ - lib/miscreant/internals/block.rb
46
50
  - lib/miscreant/internals/util.rb
47
51
  - lib/miscreant/version.rb
48
52
  - miscreant.gemspec
@@ -1,15 +0,0 @@
1
- # encoding: binary
2
- # frozen_string_literal: true
3
-
4
- module Miscreant
5
- module Internals
6
- # The Advanced Encryption Standard Block Cipher
7
- module AES # :nodoc:
8
- # Size of an AES block (i.e. input/output from the AES function)
9
- BLOCK_SIZE = 16
10
-
11
- # A bytestring of all zeroes, the same length as an AES block
12
- ZERO_BLOCK = ("\0" * BLOCK_SIZE).freeze
13
- end
14
- end
15
- end
@@ -1,79 +0,0 @@
1
- # encoding: binary
2
- # frozen_string_literal: true
3
-
4
- module Miscreant
5
- module Internals
6
- module AES
7
- # The AES-CMAC message authentication code
8
- class CMAC # :nodoc:
9
- # Create a new AES-CMAC instance
10
- #
11
- # @param key [String] 16-byte or 32-byte Encoding::BINARY cryptographic key
12
- #
13
- # @return [Miscreant::AES::CMAC] new AES-CMAC instance
14
- def initialize(key)
15
- raise TypeError, "expected String, got #{key.class}" unless key.is_a?(String)
16
- raise ArgumentError, "key must be Encoding::BINARY" unless key.encoding == Encoding::BINARY
17
- raise ArgumentError, "key must be 32 or 64 bytes" unless [16, 32].include?(key.length)
18
-
19
- # The only valid use of ECB mode: constructing higher-level cryptographic primitives
20
- @cipher = OpenSSL::Cipher.new("AES-#{key.length * 8}-ECB")
21
- @cipher.encrypt
22
- @cipher.padding = 0
23
- @cipher.key = key
24
- @key1, @key2 = _generate_subkeys
25
- end
26
-
27
- # Inspect this AES-CMAC instance
28
- #
29
- # @return [String] description of this instance
30
- def inspect
31
- to_s
32
- end
33
-
34
- # Compute the AES-CMAC of the given input message in a single shot,
35
- # outputting the MAC tag.
36
- #
37
- # Unlike other AES-CMAC implementations, this one does not support
38
- # incremental processing/IUF operation. (Though that would enable
39
- # slightly more efficient decryption for AES-SIV)
40
- #
41
- # @param message [String] an Encoding::BINARY string to authenticate
42
- #
43
- # @return [String] CMAC tag
44
- def digest(message)
45
- raise TypeError, "expected String, got #{message.class}" unless message.is_a?(String)
46
- raise ArgumentError, "message must be Encoding::BINARY" unless message.encoding == Encoding::BINARY
47
-
48
- if message.empty? || message.length % AES::BLOCK_SIZE != 0
49
- message = Util.pad(message, AES::BLOCK_SIZE)
50
- final_block = @key2
51
- else
52
- final_block = @key1
53
- end
54
-
55
- count = message.length / AES::BLOCK_SIZE
56
- result = AES::ZERO_BLOCK
57
-
58
- count.times do |i|
59
- block = message.slice(AES::BLOCK_SIZE * i, AES::BLOCK_SIZE)
60
- block = Util.xor(final_block, block) if i == count - 1
61
- block = Util.xor(block, result)
62
- result = @cipher.update(block) + @cipher.final
63
- end
64
-
65
- result
66
- end
67
-
68
- private
69
-
70
- def _generate_subkeys
71
- key0 = @cipher.update(AES::ZERO_BLOCK) + @cipher.final
72
- key1 = Util.dbl(key0)
73
- key2 = Util.dbl(key1)
74
- [key1, key2]
75
- end
76
- end
77
- end
78
- end
79
- end
@@ -1,120 +0,0 @@
1
- # encoding: binary
2
- # frozen_string_literal: true
3
-
4
- module Miscreant
5
- module Internals
6
- module AES
7
- # The AES-SIV misuse resistant authenticated encryption cipher
8
- class SIV # :nodoc:
9
- # Generate a new random AES-SIV key of the given size
10
- #
11
- # @param size [Integer] size of key in bytes (32 or 64)
12
- #
13
- # @return [String] newly generated AES-SIV key
14
- def self.generate_key(size = 32)
15
- raise ArgumentError, "key size must be 32 or 64 bytes" unless [32, 64].include?(size)
16
- SecureRandom.random_bytes(size)
17
- end
18
-
19
- # Create a new AES-SIV instance
20
- #
21
- # @param key [String] 32-byte or 64-byte Encoding::BINARY cryptographic key
22
- #
23
- # @return [Miscreant::AES::SIV] new AES-SIV instance
24
- def initialize(key)
25
- raise TypeError, "expected String, got #{key.class}" unless key.is_a?(String)
26
- raise ArgumentError, "key must be Encoding::BINARY" unless key.encoding == Encoding::BINARY
27
- raise ArgumentError, "key must be 32 or 64 bytes" unless [32, 64].include?(key.length)
28
-
29
- length = key.length / 2
30
-
31
- @mac_key = key.slice(0, length)
32
- @enc_key = key.slice(length..-1)
33
- end
34
-
35
- # Inspect this AES-SIV instance
36
- #
37
- # @return [String] description of this instance
38
- def inspect
39
- to_s
40
- end
41
-
42
- # Encrypt a message using AES-SIV, authenticating it along with the associated data
43
- #
44
- # @param plaintext [String] an Encoding::BINARY string to encrypt
45
- # @param associated_data [Array<String>] optional array of message headers to authenticate
46
- #
47
- # @return [String] encrypted ciphertext
48
- def seal(plaintext, associated_data = [])
49
- raise TypeError, "expected String, got #{plaintext.class}" unless plaintext.is_a?(String)
50
- raise ArgumentError, "plaintext must be Encoding::BINARY" unless plaintext.encoding == Encoding::BINARY
51
-
52
- v = _s2v(associated_data, plaintext)
53
- ciphertext = _transform(v, plaintext)
54
- v + ciphertext
55
- end
56
-
57
- # Verify and decrypt an AES-SIV ciphertext, authenticating it along with the associated data
58
- #
59
- # @param ciphertext [String] an Encoding::BINARY string to decrypt
60
- # @param associated_data [Array<String>] optional array of message headers to authenticate
61
- #
62
- # @raise [Miscreant::IntegrityError] ciphertext and/or associated data are corrupt or tampered with
63
- # @return [String] decrypted plaintext
64
- def open(ciphertext, associated_data = [])
65
- raise TypeError, "expected String, got #{ciphertext.class}" unless ciphertext.is_a?(String)
66
- raise ArgumentError, "ciphertext must be Encoding::BINARY" unless ciphertext.encoding == Encoding::BINARY
67
-
68
- v = ciphertext.slice(0, AES::BLOCK_SIZE)
69
- ciphertext = ciphertext.slice(AES::BLOCK_SIZE..-1)
70
- plaintext = _transform(v, ciphertext)
71
-
72
- t = _s2v(associated_data, plaintext)
73
- raise IntegrityError, "ciphertext verification failure!" unless Util.ct_equal(t, v)
74
-
75
- plaintext
76
- end
77
-
78
- private
79
-
80
- # Performs raw unauthenticted encryption or decryption of the message
81
- def _transform(v, data)
82
- return "".b if data.empty?
83
-
84
- cipher = OpenSSL::Cipher::AES.new(@mac_key.length * 8, :CTR)
85
- cipher.encrypt
86
- cipher.iv = Util.zero_iv_bits(v)
87
- cipher.key = @enc_key
88
- cipher.update(data) + cipher.final
89
- end
90
-
91
- # The S2V operation consists of the doubling and XORing of the outputs
92
- # of the pseudo-random function CMAC.
93
- #
94
- # See Section 2.4 of RFC 5297 for more information
95
- def _s2v(associated_data, plaintext)
96
- # Note: the standalone S2V returns CMAC(1) if the number of passed
97
- # vectors is zero, however in SIV construction this case is never
98
- # triggered, since we always pass plaintext as the last vector (even
99
- # if it's zero-length), so we omit this case.
100
- cmac = CMAC.new(@mac_key)
101
- d = cmac.digest(AES::ZERO_BLOCK)
102
-
103
- associated_data.each do |ad|
104
- d = Util.dbl(d)
105
- d = Util.xor(d, cmac.digest(ad))
106
- end
107
-
108
- if plaintext.bytesize >= AES::BLOCK_SIZE
109
- d = Util.xorend(plaintext, d)
110
- else
111
- d = Util.dbl(d)
112
- d = Util.xor(d, Util.pad(plaintext, AES::BLOCK_SIZE))
113
- end
114
-
115
- cmac.digest(d)
116
- end
117
- end
118
- end
119
- end
120
- end