Xoodyak Ruby Gem
A blazing fast, secure, and modern Rust-backed Ruby implementation of the Xoodyak cryptographic scheme.
📖 Table of Contents
- Introduction
- Features
- Installation
- Usage Guide
- 1. Hashing (Unkeyed Mode)
- 2. Ruby Digest API Integration
- 3. Symmetric Encryption (Keyed Mode)
- 4. Authenticated Encryption (AEAD)
- Combined Ciphertext & Tag
- Detached Ciphertext & Tag
- 5. Advanced Keyed Customization (Nonces, Key IDs, Counters)
- 6. Forward Secrecy (State Ratcheting)
- 7. Stateful Session-based Encrypt/Decrypt
- 8. State Cloning & Checkpointing
- API Reference
- Type Safety with RBS
- Development & Testing
- License
🌟 Introduction
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:
- Hashing (unkeyed mode)
- Symmetric Encryption (keyed mode)
- Message Authentication Codes (MAC) (keyed mode)
- Authenticated Encryption with Associated Data (AEAD) (keyed mode)
- Key Derivation & Ratcheting
This gem provides a production-ready Ruby interface to Xoodyak, wrapping a highly optimized Rust implementation.
⚡ Features
- 🏎️ Blazing Fast: Native Rust extension using
magnusandrb-sysoutpaces pure Ruby cryptography. - 🔒 Sponge-based Design: Supports stateful session-based protocols.
- 🛠️ Seamless Digest Integration: Inherits from Ruby's standard
Digest::Basefor drop-in compatibility. - 📦 Zero-Configuration AEAD: Simple combined and detached AEAD interfaces.
- 🧩 RBS Typed: Complete type definitions shipped out of the box.
- 🛡️ Memory Safe: Built-in Rust safety guarantees prevent common memory leaks and buffer overflows.
📥 Installation
Add this line to your application's Gemfile:
gem 'xoodyak'
And then execute:
$ bundle install
Or install it directly via:
$ gem install xoodyak
[!NOTE] Since this gem includes a Rust extension, you must have the Rust toolchain (cargo/rustc) installed on your system to compile it.
🚀 Usage Guide
1. Hashing (Unkeyed Mode)
In unkeyed mode, Xoodyak acts as a standard cryptographic hash function. You can feed data incrementally using absorb and extract the hash using squeeze.
require 'xoodyak'
# Initialize in unkeyed (hashing) mode
hash_sponge = Xoodyak.new
# Absorb data
hash_sponge.absorb("Hello, world!")
# Squeeze out the digest (you can request any length!)
digest = hash_sponge.squeeze(32)
# => returns a 32-byte binary string
2. Ruby Digest API Integration
For standard hashing tasks, this gem integrates directly with Ruby's Digest framework.
require 'xoodyak'
# 1. Instantiate the Digest class
digest = Xoodyak::Digest.new
digest.update("Hello, ")
digest.update("world!")
puts digest.hexdigest
# => "c1ae6b98..."
# 2. Or use the shortcut methods
hex_hash = Digest::Xoodyak.hexdigest("Hello, world!")
binary_hash = Digest::Xoodyak.digest("Hello, world!")
# 3. Dynamic loading also works
algo = Digest("Xoodyak").new
3. Symmetric Encryption (Keyed Mode)
By passing a key during initialization, Xoodyak enters keyed mode. This allows standard symmetric encryption and decryption.
require 'xoodyak'
key = "my-secure-key-16" # Can be any length (Xoodyak handles varying key lengths)
# Encrypting
encryptor = Xoodyak.new(key)
ciphertext = encryptor.encrypt("super secret message")
# Decrypting (initialize a new state with the same key)
decryptor = Xoodyak.new(key)
plaintext = decryptor.decrypt(ciphertext)
puts plaintext # => "super secret message"
4. Authenticated Encryption (AEAD)
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).
Combined Ciphertext & Tag
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.
require 'xoodyak'
require 'securerandom'
key = "my-secure-key-16"
nonce = SecureRandom.bytes(16) # Nonces must be unique for each encryption!
# Encrypt with Associated Data
alice = Xoodyak.new(key, nonce)
alice.absorb("Associated Data (unencrypted header)")
ciphertext_with_tag = alice.aead_encrypt("confidential message")
# Decrypt and Verify
bob = Xoodyak.new(key, nonce)
bob.absorb("Associated Data (unencrypted header)") # Must match Alice's AD
begin
decrypted = bob.aead_decrypt(ciphertext_with_tag)
puts decrypted # => "confidential message"
rescue Xoodyak::Error => e
# Raised if ciphertext or associated data was altered
puts "Integrity check failed: #{e.}"
end
Detached Ciphertext & Tag
If your protocol stores or transmits the ciphertext and tag separately, you can use the detached API:
require 'xoodyak'
require 'securerandom'
key = "my-secure-key-16"
nonce = SecureRandom.bytes(16)
# Encrypt
alice = Xoodyak.new(key, nonce)
alice.absorb("metadata")
ciphertext, tag = alice.aead_encrypt_detached("confidential message")
# Decrypt and Verify
bob = Xoodyak.new(key, nonce)
bob.absorb("metadata")
begin
decrypted = bob.aead_decrypt_detached(ciphertext, tag)
puts decrypted # => "confidential message"
rescue Xoodyak::Error => e
puts "Integrity check failed: #{e.}"
end
5. Advanced Keyed Customization (Nonces, Key IDs, Counters)
Xoodyak supports initializing the keyed state with a variety of optional parameters:
key(required for keyed mode)nonce(optional binary string)key_id(optional binary string)counter(optional binary string)
# Initialize with key, nonce, key_id, and counter
xoodyak = Xoodyak.new(key, nonce, key_id, counter)
[!WARNING] Passing
nonce,key_id, orcounterwithout akeywill raise anArgumentError.
6. Forward Secrecy (State Ratcheting)
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.
require 'xoodyak'
xoodyak = Xoodyak.new("my-secret-key")
# Perform operations...
xoodyak.absorb("some context")
# Ratchet the state
xoodyak.ratchet
# Squeeze out session keys or continue encrypting
session_key = xoodyak.squeeze(32)
7. Stateful Session-based Encrypt/Decrypt
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.
require 'xoodyak'
require 'securerandom'
key = "session-key-1234"
nonce = SecureRandom.bytes(16)
alice = Xoodyak.new(key, nonce)
bob = Xoodyak.new(key, nonce)
# Alice sends first message
ct1 = alice.encrypt("Message 1")
# Bob receives and decrypts
puts bob.decrypt(ct1) # => "Message 1"
# Alice sends second message (depends on state mutated by msg1!)
ct2 = alice.encrypt("Message 2")
# Bob decrypts
puts bob.decrypt(ct2) # => "Message 2"
[!IMPORTANT] 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.
8. State Cloning & Checkpointing
You can duplicate or clone the state of a Xoodyak instance. This is useful for saving checkpoints or branching a cryptographic session.
require 'xoodyak'
xoodyak = Xoodyak.new
xoodyak.absorb("initial setup data")
# Duplicate the state
checkpoint = xoodyak.dup
# Both instances can now diverge independently
xoodyak.absorb("branch A")
checkpoint.absorb("branch B")
puts xoodyak.squeeze(16).unpack1("H*") # Squeezes based on "initial setup data" + "branch A"
puts checkpoint.squeeze(16).unpack1("H*") # Squeezes based on "initial setup data" + "branch B"
🛠️ API Reference
Xoodyak Class
| Method | Signature | Mode | Description |
|---|---|---|---|
initialize |
(key=nil, nonce=nil, key_id=nil, counter=nil) |
Any | Creates a Xoodyak instance. Enters keyed mode if a key is provided. |
absorb |
(bin: String) -> void |
Any | Absorbs binary data into the state. |
squeeze |
(len: Integer) -> String |
Any | Squeezes len bytes from the state. |
squeeze_key |
(len: Integer) -> String |
Any | Squeezes len key bytes from the state. |
encrypt |
(bin: String) -> String |
Keyed | Encrypts a message. |
decrypt |
(bin: String) -> String |
Keyed | Decrypts a message. |
aead_encrypt |
(bin: String) -> String |
Keyed | Encrypts a message, appending a 16-byte authentication tag. |
aead_decrypt |
(bin: String) -> String |
Keyed | Verifies the tag and decrypts a combined AEAD message. |
aead_encrypt_detached |
(bin: String) -> [String, String] |
Keyed | Encrypts a message, returning [ciphertext, tag]. |
aead_decrypt_detached |
(bin: String, tag: String) -> String |
Keyed | Verifies the detached tag and decrypts the ciphertext. |
ratchet |
() -> void |
Keyed | Ratchets the state to provide forward secrecy. |
dup / clone |
() -> Xoodyak |
Any | Creates a deep copy of the Xoodyak instance state. |
🧩 Type Safety with RBS
This gem is packaged with complete RBS type definitions. You can typecheck your application using Steep or other Ruby signature verification tools.
Type signatures are defined in sig/xoodyak.rbs.
🔧 Development & Testing
After checking out the repo, run bin/setup to install dependencies.
Compilation
Since the core cryptographic operations are written in Rust, you must compile the C-extension locally:
bundle exec rake compile
Running Tests
Run the RSpec test suite:
bundle exec rake spec
Linting
Check code formatting and style guidelines:
bundle exec rake rubocop
📄 License
This gem is available as open source under the terms of the BSD 2-Clause License.