xaes_gcm 0.1.1
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/LICENSE.txt +21 -0
- data/README.md +66 -0
- data/Rakefile +8 -0
- data/lib/xaes_gcm/key.rb +81 -0
- data/lib/xaes_gcm/version.rb +5 -0
- data/lib/xaes_gcm.rb +30 -0
- data/sig/xaes_gcm.rbs +28 -0
- metadata +52 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8c94c5ea1a1fb9d1ee9045139a86f53f5c8b7ebf89edc7521136a4065e4e32b5
|
|
4
|
+
data.tar.gz: 4fd2f8e6b2eccb6de066608818177084ad4f5a9c289b9448842224eaa39ad9d7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c8a2dc2bc6fb43f227b9fb606cc05ff8caa0f90a160b5e704a19e65bcd959bc2496fa49ff1959b5903fe22087087cc816710ad914a53b498e9c57346e48ac109
|
|
7
|
+
data.tar.gz: c881c39b0475962a1ee080dfb23c3db5fbb64713ac6fb74225c816fdcac89623789c97ff6d5fa2f8bc0064f468fc6b9a1d2975355cbe9fc9d600069e604b5c38
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Copyright (c) 2020
|
|
2
|
+
The C2SP Authors. All rights reserved.
|
|
3
|
+
Copyright (c) 2026 Sorah Fukumori.
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions
|
|
7
|
+
are met:
|
|
8
|
+
1. Redistributions of source code must retain the above copyright
|
|
9
|
+
notice, this list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
THIS SOFTWARE IS PROVIDED BY The C2SP Authors ``AS IS'' AND
|
|
12
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
13
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
14
|
+
ARE DISCLAIMED. IN NO EVENT SHALL The C2SP Authors BE LIABLE
|
|
15
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
16
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
17
|
+
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
18
|
+
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
19
|
+
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
20
|
+
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
21
|
+
SUCH DAMAGE.
|
data/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# xaes_gcm
|
|
2
|
+
|
|
3
|
+
Ruby implementation of [XAES-256-GCM](https://c2sp.org/XAES-256-GCM), an extended-nonce AEAD built on AES-256-GCM.
|
|
4
|
+
|
|
5
|
+
XAES-256-GCM uses 192-bit (24-byte) nonces instead of AES-256-GCM's 96-bit nonces. The longer nonce makes it safe to generate nonces randomly for a practically unlimited number of messages, without risking nonce reuse.
|
|
6
|
+
|
|
7
|
+
This gem implements the key and nonce derivation step of XAES-256-GCM. It derives a standard AES-256-GCM key and nonce from the extended inputs, which you then use with Ruby's built-in `OpenSSL::Cipher` for encryption and decryption.
|
|
8
|
+
|
|
9
|
+
## Security Warning
|
|
10
|
+
|
|
11
|
+
> [!CAUTION]
|
|
12
|
+
> No security audits of this gem have ever been performed. USE AT YOUR OWN RISK!
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bundle add xaes_gcm
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or install directly:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
gem install xaes_gcm
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
require "xaes_gcm"
|
|
30
|
+
|
|
31
|
+
# Create a reusable key (precomputes the AES key schedule and subkey)
|
|
32
|
+
key = OpenSSL::Random.random_bytes(XaesGcm::KEY_SIZE) # 32 bytes
|
|
33
|
+
xkey = XaesGcm::Key.new(key)
|
|
34
|
+
|
|
35
|
+
# Encrypt (generates a random 192-bit nonce by default)
|
|
36
|
+
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
37
|
+
cipher.encrypt
|
|
38
|
+
nonce = xkey.apply(cipher)
|
|
39
|
+
cipher.auth_data = "optional authenticated data"
|
|
40
|
+
ciphertext = cipher.update(plaintext) + cipher.final
|
|
41
|
+
tag = cipher.auth_tag
|
|
42
|
+
|
|
43
|
+
# Decrypt (pass the same nonce used for encryption)
|
|
44
|
+
decipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
45
|
+
decipher.decrypt
|
|
46
|
+
xkey.apply(decipher, nonce:)
|
|
47
|
+
decipher.auth_tag = tag
|
|
48
|
+
decipher.auth_data = "optional authenticated data"
|
|
49
|
+
plaintext = decipher.update(ciphertext) + decipher.final
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`Key#apply` generates a random nonce, derives the AES-256-GCM key and nonce, sets them on the cipher, and returns the 24-byte nonce. Pass the same nonce back for decryption. `Key` precomputes the AES key schedule and subkey, so reuse the same instance when encrypting multiple messages under the same key.
|
|
53
|
+
|
|
54
|
+
## Alternative gems
|
|
55
|
+
|
|
56
|
+
There's alternative gem `xaes_256_gcm`: https://github.com/vcsjones/xaes-256-gcm-ruby
|
|
57
|
+
|
|
58
|
+
Key differences:
|
|
59
|
+
|
|
60
|
+
- Smaller code footprint
|
|
61
|
+
- Leaving OpenSSL::Cipher setup to the user
|
|
62
|
+
- Accumulated randomized test vectors are included in the test suite
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
The gem is available as open source under the terms of the [BSD 1-Clause License](https://opensource.org/licenses/BSD-1-Clause).
|
data/Rakefile
ADDED
data/lib/xaes_gcm/key.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XaesGcm
|
|
4
|
+
class Key
|
|
5
|
+
def initialize(key)
|
|
6
|
+
raise ArgumentError, "key must be #{KEY_SIZE} bytes" unless key.bytesize == KEY_SIZE
|
|
7
|
+
|
|
8
|
+
@cipher = OpenSSL::Cipher.new('aes-256-ecb')
|
|
9
|
+
@cipher.encrypt
|
|
10
|
+
@cipher.padding = 0
|
|
11
|
+
@cipher.key = key
|
|
12
|
+
|
|
13
|
+
# L = AES-256-ECB_K(0^128)
|
|
14
|
+
l = @cipher.update("\x00" * 16) + @cipher.final
|
|
15
|
+
|
|
16
|
+
# K1: shift L left by 1 bit, XOR last byte with 0x87 if MSB was set
|
|
17
|
+
msb = l.getbyte(0) >> 7
|
|
18
|
+
k1_bytes = Array.new(16) do |i|
|
|
19
|
+
next_bit = (i < 15) ? (l.getbyte(i + 1) >> 7) : 0
|
|
20
|
+
((l.getbyte(i) << 1) | next_bit) & 0b11111111
|
|
21
|
+
end
|
|
22
|
+
k1_bytes[-1] ^= 0b10000111 & -(msb & 1)
|
|
23
|
+
@k1 = k1_bytes.pack('C*')
|
|
24
|
+
@cipher.freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if HAVE_INSTANCE_VARIABLES_TO_INSPECT
|
|
28
|
+
def instance_variables_to_inspect = []
|
|
29
|
+
else
|
|
30
|
+
def inspect
|
|
31
|
+
"#<#{self.class}>"
|
|
32
|
+
end
|
|
33
|
+
alias to_s inspect
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def enable_hazmat!
|
|
37
|
+
@enable_hazmat = true
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def apply(cipher, nonce: OpenSSL::Random.random_bytes(NONCE_SIZE))
|
|
42
|
+
raise ArgumentError, "cipher must be AES-256-GCM" unless cipher.name == "AES-256-GCM"
|
|
43
|
+
|
|
44
|
+
dk = derive_key_raw(nonce:)
|
|
45
|
+
cipher.key = dk.key
|
|
46
|
+
cipher.iv = dk.nonce
|
|
47
|
+
nonce
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def derive_key(nonce:)
|
|
51
|
+
raise RuntimeError, "derive_key is a hazmat API that exposes raw key material; call enable_hazmat! on the Key instance to use it directly" unless @enable_hazmat
|
|
52
|
+
derive_key_raw(nonce:)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def derive_key_raw(nonce:)
|
|
58
|
+
raise ArgumentError, "nonce must be #{NONCE_SIZE} bytes" unless nonce.bytesize == NONCE_SIZE
|
|
59
|
+
|
|
60
|
+
n12 = nonce.byteslice(0, 12)
|
|
61
|
+
|
|
62
|
+
m1 = "\x00\x01X\x00".b + n12
|
|
63
|
+
m2 = "\x00\x02X\x00".b + n12
|
|
64
|
+
|
|
65
|
+
m1_xored = xor_blocks(m1, @k1)
|
|
66
|
+
m2_xored = xor_blocks(m2, @k1)
|
|
67
|
+
|
|
68
|
+
cipher = @cipher.dup
|
|
69
|
+
derived = cipher.update(m1_xored + m2_xored)
|
|
70
|
+
|
|
71
|
+
DerivedKey.new(key: derived, nonce: nonce.byteslice(12, 12))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def xor_blocks(a, b)
|
|
75
|
+
a_bytes = a.unpack('C*')
|
|
76
|
+
b_bytes = b.unpack('C*')
|
|
77
|
+
a_bytes.length.times { |i| a_bytes[i] ^= b_bytes[i] }
|
|
78
|
+
a_bytes.pack('C*')
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/xaes_gcm.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require_relative "xaes_gcm/version"
|
|
5
|
+
|
|
6
|
+
module XaesGcm
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
KEY_SIZE = 32
|
|
10
|
+
NONCE_SIZE = 24
|
|
11
|
+
|
|
12
|
+
# Detect instance_variables_to_inspect support (Ruby feature #13555)
|
|
13
|
+
HAVE_INSTANCE_VARIABLES_TO_INSPECT = begin
|
|
14
|
+
klass = Class.new do
|
|
15
|
+
def initialize = @secret = true
|
|
16
|
+
def instance_variables_to_inspect = []
|
|
17
|
+
end
|
|
18
|
+
!klass.new.inspect.include?("@secret")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
DerivedKey = Data.define(:key, :nonce) do
|
|
22
|
+
# Data.define#inspect doesn't use instance_variables_to_inspect
|
|
23
|
+
def inspect
|
|
24
|
+
"#<#{self.class}>"
|
|
25
|
+
end
|
|
26
|
+
alias to_s inspect
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
require_relative "xaes_gcm/key"
|
data/sig/xaes_gcm.rbs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module XaesGcm
|
|
2
|
+
VERSION: String
|
|
3
|
+
KEY_SIZE: Integer
|
|
4
|
+
NONCE_SIZE: Integer
|
|
5
|
+
|
|
6
|
+
class Error < StandardError
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class DerivedKey
|
|
10
|
+
attr_reader key: String
|
|
11
|
+
attr_reader nonce: String
|
|
12
|
+
|
|
13
|
+
def self.new: (key: String, nonce: String) -> DerivedKey
|
|
14
|
+
def self.[]: (key: String, nonce: String) -> DerivedKey
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Key
|
|
18
|
+
def initialize: (String key) -> void
|
|
19
|
+
def enable_hazmat!: () -> self
|
|
20
|
+
def apply: (OpenSSL::Cipher cipher, ?nonce: String) -> String
|
|
21
|
+
def derive_key: (nonce: String) -> DerivedKey
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def derive_key_raw: (nonce: String) -> DerivedKey
|
|
26
|
+
def xor_blocks: (String a, String b) -> String
|
|
27
|
+
end
|
|
28
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: xaes_gcm
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sorah Fukumori
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Ruby implementation of XAES-256-GCM (c2sp.org/XAES-256-GCM), an extended-nonce
|
|
13
|
+
AEAD built on AES-256-GCM. Derives standard AES-256-GCM keys and nonces from 256-bit
|
|
14
|
+
keys and 192-bit nonces.
|
|
15
|
+
email:
|
|
16
|
+
- sorah@ivry.jp
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE.txt
|
|
22
|
+
- README.md
|
|
23
|
+
- Rakefile
|
|
24
|
+
- lib/xaes_gcm.rb
|
|
25
|
+
- lib/xaes_gcm/key.rb
|
|
26
|
+
- lib/xaes_gcm/version.rb
|
|
27
|
+
- sig/xaes_gcm.rbs
|
|
28
|
+
homepage: https://github.com/sorah/xaes_gcm
|
|
29
|
+
licenses:
|
|
30
|
+
- BSD-1-Clause
|
|
31
|
+
metadata:
|
|
32
|
+
allowed_push_host: https://rubygems.org
|
|
33
|
+
homepage_uri: https://github.com/sorah/xaes_gcm
|
|
34
|
+
source_code_uri: https://github.com/sorah/xaes_gcm
|
|
35
|
+
rdoc_options: []
|
|
36
|
+
require_paths:
|
|
37
|
+
- lib
|
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: 3.2.0
|
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
requirements: []
|
|
49
|
+
rubygems_version: 3.6.9
|
|
50
|
+
specification_version: 4
|
|
51
|
+
summary: XAES-256-GCM extended-nonce AEAD key derivation
|
|
52
|
+
test_files: []
|