xoodyak 0.1.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/LICENSE.md +25 -0
- data/README.md +352 -0
- data/ext/xoodyak/Cargo.toml +24 -0
- data/ext/xoodyak/build.rs +5 -0
- data/ext/xoodyak/extconf.rb +23 -0
- data/ext/xoodyak/src/lib.rs +438 -0
- data/lib/digest/xoodyak.rb +3 -0
- data/lib/xoodyak/version.rb +5 -0
- data/lib/xoodyak.rb +27 -0
- data/sig/xoodyak.rbs +34 -0
- metadata +70 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: '087e6888550b824a43b269965b1066bd4daad3d1942cc517ced8435ffcfaa21b'
|
|
4
|
+
data.tar.gz: 0cabd3ab1dfddae09c63de3040479e365e11c10726d9a3a484f7983927080173
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ea9b1f41888eef78c4ca2e960988f7f04cc418386b2883f0cf4653be7a699b7eef0eb5d44c9b072fd0a9dbf2340bf6d5082ceb16c7f5582c7bf502bc0a90c0cb
|
|
7
|
+
data.tar.gz: 1c0a4f756035ff03478a7031c40fd505d03f7c32fb02e0e5e755ac7ed872c587dafe2b71f161cd308907c4fa183769553ede8bc3bf615fdee91c12303999bab1
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Sarun Rattanasiri
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
17
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
18
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
20
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
21
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
23
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
24
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
25
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# Xoodyak Ruby Gem
|
|
2
|
+
|
|
3
|
+
A blazing fast, secure, and modern Rust-backed Ruby implementation of the Xoodyak cryptographic scheme.
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/rb/xoodyak)
|
|
6
|
+
[](https://github.com/midnight-wonderer/xoodyak-rb/blob/main/LICENSE.md)
|
|
7
|
+
[](https://github.com/midnight-wonderer/xoodyak-rb/blob/main/sig/xoodyak.rbs)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 📖 Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Introduction](#-introduction)
|
|
14
|
+
- [Features](#-features)
|
|
15
|
+
- [Installation](#-installation)
|
|
16
|
+
- [Usage Guide](#-usage-guide)
|
|
17
|
+
- [1. Hashing (Unkeyed Mode)](#1-hashing-unkeyed-mode)
|
|
18
|
+
- [2. Ruby Digest API Integration](#2-ruby-digest-api-integration)
|
|
19
|
+
- [3. Symmetric Encryption (Keyed Mode)](#3-symmetric-encryption-keyed-mode)
|
|
20
|
+
- [4. Authenticated Encryption (AEAD)](#4-authenticated-encryption-aead)
|
|
21
|
+
- [Combined Ciphertext & Tag](#combined-ciphertext--tag)
|
|
22
|
+
- [Detached Ciphertext & Tag](#detached-ciphertext--tag)
|
|
23
|
+
- [5. Advanced Keyed Customization (Nonces, Key IDs, Counters)](#5-advanced-keyed-customization-nonces-key-ids-counters)
|
|
24
|
+
- [6. Forward Secrecy (State Ratcheting)](#6-forward-secrecy-state-ratcheting)
|
|
25
|
+
- [7. Stateful Session-based Encrypt/Decrypt](#7-stateful-session-based-encryptdecrypt)
|
|
26
|
+
- [8. State Cloning & Checkpointing](#8-state-cloning--checkpointing)
|
|
27
|
+
- [API Reference](#-api-reference)
|
|
28
|
+
- [Type Safety with RBS](#-type-safety-with-rbs)
|
|
29
|
+
- [Development & Testing](#-development--testing)
|
|
30
|
+
- [License](#-license)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 🌟 Introduction
|
|
35
|
+
|
|
36
|
+
**Xoodyak** is a lightweight cryptographic scheme designed by the Keccak team (creators of SHA-3). It is part of the Keccak family and is optimized for low-resource environments. Xoodyak operates as a stateful "sponge" construction, making it extremely versatile. A single instance can perform:
|
|
37
|
+
- **Hashing** (unkeyed mode)
|
|
38
|
+
- **Symmetric Encryption** (keyed mode)
|
|
39
|
+
- **Message Authentication Codes (MAC)** (keyed mode)
|
|
40
|
+
- **Authenticated Encryption with Associated Data (AEAD)** (keyed mode)
|
|
41
|
+
- **Key Derivation & Ratcheting**
|
|
42
|
+
|
|
43
|
+
This gem provides a production-ready Ruby interface to Xoodyak, wrapping a highly optimized Rust implementation.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## ⚡ Features
|
|
48
|
+
|
|
49
|
+
- 🏎️ **Blazing Fast**: Native Rust extension using `magnus` and `rb-sys` outpaces pure Ruby cryptography.
|
|
50
|
+
- 🔒 **Sponge-based Design**: Supports stateful session-based protocols.
|
|
51
|
+
- 🛠️ **Seamless Digest Integration**: Inherits from Ruby's standard `Digest::Base` for drop-in compatibility.
|
|
52
|
+
- 📦 **Zero-Configuration AEAD**: Simple combined and detached AEAD interfaces.
|
|
53
|
+
- 🧩 **RBS Typed**: Complete type definitions shipped out of the box.
|
|
54
|
+
- 🛡️ **Memory Safe**: Built-in Rust safety guarantees prevent common memory leaks and buffer overflows.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 📥 Installation
|
|
59
|
+
|
|
60
|
+
Add this line to your application's Gemfile:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
gem 'xoodyak'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
And then execute:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
$ bundle install
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Or install it directly via:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
$ gem install xoodyak
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
> [!NOTE]
|
|
79
|
+
> Since this gem includes a Rust extension, you must have the **Rust toolchain** (cargo/rustc) installed on your system to compile it.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 🚀 Usage Guide
|
|
84
|
+
|
|
85
|
+
### 1. Hashing (Unkeyed Mode)
|
|
86
|
+
|
|
87
|
+
In unkeyed mode, Xoodyak acts as a standard cryptographic hash function. You can feed data incrementally using `absorb` and extract the hash using `squeeze`.
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
require 'xoodyak'
|
|
91
|
+
|
|
92
|
+
# Initialize in unkeyed (hashing) mode
|
|
93
|
+
hash_sponge = Xoodyak.new
|
|
94
|
+
|
|
95
|
+
# Absorb data
|
|
96
|
+
hash_sponge.absorb("Hello, world!")
|
|
97
|
+
|
|
98
|
+
# Squeeze out the digest (you can request any length!)
|
|
99
|
+
digest = hash_sponge.squeeze(32)
|
|
100
|
+
# => returns a 32-byte binary string
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 2. Ruby Digest API Integration
|
|
104
|
+
|
|
105
|
+
For standard hashing tasks, this gem integrates directly with Ruby's `Digest` framework.
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
require 'xoodyak'
|
|
109
|
+
|
|
110
|
+
# 1. Instantiate the Digest class
|
|
111
|
+
digest = Xoodyak::Digest.new
|
|
112
|
+
digest.update("Hello, ")
|
|
113
|
+
digest.update("world!")
|
|
114
|
+
puts digest.hexdigest
|
|
115
|
+
# => "c1ae6b98..."
|
|
116
|
+
|
|
117
|
+
# 2. Or use the shortcut methods
|
|
118
|
+
hex_hash = Digest::Xoodyak.hexdigest("Hello, world!")
|
|
119
|
+
binary_hash = Digest::Xoodyak.digest("Hello, world!")
|
|
120
|
+
|
|
121
|
+
# 3. Dynamic loading also works
|
|
122
|
+
algo = Digest("Xoodyak").new
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 3. Symmetric Encryption (Keyed Mode)
|
|
126
|
+
|
|
127
|
+
By passing a key during initialization, Xoodyak enters **keyed mode**. This allows standard symmetric encryption and decryption.
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
require 'xoodyak'
|
|
131
|
+
|
|
132
|
+
key = "my-secure-key-16" # Can be any length (Xoodyak handles varying key lengths)
|
|
133
|
+
|
|
134
|
+
# Encrypting
|
|
135
|
+
encryptor = Xoodyak.new(key)
|
|
136
|
+
ciphertext = encryptor.encrypt("super secret message")
|
|
137
|
+
|
|
138
|
+
# Decrypting (initialize a new state with the same key)
|
|
139
|
+
decryptor = Xoodyak.new(key)
|
|
140
|
+
plaintext = decryptor.decrypt(ciphertext)
|
|
141
|
+
puts plaintext # => "super secret message"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 4. Authenticated Encryption (AEAD)
|
|
145
|
+
|
|
146
|
+
Standard encryption protects confidentiality but not integrity. **AEAD** (Authenticated Encryption with Associated Data) is highly recommended because it also authenticates the message and optional "Associated Data" (like unencrypted routing headers).
|
|
147
|
+
|
|
148
|
+
#### Combined Ciphertext & Tag
|
|
149
|
+
|
|
150
|
+
`aead_encrypt` appends a 16-byte authentication tag directly to the ciphertext. `aead_decrypt` verifies the tag and returns the decrypted text, raising an error if the tag is invalid.
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
require 'xoodyak'
|
|
154
|
+
require 'securerandom'
|
|
155
|
+
|
|
156
|
+
key = "my-secure-key-16"
|
|
157
|
+
nonce = SecureRandom.bytes(16) # Nonces must be unique for each encryption!
|
|
158
|
+
|
|
159
|
+
# Encrypt with Associated Data
|
|
160
|
+
alice = Xoodyak.new(key, nonce)
|
|
161
|
+
alice.absorb("Associated Data (unencrypted header)")
|
|
162
|
+
ciphertext_with_tag = alice.aead_encrypt("confidential message")
|
|
163
|
+
|
|
164
|
+
# Decrypt and Verify
|
|
165
|
+
bob = Xoodyak.new(key, nonce)
|
|
166
|
+
bob.absorb("Associated Data (unencrypted header)") # Must match Alice's AD
|
|
167
|
+
|
|
168
|
+
begin
|
|
169
|
+
decrypted = bob.aead_decrypt(ciphertext_with_tag)
|
|
170
|
+
puts decrypted # => "confidential message"
|
|
171
|
+
rescue Xoodyak::Error => e
|
|
172
|
+
# Raised if ciphertext or associated data was altered
|
|
173
|
+
puts "Integrity check failed: #{e.message}"
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Detached Ciphertext & Tag
|
|
178
|
+
|
|
179
|
+
If your protocol stores or transmits the ciphertext and tag separately, you can use the detached API:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
require 'xoodyak'
|
|
183
|
+
require 'securerandom'
|
|
184
|
+
|
|
185
|
+
key = "my-secure-key-16"
|
|
186
|
+
nonce = SecureRandom.bytes(16)
|
|
187
|
+
|
|
188
|
+
# Encrypt
|
|
189
|
+
alice = Xoodyak.new(key, nonce)
|
|
190
|
+
alice.absorb("metadata")
|
|
191
|
+
ciphertext, tag = alice.aead_encrypt_detached("confidential message")
|
|
192
|
+
|
|
193
|
+
# Decrypt and Verify
|
|
194
|
+
bob = Xoodyak.new(key, nonce)
|
|
195
|
+
bob.absorb("metadata")
|
|
196
|
+
|
|
197
|
+
begin
|
|
198
|
+
decrypted = bob.aead_decrypt_detached(ciphertext, tag)
|
|
199
|
+
puts decrypted # => "confidential message"
|
|
200
|
+
rescue Xoodyak::Error => e
|
|
201
|
+
puts "Integrity check failed: #{e.message}"
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 5. Advanced Keyed Customization (Nonces, Key IDs, Counters)
|
|
206
|
+
|
|
207
|
+
Xoodyak supports initializing the keyed state with a variety of optional parameters:
|
|
208
|
+
- `key` (required for keyed mode)
|
|
209
|
+
- `nonce` (optional binary string)
|
|
210
|
+
- `key_id` (optional binary string)
|
|
211
|
+
- `counter` (optional binary string)
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
# Initialize with key, nonce, key_id, and counter
|
|
215
|
+
xoodyak = Xoodyak.new(key, nonce, key_id, counter)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
> [!WARNING]
|
|
219
|
+
> Passing `nonce`, `key_id`, or `counter` without a `key` will raise an `ArgumentError`.
|
|
220
|
+
|
|
221
|
+
### 6. Forward Secrecy (State Ratcheting)
|
|
222
|
+
|
|
223
|
+
State ratcheting advances the keyed state in a non-reversible way. Even if an attacker gains access to the current state, they cannot reconstruct past states, providing forward secrecy.
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
require 'xoodyak'
|
|
227
|
+
|
|
228
|
+
xoodyak = Xoodyak.new("my-secret-key")
|
|
229
|
+
|
|
230
|
+
# Perform operations...
|
|
231
|
+
xoodyak.absorb("some context")
|
|
232
|
+
|
|
233
|
+
# Ratchet the state
|
|
234
|
+
xoodyak.ratchet
|
|
235
|
+
|
|
236
|
+
# Squeeze out session keys or continue encrypting
|
|
237
|
+
session_key = xoodyak.squeeze(32)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### 7. Stateful Session-based Encrypt/Decrypt
|
|
241
|
+
|
|
242
|
+
Xoodyak is stateful: every operation transitions the internal sponge state. This allows Bob and Alice to have a stateful session where they encrypt and decrypt a stream of messages in order.
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
require 'xoodyak'
|
|
246
|
+
require 'securerandom'
|
|
247
|
+
|
|
248
|
+
key = "session-key-1234"
|
|
249
|
+
nonce = SecureRandom.bytes(16)
|
|
250
|
+
|
|
251
|
+
alice = Xoodyak.new(key, nonce)
|
|
252
|
+
bob = Xoodyak.new(key, nonce)
|
|
253
|
+
|
|
254
|
+
# Alice sends first message
|
|
255
|
+
ct1 = alice.encrypt("Message 1")
|
|
256
|
+
# Bob receives and decrypts
|
|
257
|
+
puts bob.decrypt(ct1) # => "Message 1"
|
|
258
|
+
|
|
259
|
+
# Alice sends second message (depends on state mutated by msg1!)
|
|
260
|
+
ct2 = alice.encrypt("Message 2")
|
|
261
|
+
# Bob decrypts
|
|
262
|
+
puts bob.decrypt(ct2) # => "Message 2"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
> [!IMPORTANT]
|
|
266
|
+
> Because the state mutates with each operation, Alice and Bob must remain in perfect sync. If any message is lost, reordered, or duplicated, decryption will fail. This provides built-in replay and out-of-order protection.
|
|
267
|
+
|
|
268
|
+
### 8. State Cloning & Checkpointing
|
|
269
|
+
|
|
270
|
+
You can duplicate or clone the state of a Xoodyak instance. This is useful for saving checkpoints or branching a cryptographic session.
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
require 'xoodyak'
|
|
274
|
+
|
|
275
|
+
xoodyak = Xoodyak.new
|
|
276
|
+
xoodyak.absorb("initial setup data")
|
|
277
|
+
|
|
278
|
+
# Duplicate the state
|
|
279
|
+
checkpoint = xoodyak.dup
|
|
280
|
+
|
|
281
|
+
# Both instances can now diverge independently
|
|
282
|
+
xoodyak.absorb("branch A")
|
|
283
|
+
checkpoint.absorb("branch B")
|
|
284
|
+
|
|
285
|
+
puts xoodyak.squeeze(16).unpack1("H*") # Squeezes based on "initial setup data" + "branch A"
|
|
286
|
+
puts checkpoint.squeeze(16).unpack1("H*") # Squeezes based on "initial setup data" + "branch B"
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## 🛠️ API Reference
|
|
292
|
+
|
|
293
|
+
### `Xoodyak` Class
|
|
294
|
+
|
|
295
|
+
| Method | Signature | Mode | Description |
|
|
296
|
+
| :--- | :--- | :--- | :--- |
|
|
297
|
+
| `initialize` | `(key=nil, nonce=nil, key_id=nil, counter=nil)` | Any | Creates a Xoodyak instance. Enters keyed mode if a key is provided. |
|
|
298
|
+
| `absorb` | `(bin: String) -> void` | Any | Absorbs binary data into the state. |
|
|
299
|
+
| `squeeze` | `(len: Integer) -> String` | Any | Squeezes `len` bytes from the state. |
|
|
300
|
+
| `squeeze_key` | `(len: Integer) -> String` | Any | Squeezes `len` key bytes from the state. |
|
|
301
|
+
| `encrypt` | `(bin: String) -> String` | Keyed | Encrypts a message. |
|
|
302
|
+
| `decrypt` | `(bin: String) -> String` | Keyed | Decrypts a message. |
|
|
303
|
+
| `aead_encrypt` | `(bin: String) -> String` | Keyed | Encrypts a message, appending a 16-byte authentication tag. |
|
|
304
|
+
| `aead_decrypt` | `(bin: String) -> String` | Keyed | Verifies the tag and decrypts a combined AEAD message. |
|
|
305
|
+
| `aead_encrypt_detached` | `(bin: String) -> [String, String]` | Keyed | Encrypts a message, returning `[ciphertext, tag]`. |
|
|
306
|
+
| `aead_decrypt_detached` | `(bin: String, tag: String) -> String` | Keyed | Verifies the detached tag and decrypts the ciphertext. |
|
|
307
|
+
| `ratchet` | `() -> void` | Keyed | Ratchets the state to provide forward secrecy. |
|
|
308
|
+
| `dup` / `clone` | `() -> Xoodyak` | Any | Creates a deep copy of the Xoodyak instance state. |
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## 🧩 Type Safety with RBS
|
|
313
|
+
|
|
314
|
+
This gem is packaged with complete RBS type definitions. You can typecheck your application using Steep or other Ruby signature verification tools.
|
|
315
|
+
|
|
316
|
+
Type signatures are defined in [sig/xoodyak.rbs](file:///storage/projects/xoodyak-rb/sig/xoodyak.rbs).
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## 🔧 Development & Testing
|
|
321
|
+
|
|
322
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
|
323
|
+
|
|
324
|
+
### Compilation
|
|
325
|
+
|
|
326
|
+
Since the core cryptographic operations are written in Rust, you must compile the C-extension locally:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
bundle exec rake compile
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Running Tests
|
|
333
|
+
|
|
334
|
+
Run the RSpec test suite:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
bundle exec rake spec
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Linting
|
|
341
|
+
|
|
342
|
+
Check code formatting and style guidelines:
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
bundle exec rake rubocop
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## 📄 License
|
|
351
|
+
|
|
352
|
+
This gem is available as open source under the terms of the [BSD 2-Clause License](LICENSE.md).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "xoodyak"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
publish = false
|
|
6
|
+
|
|
7
|
+
[lib]
|
|
8
|
+
crate-type = ["cdylib"]
|
|
9
|
+
|
|
10
|
+
[dependencies]
|
|
11
|
+
magnus = { version = "0.8.2", features = ["rb-sys"] }
|
|
12
|
+
rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
|
|
13
|
+
xoodyak = "0.8.4"
|
|
14
|
+
|
|
15
|
+
[build-dependencies]
|
|
16
|
+
rb-sys-env = "0.2.2"
|
|
17
|
+
|
|
18
|
+
[dev-dependencies]
|
|
19
|
+
rb-sys-test-helpers = { version = "0.2.2" }
|
|
20
|
+
|
|
21
|
+
[lints.rust]
|
|
22
|
+
unexpected_cfgs = { level = "warn", check-cfg = [
|
|
23
|
+
'cfg(digest_use_rb_ext_resolve_symbol)',
|
|
24
|
+
] }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mkmf"
|
|
4
|
+
require "rb_sys/mkmf"
|
|
5
|
+
|
|
6
|
+
# Detect when DIGEST_USE_RB_EXT_RESOLVE_SYMBOL is defined truthy,
|
|
7
|
+
# which indicates rb_ext_resolve_symbol("digest.so", "rb_digest_wrap_metadata")
|
|
8
|
+
# will work with the version of Digest we're building against.
|
|
9
|
+
digest_use_rb_ext_resolve_symbol = try_compile(<<~C)
|
|
10
|
+
#include <ruby/digest.h>
|
|
11
|
+
|
|
12
|
+
#if !defined(DIGEST_USE_RB_EXT_RESOLVE_SYMBOL)
|
|
13
|
+
# error DIGEST_USE_RB_EXT_RESOLVE_SYMBOL not defined
|
|
14
|
+
#endif
|
|
15
|
+
#if !DIGEST_USE_RB_EXT_RESOLVE_SYMBOL
|
|
16
|
+
# error DIGEST_USE_RB_EXT_RESOLVE_SYMBOL is 0
|
|
17
|
+
#endif
|
|
18
|
+
C
|
|
19
|
+
puts("digest_use_rb_ext_resolve_symbol=#{digest_use_rb_ext_resolve_symbol}")
|
|
20
|
+
|
|
21
|
+
create_rust_makefile("xoodyak/xoodyak") do |r|
|
|
22
|
+
r.extra_rustflags = ["--cfg digest_use_rb_ext_resolve_symbol"] if digest_use_rb_ext_resolve_symbol
|
|
23
|
+
end
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
use std::cell::RefCell;
|
|
2
|
+
use std::ffi::{c_int, c_uchar, c_void};
|
|
3
|
+
use std::mem::MaybeUninit;
|
|
4
|
+
use std::os::raw::c_char;
|
|
5
|
+
use magnus::{
|
|
6
|
+
prelude::*, scan_args::scan_args, Error, ExceptionClass, RString, Ruby, Value,
|
|
7
|
+
};
|
|
8
|
+
use rb_sys::{VALUE, size_t};
|
|
9
|
+
use xoodyak::{XoodyakHash, XoodyakKeyed, XoodyakCommon, XoodyakError, XOODYAK_AUTH_TAG_BYTES};
|
|
10
|
+
|
|
11
|
+
// Low-level Digest bindings
|
|
12
|
+
pub const RUBY_DIGEST_API_VERSION: c_int = 3;
|
|
13
|
+
|
|
14
|
+
pub type RbDigestHashInitFuncT = unsafe extern "C" fn(*mut c_void) -> c_int;
|
|
15
|
+
pub type RbDigestHashUpdateFuncT = unsafe extern "C" fn(*mut c_void, *mut c_uchar, size_t);
|
|
16
|
+
pub type RbDigestHashFinishFuncT = unsafe extern "C" fn(*mut c_void, *mut c_uchar) -> c_int;
|
|
17
|
+
|
|
18
|
+
#[derive(Debug)]
|
|
19
|
+
#[repr(C)]
|
|
20
|
+
pub struct RbDigestMetadataT {
|
|
21
|
+
pub api_version: c_int,
|
|
22
|
+
pub digest_len: size_t,
|
|
23
|
+
pub block_len: size_t,
|
|
24
|
+
pub ctx_size: size_t,
|
|
25
|
+
pub init_func: RbDigestHashInitFuncT,
|
|
26
|
+
pub update_func: RbDigestHashUpdateFuncT,
|
|
27
|
+
pub finish_func: RbDigestHashFinishFuncT,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[allow(dead_code)]
|
|
31
|
+
type WrapperType = unsafe extern "C" fn(&'static RbDigestMetadataT) -> VALUE;
|
|
32
|
+
|
|
33
|
+
#[cfg(any(digest_use_rb_ext_resolve_symbol, ruby_gte_3_4))]
|
|
34
|
+
pub unsafe fn rb_digest_make_metadata(meta: &'static RbDigestMetadataT) -> VALUE {
|
|
35
|
+
static mut WRAPPER: Option<WrapperType> = None;
|
|
36
|
+
|
|
37
|
+
unsafe fn load_wrapper() {
|
|
38
|
+
use rb_sys::rb_ext_resolve_symbol;
|
|
39
|
+
use std::ffi::c_char;
|
|
40
|
+
use std::sync::Once;
|
|
41
|
+
|
|
42
|
+
static INIT: Once = Once::new();
|
|
43
|
+
|
|
44
|
+
INIT.call_once(|| {
|
|
45
|
+
let lib_name = "digest.so\0".as_ptr() as *const c_char;
|
|
46
|
+
let symbol_name = "rb_digest_wrap_metadata\0".as_ptr() as *const c_char;
|
|
47
|
+
let symbol_ptr = unsafe { rb_ext_resolve_symbol(lib_name, symbol_name) };
|
|
48
|
+
|
|
49
|
+
if !symbol_ptr.is_null() {
|
|
50
|
+
unsafe {
|
|
51
|
+
WRAPPER = Some(std::mem::transmute::<*mut c_void, WrapperType>(symbol_ptr));
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
panic!("Failed to resolve rb_digest_wrap_metadata");
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
unsafe {
|
|
60
|
+
load_wrapper();
|
|
61
|
+
if let Some(wrapper) = WRAPPER {
|
|
62
|
+
return wrapper(meta);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
panic!("Failed to resolve rb_digest_wrap_metadata");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[cfg(not(any(digest_use_rb_ext_resolve_symbol, ruby_gte_3_4)))]
|
|
69
|
+
pub unsafe fn rb_digest_make_metadata(meta: &'static RbDigestMetadataT) -> VALUE {
|
|
70
|
+
use rb_sys::{rb_data_object_wrap, rb_obj_freeze};
|
|
71
|
+
unsafe {
|
|
72
|
+
let data = rb_data_object_wrap(
|
|
73
|
+
0 as VALUE,
|
|
74
|
+
meta as *const RbDigestMetadataT as *mut c_void,
|
|
75
|
+
None,
|
|
76
|
+
None,
|
|
77
|
+
);
|
|
78
|
+
rb_obj_freeze(data)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
struct RbXoodyakDigestCtx {
|
|
83
|
+
hash: XoodyakHash,
|
|
84
|
+
buffer: [u8; 16],
|
|
85
|
+
buf_len: usize,
|
|
86
|
+
first_block_absorbed: bool,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Struct to configure dynamic digest metadata
|
|
90
|
+
struct XoodyakDigest;
|
|
91
|
+
|
|
92
|
+
impl XoodyakDigest {
|
|
93
|
+
const BLOCK_LEN: usize = 16;
|
|
94
|
+
const DIGEST_LEN: usize = 32;
|
|
95
|
+
|
|
96
|
+
fn digest_metadata() -> &'static RbDigestMetadataT {
|
|
97
|
+
static DIGEST_METADATA: RbDigestMetadataT = RbDigestMetadataT {
|
|
98
|
+
api_version: RUBY_DIGEST_API_VERSION,
|
|
99
|
+
digest_len: XoodyakDigest::DIGEST_LEN as _,
|
|
100
|
+
block_len: XoodyakDigest::BLOCK_LEN as _,
|
|
101
|
+
ctx_size: std::mem::size_of::<RbXoodyakDigestCtx>() as _,
|
|
102
|
+
init_func: XoodyakDigest::init_in_place,
|
|
103
|
+
update_func: XoodyakDigest::update,
|
|
104
|
+
finish_func: XoodyakDigest::finish,
|
|
105
|
+
};
|
|
106
|
+
&DIGEST_METADATA
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
extern "C" fn init_in_place(ctx: *mut c_void) -> c_int {
|
|
110
|
+
let ctx = ctx as *mut MaybeUninit<RbXoodyakDigestCtx>;
|
|
111
|
+
let ctx = unsafe { &mut *ctx };
|
|
112
|
+
ctx.write(RbXoodyakDigestCtx {
|
|
113
|
+
hash: XoodyakHash::new(),
|
|
114
|
+
buffer: [0u8; 16],
|
|
115
|
+
buf_len: 0,
|
|
116
|
+
first_block_absorbed: false,
|
|
117
|
+
});
|
|
118
|
+
true as _
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
extern "C" fn update(ctx: *mut c_void, data: *mut c_uchar, len: size_t) {
|
|
122
|
+
let ctx = ctx as *mut MaybeUninit<RbXoodyakDigestCtx>;
|
|
123
|
+
let ctx = unsafe { &mut *ctx };
|
|
124
|
+
let ctx = unsafe { ctx.assume_init_mut() };
|
|
125
|
+
let mut slice = unsafe { std::slice::from_raw_parts(data, len as _) };
|
|
126
|
+
|
|
127
|
+
while !slice.is_empty() {
|
|
128
|
+
let space = 16 - ctx.buf_len;
|
|
129
|
+
if slice.len() >= space {
|
|
130
|
+
ctx.buffer[ctx.buf_len..16].copy_from_slice(&slice[..space]);
|
|
131
|
+
slice = &slice[space..];
|
|
132
|
+
|
|
133
|
+
if !ctx.first_block_absorbed {
|
|
134
|
+
ctx.hash.absorb(&ctx.buffer);
|
|
135
|
+
ctx.first_block_absorbed = true;
|
|
136
|
+
} else {
|
|
137
|
+
ctx.hash.absorb_more(&ctx.buffer, 16);
|
|
138
|
+
}
|
|
139
|
+
ctx.buf_len = 0;
|
|
140
|
+
} else {
|
|
141
|
+
ctx.buffer[ctx.buf_len..ctx.buf_len + slice.len()].copy_from_slice(slice);
|
|
142
|
+
ctx.buf_len += slice.len();
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
extern "C" fn finish(ctx: *mut c_void, digest: *mut c_uchar) -> c_int {
|
|
149
|
+
let ctx = ctx as *mut MaybeUninit<RbXoodyakDigestCtx>;
|
|
150
|
+
let ctx = unsafe { &mut *ctx };
|
|
151
|
+
let ctx = unsafe { ctx.assume_init_mut() };
|
|
152
|
+
|
|
153
|
+
if ctx.buf_len > 0 {
|
|
154
|
+
if !ctx.first_block_absorbed {
|
|
155
|
+
ctx.hash.absorb(&ctx.buffer[..ctx.buf_len]);
|
|
156
|
+
ctx.first_block_absorbed = true;
|
|
157
|
+
} else {
|
|
158
|
+
ctx.hash.absorb_more(&ctx.buffer[..ctx.buf_len], 16);
|
|
159
|
+
}
|
|
160
|
+
ctx.buf_len = 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let outbuf = unsafe { std::slice::from_raw_parts_mut(digest, Self::DIGEST_LEN) };
|
|
164
|
+
ctx.hash.squeeze(outbuf);
|
|
165
|
+
true as _
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fn get_error_class(name: &str) -> Option<ExceptionClass> {
|
|
170
|
+
let ruby = Ruby::get().unwrap();
|
|
171
|
+
ruby.class_object()
|
|
172
|
+
.const_get::<_, magnus::RClass>("Xoodyak")
|
|
173
|
+
.ok()
|
|
174
|
+
.and_then(|xoodyak| xoodyak.const_get::<_, ExceptionClass>(name).ok())
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn keyed_mode_error(msg: &'static str) -> Error {
|
|
178
|
+
let ruby = Ruby::get().unwrap();
|
|
179
|
+
if let Some(error_class) = get_error_class("KeyedModeError") {
|
|
180
|
+
Error::new(error_class, msg)
|
|
181
|
+
} else {
|
|
182
|
+
Error::new(ruby.exception_runtime_error(), msg)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fn argument_error(msg: &'static str) -> Error {
|
|
187
|
+
let ruby = Ruby::get().unwrap();
|
|
188
|
+
if let Some(error_class) = get_error_class("ArgumentError") {
|
|
189
|
+
Error::new(error_class, msg)
|
|
190
|
+
} else {
|
|
191
|
+
Error::new(ruby.exception_arg_error(), msg)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fn verification_error(msg: &'static str) -> Error {
|
|
196
|
+
let ruby = Ruby::get().unwrap();
|
|
197
|
+
if let Some(error_class) = get_error_class("VerificationError") {
|
|
198
|
+
Error::new(error_class, msg)
|
|
199
|
+
} else {
|
|
200
|
+
Error::new(ruby.exception_runtime_error(), msg)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Custom error mapping
|
|
205
|
+
fn map_xoodyak_err(err: XoodyakError) -> Error {
|
|
206
|
+
match err {
|
|
207
|
+
XoodyakError::InvalidBufferLength => argument_error("invalid buffer length"),
|
|
208
|
+
XoodyakError::InvalidParameterLength => argument_error("invalid parameter length"),
|
|
209
|
+
XoodyakError::KeyRequired => argument_error("key required"),
|
|
210
|
+
XoodyakError::TagMismatch => verification_error("tag mismatch"),
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Unified Xoodyak class
|
|
215
|
+
#[derive(Clone)]
|
|
216
|
+
enum XoodyakState {
|
|
217
|
+
Unkeyed(XoodyakHash),
|
|
218
|
+
Keyed(XoodyakKeyed),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#[derive(magnus::TypedData, Clone)]
|
|
222
|
+
#[magnus(class = "Xoodyak", free_immediately)]
|
|
223
|
+
pub struct Xoodyak {
|
|
224
|
+
state: RefCell<XoodyakState>,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
impl Default for XoodyakState {
|
|
228
|
+
fn default() -> Self {
|
|
229
|
+
XoodyakState::Unkeyed(XoodyakHash::new())
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
impl Default for Xoodyak {
|
|
234
|
+
fn default() -> Self {
|
|
235
|
+
Xoodyak {
|
|
236
|
+
state: RefCell::new(XoodyakState::default()),
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
impl magnus::DataTypeFunctions for Xoodyak {}
|
|
242
|
+
|
|
243
|
+
impl Xoodyak {
|
|
244
|
+
fn initialize(rb_self: magnus::typed_data::Obj<Self>, args: &[Value]) -> Result<(), Error> {
|
|
245
|
+
let args = scan_args::<(), (Option<Option<RString>>, Option<Option<RString>>, Option<Option<RString>>, Option<Option<RString>>), (), (), (), ()>(args)?;
|
|
246
|
+
let (key, nonce, key_id, counter) = args.optional;
|
|
247
|
+
let key = key.flatten();
|
|
248
|
+
let nonce = nonce.flatten();
|
|
249
|
+
let key_id = key_id.flatten();
|
|
250
|
+
let counter = counter.flatten();
|
|
251
|
+
if let Some(k) = key {
|
|
252
|
+
let key_bytes = unsafe { k.as_slice() };
|
|
253
|
+
let nonce_bytes = nonce.as_ref().map(|n| unsafe { n.as_slice() });
|
|
254
|
+
let key_id_bytes = key_id.as_ref().map(|ki| unsafe { ki.as_slice() });
|
|
255
|
+
let counter_bytes = counter.as_ref().map(|c| unsafe { c.as_slice() });
|
|
256
|
+
let keyed = XoodyakKeyed::new(key_bytes, nonce_bytes, key_id_bytes, counter_bytes)
|
|
257
|
+
.map_err(map_xoodyak_err)?;
|
|
258
|
+
*rb_self.state.borrow_mut() = XoodyakState::Keyed(keyed);
|
|
259
|
+
} else {
|
|
260
|
+
if nonce.is_some() || key_id.is_some() || counter.is_some() {
|
|
261
|
+
return Err(argument_error(
|
|
262
|
+
"nonce, key_id, and counter can only be used in keyed mode (when key is provided)",
|
|
263
|
+
));
|
|
264
|
+
}
|
|
265
|
+
*rb_self.state.borrow_mut() = XoodyakState::Unkeyed(XoodyakHash::new());
|
|
266
|
+
}
|
|
267
|
+
Ok(())
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
fn initialize_copy(&self, other: &Xoodyak) -> Result<(), Error> {
|
|
271
|
+
let other_state = other.state.borrow().clone();
|
|
272
|
+
*self.state.borrow_mut() = other_state;
|
|
273
|
+
Ok(())
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
fn absorb(&self, bin: RString) {
|
|
277
|
+
let bin_bytes = unsafe { bin.as_slice() };
|
|
278
|
+
match &mut *self.state.borrow_mut() {
|
|
279
|
+
XoodyakState::Unkeyed(ref mut h) => h.absorb(bin_bytes),
|
|
280
|
+
XoodyakState::Keyed(ref mut k) => k.absorb(bin_bytes),
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
fn squeeze(&self, len: usize) -> RString {
|
|
285
|
+
let mut buf = vec![0u8; len];
|
|
286
|
+
match &mut *self.state.borrow_mut() {
|
|
287
|
+
XoodyakState::Unkeyed(ref mut h) => h.squeeze(&mut buf),
|
|
288
|
+
XoodyakState::Keyed(ref mut k) => k.squeeze(&mut buf),
|
|
289
|
+
}
|
|
290
|
+
Ruby::get().unwrap().str_from_slice(&buf)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
fn squeeze_key(&self, len: usize) -> RString {
|
|
294
|
+
let mut buf = vec![0u8; len];
|
|
295
|
+
match &mut *self.state.borrow_mut() {
|
|
296
|
+
XoodyakState::Unkeyed(ref mut h) => h.squeeze_key(&mut buf),
|
|
297
|
+
XoodyakState::Keyed(ref mut k) => k.squeeze_key(&mut buf),
|
|
298
|
+
}
|
|
299
|
+
Ruby::get().unwrap().str_from_slice(&buf)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
fn encrypt(&self, bin: RString) -> Result<RString, Error> {
|
|
303
|
+
let bin_bytes = unsafe { bin.as_slice() };
|
|
304
|
+
match &mut *self.state.borrow_mut() {
|
|
305
|
+
XoodyakState::Unkeyed(_) => Err(keyed_mode_error("encrypt is only supported in keyed mode")),
|
|
306
|
+
XoodyakState::Keyed(ref mut k) => {
|
|
307
|
+
let mut out = vec![0u8; bin_bytes.len()];
|
|
308
|
+
k.encrypt(&mut out, bin_bytes).map_err(map_xoodyak_err)?;
|
|
309
|
+
Ok(Ruby::get().unwrap().str_from_slice(&out))
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
fn decrypt(&self, bin: RString) -> Result<RString, Error> {
|
|
315
|
+
let bin_bytes = unsafe { bin.as_slice() };
|
|
316
|
+
match &mut *self.state.borrow_mut() {
|
|
317
|
+
XoodyakState::Unkeyed(_) => Err(keyed_mode_error("decrypt is only supported in keyed mode")),
|
|
318
|
+
XoodyakState::Keyed(ref mut k) => {
|
|
319
|
+
let mut out = vec![0u8; bin_bytes.len()];
|
|
320
|
+
k.decrypt(&mut out, bin_bytes).map_err(map_xoodyak_err)?;
|
|
321
|
+
Ok(Ruby::get().unwrap().str_from_slice(&out))
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
fn aead_encrypt(&self, bin: RString) -> Result<RString, Error> {
|
|
327
|
+
let bin_bytes = unsafe { bin.as_slice() };
|
|
328
|
+
match &mut *self.state.borrow_mut() {
|
|
329
|
+
XoodyakState::Unkeyed(_) => Err(keyed_mode_error("aead_encrypt is only supported in keyed mode")),
|
|
330
|
+
XoodyakState::Keyed(ref mut k) => {
|
|
331
|
+
let mut out = vec![0u8; bin_bytes.len() + XOODYAK_AUTH_TAG_BYTES];
|
|
332
|
+
k.aead_encrypt(&mut out, Some(bin_bytes)).map_err(map_xoodyak_err)?;
|
|
333
|
+
Ok(Ruby::get().unwrap().str_from_slice(&out))
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
fn aead_decrypt(&self, bin: RString) -> Result<RString, Error> {
|
|
339
|
+
let bin_bytes = unsafe { bin.as_slice() };
|
|
340
|
+
match &mut *self.state.borrow_mut() {
|
|
341
|
+
XoodyakState::Unkeyed(_) => Err(keyed_mode_error("aead_decrypt is only supported in keyed mode")),
|
|
342
|
+
XoodyakState::Keyed(ref mut k) => {
|
|
343
|
+
if bin_bytes.len() < XOODYAK_AUTH_TAG_BYTES {
|
|
344
|
+
return Err(argument_error("ciphertext is too short to contain a tag"));
|
|
345
|
+
}
|
|
346
|
+
let mut out = vec![0u8; bin_bytes.len() - XOODYAK_AUTH_TAG_BYTES];
|
|
347
|
+
k.aead_decrypt(&mut out, bin_bytes).map_err(map_xoodyak_err)?;
|
|
348
|
+
Ok(Ruby::get().unwrap().str_from_slice(&out))
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
fn aead_encrypt_detached(&self, bin: RString) -> Result<magnus::RArray, Error> {
|
|
354
|
+
let bin_bytes = unsafe { bin.as_slice() };
|
|
355
|
+
match &mut *self.state.borrow_mut() {
|
|
356
|
+
XoodyakState::Unkeyed(_) => Err(keyed_mode_error("aead_encrypt_detached is only supported in keyed mode")),
|
|
357
|
+
XoodyakState::Keyed(ref mut k) => {
|
|
358
|
+
let mut out = vec![0u8; bin_bytes.len()];
|
|
359
|
+
let tag = k.aead_encrypt_detached(&mut out, Some(bin_bytes)).map_err(map_xoodyak_err)?;
|
|
360
|
+
let ruby = Ruby::get().unwrap();
|
|
361
|
+
let ct_str = ruby.str_from_slice(&out);
|
|
362
|
+
let tag_str = ruby.str_from_slice(tag.as_ref());
|
|
363
|
+
Ok(ruby.ary_new_from_values(&[ct_str.as_value(), tag_str.as_value()]))
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
fn aead_decrypt_detached(&self, bin: RString, tag: RString) -> Result<RString, Error> {
|
|
369
|
+
let bin_bytes = unsafe { bin.as_slice() };
|
|
370
|
+
let tag_bytes = unsafe { tag.as_slice() };
|
|
371
|
+
match &mut *self.state.borrow_mut() {
|
|
372
|
+
XoodyakState::Unkeyed(_) => Err(keyed_mode_error("aead_decrypt_detached is only supported in keyed mode")),
|
|
373
|
+
XoodyakState::Keyed(ref mut k) => {
|
|
374
|
+
let t_array: [u8; XOODYAK_AUTH_TAG_BYTES] = tag_bytes.try_into().map_err(|_| {
|
|
375
|
+
argument_error("tag must be 16 bytes")
|
|
376
|
+
})?;
|
|
377
|
+
let mut out = vec![0u8; bin_bytes.len()];
|
|
378
|
+
k.aead_decrypt_detached(&mut out, &t_array.into(), Some(bin_bytes))
|
|
379
|
+
.map_err(map_xoodyak_err)?;
|
|
380
|
+
Ok(Ruby::get().unwrap().str_from_slice(&out))
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
fn ratchet(&self) -> Result<(), Error> {
|
|
386
|
+
match &mut *self.state.borrow_mut() {
|
|
387
|
+
XoodyakState::Unkeyed(_) => Err(keyed_mode_error("ratchet is only supported in keyed mode")),
|
|
388
|
+
XoodyakState::Keyed(ref mut k) => {
|
|
389
|
+
k.ratchet();
|
|
390
|
+
Ok(())
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#[magnus::init]
|
|
397
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
398
|
+
// Define the Xoodyak class at the top level
|
|
399
|
+
let class = ruby.define_class("Xoodyak", ruby.class_object())?;
|
|
400
|
+
class.define_alloc_func::<Xoodyak>();
|
|
401
|
+
class.define_method("initialize", magnus::method!(Xoodyak::initialize, -1))?;
|
|
402
|
+
class.define_method("initialize_copy", magnus::method!(Xoodyak::initialize_copy, 1))?;
|
|
403
|
+
class.define_method("absorb", magnus::method!(Xoodyak::absorb, 1))?;
|
|
404
|
+
class.define_method("squeeze", magnus::method!(Xoodyak::squeeze, 1))?;
|
|
405
|
+
class.define_method("squeeze_key", magnus::method!(Xoodyak::squeeze_key, 1))?;
|
|
406
|
+
class.define_method("encrypt", magnus::method!(Xoodyak::encrypt, 1))?;
|
|
407
|
+
class.define_method("decrypt", magnus::method!(Xoodyak::decrypt, 1))?;
|
|
408
|
+
class.define_method("aead_encrypt", magnus::method!(Xoodyak::aead_encrypt, 1))?;
|
|
409
|
+
class.define_method("aead_decrypt", magnus::method!(Xoodyak::aead_decrypt, 1))?;
|
|
410
|
+
class.define_method("aead_encrypt_detached", magnus::method!(Xoodyak::aead_encrypt_detached, 1))?;
|
|
411
|
+
class.define_method("aead_decrypt_detached", magnus::method!(Xoodyak::aead_decrypt_detached, 2))?;
|
|
412
|
+
class.define_method("ratchet", magnus::method!(Xoodyak::ratchet, 0))?;
|
|
413
|
+
|
|
414
|
+
// Define the custom Error classes/modules under Xoodyak class
|
|
415
|
+
let error_module = class.define_module("Error")?;
|
|
416
|
+
|
|
417
|
+
let keyed_mode_error = class.define_error("KeyedModeError", ruby.exception_standard_error())?;
|
|
418
|
+
keyed_mode_error.include_module(error_module)?;
|
|
419
|
+
|
|
420
|
+
let verification_error = class.define_error("VerificationError", ruby.exception_standard_error())?;
|
|
421
|
+
verification_error.include_module(error_module)?;
|
|
422
|
+
|
|
423
|
+
let argument_error = class.define_error("ArgumentError", ruby.exception_arg_error())?;
|
|
424
|
+
argument_error.include_module(error_module)?;
|
|
425
|
+
|
|
426
|
+
// Define Digest subclassing Digest::Base nested under Xoodyak class
|
|
427
|
+
ruby.require("digest")?;
|
|
428
|
+
let digest_module = ruby.define_module("Digest")?;
|
|
429
|
+
let digest_base = digest_module.const_get::<_, magnus::RClass>("Base")?;
|
|
430
|
+
let digest_klass = class.define_class("Digest", digest_base)?;
|
|
431
|
+
|
|
432
|
+
use magnus::rb_sys::AsRawValue;
|
|
433
|
+
let meta = unsafe { rb_digest_make_metadata(XoodyakDigest::digest_metadata()) };
|
|
434
|
+
let metadata_id = unsafe { rb_sys::rb_intern("metadata\0".as_ptr() as *const c_char) };
|
|
435
|
+
unsafe { rb_sys::rb_ivar_set(digest_klass.as_raw(), metadata_id, meta) };
|
|
436
|
+
|
|
437
|
+
Ok(())
|
|
438
|
+
}
|
data/lib/xoodyak.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
# Define the Digest class in the Xoodyak namespace inheriting from Digest::Base,
|
|
6
|
+
# so the C extension can successfully find it and attach the digest metadata.
|
|
7
|
+
class Xoodyak
|
|
8
|
+
class Digest < ::Digest::Base
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require_relative "xoodyak/version"
|
|
13
|
+
|
|
14
|
+
begin
|
|
15
|
+
RUBY_VERSION =~ /(\d+\.\d+)/
|
|
16
|
+
require "xoodyak/#{Regexp.last_match(1)}/xoodyak"
|
|
17
|
+
rescue LoadError
|
|
18
|
+
begin
|
|
19
|
+
require_relative "xoodyak/xoodyak"
|
|
20
|
+
rescue LoadError
|
|
21
|
+
require "xoodyak/xoodyak"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module Digest
|
|
26
|
+
Xoodyak = ::Xoodyak::Digest
|
|
27
|
+
end
|
data/sig/xoodyak.rbs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class Xoodyak
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
module Error
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class KeyedModeError < ::StandardError
|
|
8
|
+
include Error
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class VerificationError < ::StandardError
|
|
12
|
+
include Error
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class ArgumentError < ::ArgumentError
|
|
16
|
+
include Error
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class Digest < ::Digest::Base
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize: (?String? key, ?String? nonce, ?String? key_id, ?String? counter) -> void
|
|
23
|
+
def initialize_copy: (Xoodyak other) -> void
|
|
24
|
+
def absorb: (String bin) -> void
|
|
25
|
+
def squeeze: (Integer len) -> String
|
|
26
|
+
def squeeze_key: (Integer len) -> String
|
|
27
|
+
def encrypt: (String bin) -> String
|
|
28
|
+
def decrypt: (String bin) -> String
|
|
29
|
+
def aead_encrypt: (String bin) -> String
|
|
30
|
+
def aead_decrypt: (String bin) -> String
|
|
31
|
+
def aead_encrypt_detached: (String bin) -> [String, String]
|
|
32
|
+
def aead_decrypt_detached: (String bin, String tag) -> String
|
|
33
|
+
def ratchet: () -> void
|
|
34
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: xoodyak
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sarun Rattanasiri
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rb_sys
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.9.91
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.9.91
|
|
26
|
+
description: A Ruby wrapper for the Xoodyak cryptographic scheme, built in Rust using
|
|
27
|
+
magnus and rb-sys. It supports hashing (unkeyed mode), symmetric encryption and
|
|
28
|
+
AEAD (keyed mode), forward secrecy (state ratcheting), and integrates with the standard
|
|
29
|
+
Ruby Digest API.
|
|
30
|
+
email: midnight_w@gmx.tw
|
|
31
|
+
executables: []
|
|
32
|
+
extensions:
|
|
33
|
+
- ext/xoodyak/extconf.rb
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- LICENSE.md
|
|
37
|
+
- README.md
|
|
38
|
+
- ext/xoodyak/Cargo.toml
|
|
39
|
+
- ext/xoodyak/build.rs
|
|
40
|
+
- ext/xoodyak/extconf.rb
|
|
41
|
+
- ext/xoodyak/src/lib.rs
|
|
42
|
+
- lib/digest/xoodyak.rb
|
|
43
|
+
- lib/xoodyak.rb
|
|
44
|
+
- lib/xoodyak/version.rb
|
|
45
|
+
- sig/xoodyak.rbs
|
|
46
|
+
homepage: https://github.com/midnight-wonderer/xoodyak-rb
|
|
47
|
+
licenses:
|
|
48
|
+
- BSD-2-Clause
|
|
49
|
+
metadata:
|
|
50
|
+
source_code_uri: https://github.com/midnight-wonderer/xoodyak-rb
|
|
51
|
+
bug_tracker_uri: https://github.com/midnight-wonderer/xoodyak-rb/issues
|
|
52
|
+
rdoc_options: []
|
|
53
|
+
require_paths:
|
|
54
|
+
- lib
|
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: 3.2.0
|
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: '0'
|
|
65
|
+
requirements: []
|
|
66
|
+
rubygems_version: 4.0.10
|
|
67
|
+
specification_version: 4
|
|
68
|
+
summary: A fast, memory-safe Rust-backed Ruby implementation of the Xoodyak cryptographic
|
|
69
|
+
scheme
|
|
70
|
+
test_files: []
|