miscreant 0.0.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 +7 -0
- data/.gitignore +13 -0
- data/.rspec +4 -0
- data/.rubocop.yml +34 -0
- data/.ruby-version +1 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +19 -0
- data/README.md +142 -0
- data/Rakefile +11 -0
- data/lib/miscreant.rb +20 -0
- data/lib/miscreant/aes.rb +13 -0
- data/lib/miscreant/aes/cmac.rb +77 -0
- data/lib/miscreant/aes/siv.rb +118 -0
- data/lib/miscreant/util.rb +81 -0
- data/lib/miscreant/version.rb +5 -0
- data/miscreant.gemspec +25 -0
- metadata +74 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c3b2740135b6e13589326c9b8f2c926edff86516
|
4
|
+
data.tar.gz: b4327cc7c72ce38aaec0208f967c67fdceb2508f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b24730c57fd7ef67a4e7699a63c05454875ffec00366212a31445111525b7fbd953d122f8ff88e1871e0f7caee792780394382b7b9021fb9a83772fbf8d2ad6f
|
7
|
+
data.tar.gz: ceafedfa57c9b6d6ca7a72d8d50e31041af4d9c5113ea7e2d6482ef49a456914411138d6a75f43f5f67340f8aed5fe71bac0a5bbe7581734c2379681e3e1cf15
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
AllCops:
|
2
|
+
DisplayCopNames: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# Metrics
|
6
|
+
#
|
7
|
+
|
8
|
+
Metrics/AbcSize:
|
9
|
+
Enabled: false
|
10
|
+
|
11
|
+
Metrics/BlockLength:
|
12
|
+
Max: 50
|
13
|
+
|
14
|
+
Metrics/CyclomaticComplexity:
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
Metrics/PerceivedComplexity:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
Metrics/ClassLength:
|
21
|
+
Max: 100
|
22
|
+
|
23
|
+
Metrics/LineLength:
|
24
|
+
Max: 128
|
25
|
+
|
26
|
+
Metrics/MethodLength:
|
27
|
+
Max: 25
|
28
|
+
|
29
|
+
#
|
30
|
+
# Style
|
31
|
+
#
|
32
|
+
|
33
|
+
Style/StringLiterals:
|
34
|
+
EnforcedStyle: double_quotes
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.1
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2013-2017 John Downey, The Miscreant Developers
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
# miscreant.rb [![Latest Version][gem-shield]][gem-link] [![Build Status][build-image]][build-link] [![Code Climate][codeclimate-image]][codeclimate-link] [![MIT licensed][license-image]][license-link]
|
2
|
+
|
3
|
+
[gem-shield]: https://badge.fury.io/rb/miscreant.svg
|
4
|
+
[gem-link]: https://rubygems.org/gems/miscreant
|
5
|
+
[build-image]: https://secure.travis-ci.org/miscreant/miscreant.svg?branch=master
|
6
|
+
[build-link]: http://travis-ci.org/miscreant/miscreant
|
7
|
+
[codeclimate-image]: https://codeclimate.com/github/miscreant/miscreant/badges/gpa.svg
|
8
|
+
[codeclimate-link]: https://codeclimate.com/github/miscreant/miscreant
|
9
|
+
[license-image]: https://img.shields.io/badge/license-MIT-blue.svg
|
10
|
+
[license-link]: https://github.com/miscreant/miscreant/blob/master/LICENSE.txt
|
11
|
+
|
12
|
+
> The best crypto you've never heard of, brought to you by [Phil Rogaway]
|
13
|
+
|
14
|
+
Ruby implementation of **Miscreant**: Advanced symmetric encryption using the
|
15
|
+
AES-SIV ([RFC 5297]) and [CHAIN] constructions, providing easy-to-use (or
|
16
|
+
rather, hard-to-misuse) encryption of individual messages or message streams.
|
17
|
+
|
18
|
+
**AES-SIV** provides [nonce-reuse misuse-resistance] (NRMR): accidentally
|
19
|
+
reusing a nonce with this construction is not a security catastrophe,
|
20
|
+
unlike it is with more popular AES encryption modes like [AES-GCM].
|
21
|
+
With **AES-SIV**, the worst outcome of reusing a nonce is an attacker
|
22
|
+
can see you've sent the same plaintext twice, as opposed to almost all other
|
23
|
+
AES modes where it can facilitate [chosen ciphertext attacks] and/or
|
24
|
+
full plaintext recovery.
|
25
|
+
|
26
|
+
For more information, see the [toplevel README.md].
|
27
|
+
|
28
|
+
[Phil Rogaway]: https://en.wikipedia.org/wiki/Phillip_Rogaway
|
29
|
+
[AES-SIV]: https://www.iacr.org/archive/eurocrypt2006/40040377/40040377.pdf
|
30
|
+
[RFC 5297]: https://tools.ietf.org/html/rfc5297
|
31
|
+
[CHAIN]: http://web.cs.ucdavis.edu/~rogaway/papers/oae.pdf
|
32
|
+
[nonce-reuse misuse-resistance]: https://www.lvh.io/posts/nonce-misuse-resistance-101.html
|
33
|
+
[AES-GCM]: https://en.wikipedia.org/wiki/Galois/Counter_Mode
|
34
|
+
[chosen ciphertext attacks]: https://en.wikipedia.org/wiki/Chosen-ciphertext_attack
|
35
|
+
[toplevel README.md]: https://github.com/miscreant/miscreant/blob/master/README.md
|
36
|
+
|
37
|
+
## Help and Discussion
|
38
|
+
|
39
|
+
Have questions? Want to suggest a feature or change?
|
40
|
+
|
41
|
+
* [Gitter]: web-based chat about miscreant projects including **miscreant.rb**
|
42
|
+
* [Google Group]: join via web or email ([miscreant+subscribe@googlegroups.com])
|
43
|
+
|
44
|
+
[Gitter]: https://gitter.im/miscreant/Lobby
|
45
|
+
[Google Group]: https://groups.google.com/forum/#!forum/miscreant
|
46
|
+
[miscreant+subscribe@googlegroups.com]: mailto:miscreant+subscribe@googlegroups.com
|
47
|
+
|
48
|
+
## Security Notice
|
49
|
+
|
50
|
+
Though this library is written by cryptographic professionals, it has not
|
51
|
+
undergone a thorough security audit, and cryptographic professionals are still
|
52
|
+
humans that make mistakes. Use this library at your own risk.
|
53
|
+
|
54
|
+
## Requirements
|
55
|
+
|
56
|
+
This library is tested against the following MRI versions:
|
57
|
+
|
58
|
+
- 2.2
|
59
|
+
- 2.3
|
60
|
+
- 2.4
|
61
|
+
|
62
|
+
Other Ruby versions may work, but are not officially supported.
|
63
|
+
|
64
|
+
## Installation
|
65
|
+
|
66
|
+
Add this line to your application's Gemfile:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
gem "miscreant"
|
70
|
+
```
|
71
|
+
|
72
|
+
And then execute:
|
73
|
+
|
74
|
+
$ bundle
|
75
|
+
|
76
|
+
Or install it yourself as:
|
77
|
+
|
78
|
+
$ gem install miscreant
|
79
|
+
|
80
|
+
## API
|
81
|
+
|
82
|
+
### Miscreant::AES::SIV
|
83
|
+
|
84
|
+
The `Miscreant::AES::SIV` class provides the main interface to the **AES-SIV**
|
85
|
+
misuse resistant authenticated encryption function.
|
86
|
+
|
87
|
+
To make a new instance, pass in a 32-byte or 64-byte key. Note that these
|
88
|
+
options are twice the size of what you might be expecting (AES-SIV uses two
|
89
|
+
AES keys).
|
90
|
+
|
91
|
+
You can generate a random key using the `generate_key` method (default 32 bytes):
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
key_bytes = Miscreant::AES::SIV.generate_key
|
95
|
+
key = Miscreant::AES::SIV.new(key_bytes)
|
96
|
+
# => #<Miscreant::AES::SIV:0x007fe0109e85e8>
|
97
|
+
```
|
98
|
+
|
99
|
+
#### Encryption (#seal)
|
100
|
+
|
101
|
+
The `Miscreant::AES::SIV#seal` method encrypts a message along with a set of
|
102
|
+
*associated data* message headers.
|
103
|
+
|
104
|
+
It's recommended to include a unique "nonce" value with each message. This
|
105
|
+
prevents those who may be observing your ciphertexts from being able to tell
|
106
|
+
if you encrypted the same message twice. However, unlike other cryptographic
|
107
|
+
algorithms where using a nonce has catastrophic security implications such as
|
108
|
+
key recovery, reusing a nonce with AES-SIV only leaks repeated ciphertexts to
|
109
|
+
attackers.
|
110
|
+
|
111
|
+
Example:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
message = "Hello, world!"
|
115
|
+
nonce = SecureRandom.random_bytes(16)
|
116
|
+
ciphertext = key.seal(message, nonce)
|
117
|
+
```
|
118
|
+
|
119
|
+
#### Decryption (#open)
|
120
|
+
|
121
|
+
The `Miscreant::AES::SIV#open` method decrypts a ciphertext with the given key.
|
122
|
+
|
123
|
+
Example:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
message = "Hello, world!"
|
127
|
+
nonce = SecureRandom.random_bytes(16)
|
128
|
+
ciphertext = key.seal(message, nonce)
|
129
|
+
plaintext = key.open(ciphertext, nonce)
|
130
|
+
```
|
131
|
+
|
132
|
+
## Contributing
|
133
|
+
|
134
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/miscreant/miscreant
|
135
|
+
|
136
|
+
## Copyright
|
137
|
+
|
138
|
+
Copyright (c) 2013-2017 John Downey, [The Miscreant Developers][AUTHORS].
|
139
|
+
See [LICENSE.txt] for further details.
|
140
|
+
|
141
|
+
[AUTHORS]: https://github.com/miscreant/miscreant/blob/master/AUTHORS.md
|
142
|
+
[LICENSE.txt]: https://github.com/miscreant/miscreant/blob/master/ruby/LICENSE.txt
|
data/Rakefile
ADDED
data/lib/miscreant.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
require "securerandom"
|
5
|
+
|
6
|
+
require "miscreant/version"
|
7
|
+
|
8
|
+
require "miscreant/aes"
|
9
|
+
require "miscreant/aes/siv"
|
10
|
+
require "miscreant/aes/cmac"
|
11
|
+
require "miscreant/util"
|
12
|
+
|
13
|
+
# Misuse-resistant symmetric encryption using the AES-SIV (RFC 5297) and CHAIN constructions
|
14
|
+
module Miscreant
|
15
|
+
# Parent of all cryptography-related errors
|
16
|
+
CryptoError = Class.new(StandardError)
|
17
|
+
|
18
|
+
# Ciphertext failed to verify as authentic
|
19
|
+
IntegrityError = Class.new(CryptoError)
|
20
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Miscreant
|
5
|
+
# The Advanced Encryption Standard Block Cipher
|
6
|
+
module AES
|
7
|
+
# Size of an AES block (i.e. input/output from the AES function)
|
8
|
+
BLOCK_SIZE = 16
|
9
|
+
|
10
|
+
# A bytestring of all zeroes, the same length as an AES block
|
11
|
+
ZERO_BLOCK = ("\0" * BLOCK_SIZE).freeze
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Miscreant
|
5
|
+
module AES
|
6
|
+
# The AES-CMAC message authentication code
|
7
|
+
class CMAC
|
8
|
+
# Create a new AES-CMAC instance
|
9
|
+
#
|
10
|
+
# @param key [String] 16-byte or 32-byte Encoding::BINARY cryptographic key
|
11
|
+
#
|
12
|
+
# @return [Miscreant::AES::CMAC] new AES-CMAC instance
|
13
|
+
def initialize(key)
|
14
|
+
raise TypeError, "expected String, got #{key.class}" unless key.is_a?(String)
|
15
|
+
raise ArgumentError, "key must be Encoding::BINARY" unless key.encoding == Encoding::BINARY
|
16
|
+
raise ArgumentError, "key must be 32 or 64 bytes" unless [16, 32].include?(key.length)
|
17
|
+
|
18
|
+
# The only valid use of ECB mode: constructing higher-level cryptographic primitives
|
19
|
+
@cipher = OpenSSL::Cipher.new("AES-#{key.length * 8}-ECB")
|
20
|
+
@cipher.encrypt
|
21
|
+
@cipher.padding = 0
|
22
|
+
@cipher.key = key
|
23
|
+
@key1, @key2 = _generate_subkeys
|
24
|
+
end
|
25
|
+
|
26
|
+
# Inspect this AES-CMAC instance
|
27
|
+
#
|
28
|
+
# @return [String] description of this instance
|
29
|
+
def inspect
|
30
|
+
to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
# Compute the AES-CMAC of the given input message in a single shot,
|
34
|
+
# outputting the MAC tag.
|
35
|
+
#
|
36
|
+
# Unlike other AES-CMAC implementations, this one does not support
|
37
|
+
# incremental processing/IUF operation. (Though that would enable
|
38
|
+
# slightly more efficient decryption for AES-SIV)
|
39
|
+
#
|
40
|
+
# @param message [String] an Encoding::BINARY string to authenticate
|
41
|
+
#
|
42
|
+
# @return [String] CMAC tag
|
43
|
+
def digest(message)
|
44
|
+
raise TypeError, "expected String, got #{message.class}" unless message.is_a?(String)
|
45
|
+
raise ArgumentError, "message must be Encoding::BINARY" unless message.encoding == Encoding::BINARY
|
46
|
+
|
47
|
+
if message.empty? || message.length % AES::BLOCK_SIZE != 0
|
48
|
+
message = Util.pad(message, AES::BLOCK_SIZE)
|
49
|
+
final_block = @key2
|
50
|
+
else
|
51
|
+
final_block = @key1
|
52
|
+
end
|
53
|
+
|
54
|
+
count = message.length / AES::BLOCK_SIZE
|
55
|
+
result = AES::ZERO_BLOCK
|
56
|
+
|
57
|
+
count.times do |i|
|
58
|
+
block = message.slice(AES::BLOCK_SIZE * i, AES::BLOCK_SIZE)
|
59
|
+
block = Util.xor(final_block, block) if i == count - 1
|
60
|
+
block = Util.xor(block, result)
|
61
|
+
result = @cipher.update(block) + @cipher.final
|
62
|
+
end
|
63
|
+
|
64
|
+
result
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def _generate_subkeys
|
70
|
+
key0 = @cipher.update(AES::ZERO_BLOCK) + @cipher.final
|
71
|
+
key1 = Util.dbl(key0)
|
72
|
+
key2 = Util.dbl(key1)
|
73
|
+
[key1, key2]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Miscreant
|
5
|
+
module AES
|
6
|
+
# The AES-SIV misuse resistant authenticated encryption cipher
|
7
|
+
class SIV
|
8
|
+
# Generate a new random AES-SIV key of the given size
|
9
|
+
#
|
10
|
+
# @param size [Integer] size of key in bytes (32 or 64)
|
11
|
+
#
|
12
|
+
# @return [String] newly generated AES-SIV key
|
13
|
+
def self.generate_key(size = 32)
|
14
|
+
raise ArgumentError, "key size must be 32 or 64 bytes" unless [32, 64].include?(size)
|
15
|
+
SecureRandom.random_bytes(size)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Create a new AES-SIV instance
|
19
|
+
#
|
20
|
+
# @param key [String] 32-byte or 64-byte Encoding::BINARY cryptographic key
|
21
|
+
#
|
22
|
+
# @return [Miscreant::AES::SIV] new AES-SIV instance
|
23
|
+
def initialize(key)
|
24
|
+
raise TypeError, "expected String, got #{key.class}" unless key.is_a?(String)
|
25
|
+
raise ArgumentError, "key must be Encoding::BINARY" unless key.encoding == Encoding::BINARY
|
26
|
+
raise ArgumentError, "key must be 32 or 64 bytes" unless [32, 64].include?(key.length)
|
27
|
+
|
28
|
+
length = key.length / 2
|
29
|
+
|
30
|
+
@mac_key = key.slice(0, length)
|
31
|
+
@enc_key = key.slice(length..-1)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Inspect this AES-SIV instance
|
35
|
+
#
|
36
|
+
# @return [String] description of this instance
|
37
|
+
def inspect
|
38
|
+
to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
# Encrypt a message using AES-SIV, authenticating it along with the associated data
|
42
|
+
#
|
43
|
+
# @param plaintext [String] an Encoding::BINARY string to encrypt
|
44
|
+
# @param associated_data [Array<String>] optional array of message headers to authenticate
|
45
|
+
#
|
46
|
+
# @return [String] encrypted ciphertext
|
47
|
+
def seal(plaintext, associated_data = [])
|
48
|
+
raise TypeError, "expected String, got #{plaintext.class}" unless plaintext.is_a?(String)
|
49
|
+
raise ArgumentError, "plaintext must be Encoding::BINARY" unless plaintext.encoding == Encoding::BINARY
|
50
|
+
|
51
|
+
v = _s2v(associated_data, plaintext)
|
52
|
+
ciphertext = _transform(v, plaintext)
|
53
|
+
v + ciphertext
|
54
|
+
end
|
55
|
+
|
56
|
+
# Verify and decrypt an AES-SIV ciphertext, authenticating it along with the associated data
|
57
|
+
#
|
58
|
+
# @param ciphertext [String] an Encoding::BINARY string to decrypt
|
59
|
+
# @param associated_data [Array<String>] optional array of message headers to authenticate
|
60
|
+
#
|
61
|
+
# @raise [Miscreant::IntegrityError] ciphertext and/or associated data are corrupt or tampered with
|
62
|
+
# @return [String] decrypted plaintext
|
63
|
+
def open(ciphertext, associated_data = [])
|
64
|
+
raise TypeError, "expected String, got #{ciphertext.class}" unless ciphertext.is_a?(String)
|
65
|
+
raise ArgumentError, "ciphertext must be Encoding::BINARY" unless ciphertext.encoding == Encoding::BINARY
|
66
|
+
|
67
|
+
v = ciphertext.slice(0, AES::BLOCK_SIZE)
|
68
|
+
ciphertext = ciphertext.slice(AES::BLOCK_SIZE..-1)
|
69
|
+
plaintext = _transform(v, ciphertext)
|
70
|
+
|
71
|
+
t = _s2v(associated_data, plaintext)
|
72
|
+
raise IntegrityError, "ciphertext verification failure!" unless Util.ct_equal(t, v)
|
73
|
+
|
74
|
+
plaintext
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Performs raw unauthenticted encryption or decryption of the message
|
80
|
+
def _transform(v, data)
|
81
|
+
return "".b if data.empty?
|
82
|
+
|
83
|
+
cipher = OpenSSL::Cipher::AES.new(@mac_key.length * 8, :CTR)
|
84
|
+
cipher.encrypt
|
85
|
+
cipher.iv = Util.zero_iv_bits(v)
|
86
|
+
cipher.key = @enc_key
|
87
|
+
cipher.update(data) + cipher.final
|
88
|
+
end
|
89
|
+
|
90
|
+
# The S2V operation consists of the doubling and XORing of the outputs
|
91
|
+
# of the pseudo-random function CMAC.
|
92
|
+
#
|
93
|
+
# See Section 2.4 of RFC 5297 for more information
|
94
|
+
def _s2v(associated_data, plaintext)
|
95
|
+
# Note: the standalone S2V returns CMAC(1) if the number of passed
|
96
|
+
# vectors is zero, however in SIV construction this case is never
|
97
|
+
# triggered, since we always pass plaintext as the last vector (even
|
98
|
+
# if it's zero-length), so we omit this case.
|
99
|
+
cmac = CMAC.new(@mac_key)
|
100
|
+
d = cmac.digest(AES::ZERO_BLOCK)
|
101
|
+
|
102
|
+
associated_data.each do |ad|
|
103
|
+
d = Util.dbl(d)
|
104
|
+
d = Util.xor(d, cmac.digest(ad))
|
105
|
+
end
|
106
|
+
|
107
|
+
if plaintext.bytesize >= AES::BLOCK_SIZE
|
108
|
+
d = Util.xorend(plaintext, d)
|
109
|
+
else
|
110
|
+
d = Util.dbl(d)
|
111
|
+
d = Util.xor(d, Util.pad(plaintext, AES::BLOCK_SIZE))
|
112
|
+
end
|
113
|
+
|
114
|
+
cmac.digest(d)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Miscreant
|
5
|
+
# Internal utility functions
|
6
|
+
module Util
|
7
|
+
module_function
|
8
|
+
|
9
|
+
# Perform a doubling operation as described in the CMAC and SIV papers
|
10
|
+
def dbl(value)
|
11
|
+
overflow = 0
|
12
|
+
words = value.unpack("N4").reverse
|
13
|
+
|
14
|
+
words.map! do |word|
|
15
|
+
new_word = (word << 1) & 0xFFFFFFFF
|
16
|
+
new_word |= overflow
|
17
|
+
overflow = (word & 0x80000000) >= 0x80000000 ? 1 : 0
|
18
|
+
new_word
|
19
|
+
end
|
20
|
+
|
21
|
+
result = words.reverse.pack("N4")
|
22
|
+
result[-1] = (result[-1].ord ^ select(overflow, 0x87, 0)).chr
|
23
|
+
result
|
24
|
+
end
|
25
|
+
|
26
|
+
# Pad value with a 0x80 value and zeroes up to the given length
|
27
|
+
def pad(message, length)
|
28
|
+
padded_length = message.length + length - (message.length % length)
|
29
|
+
message += "\x80"
|
30
|
+
message.ljust(padded_length, "\0")
|
31
|
+
end
|
32
|
+
|
33
|
+
# Perform a constant time(-ish) branch operation
|
34
|
+
def select(subject, result_if_one, result_if_zero)
|
35
|
+
(~(subject - 1) & result_if_one) | ((subject - 1) & result_if_zero)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Perform an xor on arbitrary bytestrings
|
39
|
+
def xor(a, b)
|
40
|
+
length = [a.length, b.length].min
|
41
|
+
output = "\0" * length
|
42
|
+
length.times do |i|
|
43
|
+
output[i] = (a[i].ord ^ b[i].ord).chr
|
44
|
+
end
|
45
|
+
output
|
46
|
+
end
|
47
|
+
|
48
|
+
# XOR the second value into the end of the first
|
49
|
+
def xorend(a, b)
|
50
|
+
difference = a.length - b.length
|
51
|
+
|
52
|
+
left = a.slice(0, difference)
|
53
|
+
right = a.slice(difference..-1)
|
54
|
+
|
55
|
+
left + xor(right, b)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Zero out the top bits in the last 32-bit words of the IV
|
59
|
+
def zero_iv_bits(iv)
|
60
|
+
# "We zero-out the top bit in each of the last two 32-bit words
|
61
|
+
# of the IV before assigning it to Ctr"
|
62
|
+
# -- http://web.cs.ucdavis.edu/~rogaway/papers/siv.pdf
|
63
|
+
iv = iv.dup
|
64
|
+
iv[8] = (iv[8].ord & 0x7f).chr
|
65
|
+
iv[12] = (iv[12].ord & 0x7f).chr
|
66
|
+
iv
|
67
|
+
end
|
68
|
+
|
69
|
+
# Perform a constant time-ish comparison of two bytestrings
|
70
|
+
def ct_equal(a, b)
|
71
|
+
return false unless a.bytesize == b.bytesize
|
72
|
+
|
73
|
+
l = a.unpack("C*")
|
74
|
+
r = 0
|
75
|
+
i = -1
|
76
|
+
|
77
|
+
b.each_byte { |v| r |= v ^ l[i += 1] }
|
78
|
+
r.zero?
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/miscreant.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
lib = File.expand_path("../lib", __FILE__)
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
require "miscreant/version"
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = "miscreant"
|
10
|
+
spec.version = Miscreant::VERSION
|
11
|
+
spec.authors = ["Tony Arcieri"]
|
12
|
+
spec.email = ["bascule@gmail.com"]
|
13
|
+
spec.licenses = ["MIT"]
|
14
|
+
spec.homepage = "https://github.com/miscreant/miscreant/tree/master/ruby/"
|
15
|
+
spec.summary = "Misuse-resistant authenticated symmetric encryption"
|
16
|
+
spec.description = "Misuse-resistant symmetric encryption using the AES-SIV (RFC 5297) and CHAIN constructions"
|
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.required_ruby_version = ">= 2.2.2"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: miscreant
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tony Arcieri
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-07-24 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.14'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.14'
|
27
|
+
description: Misuse-resistant symmetric encryption using the AES-SIV (RFC 5297) and
|
28
|
+
CHAIN constructions
|
29
|
+
email:
|
30
|
+
- bascule@gmail.com
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- ".gitignore"
|
36
|
+
- ".rspec"
|
37
|
+
- ".rubocop.yml"
|
38
|
+
- ".ruby-version"
|
39
|
+
- Gemfile
|
40
|
+
- LICENSE.txt
|
41
|
+
- README.md
|
42
|
+
- Rakefile
|
43
|
+
- lib/miscreant.rb
|
44
|
+
- lib/miscreant/aes.rb
|
45
|
+
- lib/miscreant/aes/cmac.rb
|
46
|
+
- lib/miscreant/aes/siv.rb
|
47
|
+
- lib/miscreant/util.rb
|
48
|
+
- lib/miscreant/version.rb
|
49
|
+
- miscreant.gemspec
|
50
|
+
homepage: https://github.com/miscreant/miscreant/tree/master/ruby/
|
51
|
+
licenses:
|
52
|
+
- MIT
|
53
|
+
metadata: {}
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 2.2.2
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubyforge_project:
|
70
|
+
rubygems_version: 2.6.12
|
71
|
+
signing_key:
|
72
|
+
specification_version: 4
|
73
|
+
summary: Misuse-resistant authenticated symmetric encryption
|
74
|
+
test_files: []
|