Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add XChaCha20Poly1305 box #235

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/rbnacl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ class BadAuthenticatorError < CryptoError; end
# Sealed boxes
require "rbnacl/boxes/sealed"

# Secret Key Encryption (SecretBox): XSalsa20Poly1305
# Secret Key Encryption (SecretBox): XSalsa20Poly1305 and XChaCha20Poly1305
require "rbnacl/secret_boxes/xsalsa20poly1305"
require "rbnacl/secret_boxes/xchacha20poly1305"

# Digital Signatures: Ed25519
require "rbnacl/signatures/ed25519"
Expand Down
141 changes: 141 additions & 0 deletions lib/rbnacl/secret_boxes/xchacha20poly1305.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# encoding: binary
# frozen_string_literal: true

module RbNaCl
module SecretBoxes
# The XChaCha20Poly1305 class boxes and unboxes messages using the XChaCha20Poly1305 algorithm
Copy link
Contributor

@tarcieri tarcieri Jul 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would probably be good to note there are two different flavors of algorithm calling themselves XChaCha20Poly1305

  • crypto_secretbox / djb-style: this is effectively the crypto_secretbox function but with XChaCha20 swapped for XSalsa20, and what is being implemented here
  • draft-irtf-cfrg-xchacha: uses the IETF flavor of ChaCha20

#
# This class uses the given secret key to encrypt and decrypt messages.
#
# It is VITALLY important that the nonce is a nonce, i.e. it is a number used
# only once for any given pair of keys. If you fail to do this, you
# compromise the privacy of the messages encrypted. Give your nonces a
# different prefix, or have one side use an odd counter and one an even counter.
# Just make sure they are different.
#
# The ciphertexts generated by this class include a 16-byte authenticator which
# is checked as part of the decryption. An invalid authenticator will cause
# the unbox function to raise. The authenticator is not a signature. Once
# you've looked in the box, you've demonstrated the ability to create
# arbitrary valid messages, so messages you send are repudiable. For
# non-repudiable messages, sign them before or after encryption.
class XChaCha20Poly1305
extend Sodium

sodium_type :secretbox
sodium_primitive :xchacha20poly1305
sodium_constant :KEYBYTES
sodium_constant :NONCEBYTES
sodium_constant :MACBYTES

sodium_function :secretbox_xchacha20poly1305,
:crypto_secretbox_xchacha20poly1305_easy,
%i[pointer pointer ulong_long pointer pointer]

sodium_function :secretbox_xchacha20poly1305_open,
:crypto_secretbox_xchacha20poly1305_open_easy,
%i[pointer pointer ulong_long pointer pointer]

# Create a new XChaCha20Poly1305 Box.
#
# Sets up the Box with a secret key for encrypting and decrypting messages.
#
# @param key [String] The key to encrypt and decrypt with
#
# @raise [RbNaCl::LengthError] on invalid keys
#
# @return [RbNaCl::XChaCha20Poly1305] The new Box, ready to use
def initialize(key)
@key = Util.check_string(key, KEYBYTES, "Secret key")
end

# Encrypts a message
#
# Encrypts the message with the given nonce to the key set up when
# initializing the class. Make sure the nonce is unique for any given
# key, or you might as well just send plain text.
#
# This function takes care of the padding required by the NaCL C API.
#
# @param nonce [String] A 24-byte string containing the nonce.
# @param message [String] The message to be encrypted.
#
# @raise [RbNaCl::LengthError] If the nonce is not valid
#
# @return [String] The ciphertext without the nonce prepended (BINARY encoded)
def box(nonce, message)
Util.check_length(nonce, nonce_bytes, "Nonce")

# Libsodium appends the tag, so make space for the tag.
ct = Util.zeros(message.bytesize + MACBYTES)

success = self.class.secretbox_xchacha20poly1305(ct, message, message.bytesize, nonce, @key)
raise CryptoError, "Encryption failed" unless success
ct
end
alias encrypt box

# Decrypts a ciphertext
#
# Decrypts the ciphertext with the given nonce using the key setup when
# initializing the class.
#
# This function takes care of the padding required by the NaCL C API.
#
# @param nonce [String] A 24-byte string containing the nonce.
# @param ciphertext [String] The message to be decrypted.
#
# @raise [RbNaCl::LengthError] If the nonce is not valid
# @raise [RbNaCl::CryptoError] If the ciphertext cannot be authenticated.
#
# @return [String] The decrypted message (BINARY encoded)
def open(nonce, ciphertext)
Util.check_length(nonce, nonce_bytes, "Nonce")
message = Util.zeros(ciphertext.bytesize)

success = self.class.secretbox_xchacha20poly1305_open(message, ciphertext, ciphertext.bytesize, nonce, @key)
raise CryptoError, "Decryption failed. Ciphertext failed verification." unless success

# When the message was created it's length included the length of the tag. Slice it off
# now that it passed the tag check.
message.byteslice(0, message.bytesize - MACBYTES)
end
alias decrypt open

# The crypto primitive for the XChaCha20Poly1305 instance
#
# @return [Symbol] The primitive used
def primitive
self.class.primitive
end

# The nonce bytes for the XChaCha20Poly1305 class
#
# @return [Integer] The number of bytes in a valid nonce
def self.nonce_bytes
NONCEBYTES
end

# The nonce bytes for the XChaCha20Poly1305 instance
#
# @return [Integer] The number of bytes in a valid nonce
def nonce_bytes
NONCEBYTES
end

# The key bytes for the XChaCha20Poly1305 class
#
# @return [Integer] The number of bytes in a valid key
def self.key_bytes
KEYBYTES
end

# The key bytes for the XChaCha20Poly1305 instance
#
# @return [Integer] The number of bytes in a valid key
def key_bytes
KEYBYTES
end
end
end
end
13 changes: 12 additions & 1 deletion lib/rbnacl/test_vectors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,18 @@ module RbNaCl
"b89ad929530a1bb3ab5e69f24c7f6070c8f840c9abb4f69fbfc8a7ff5126faee" \
"bbb55805ee9c1cf2ce5a57263287aec5780f04ec324c3514122cfc3231fc1a8b" \
"718a62863730a2702bb76366116bed09e0fd5c6d84b6b0c1abaf249d5dd0f7f5" \
"a7ea"
"a7ea",

# XChaCha20 test vectors
# Taken from https://github.com/jedisct1/libsodium/blob/1.0.20-RELEASE/test/default/xchacha20.c
secretbox_xchacha20poly1305_key: "d3c71d54e6b13506e07aa2e7b412a17a7a1f34df3d3148cd3f45b91ccaa5f4d9",
secretbox_xchacha20poly1305_message: "76bd706e07741e713d90efdb34ad202067263f984942aae8bda159f30dfccc72" \
"200f8093520b85c5ad124ff7c8b2d920946e5cfff4b819abf84c7b35a6205ca7" \
"2c9f8747c3044dd73fb4bebda1b476",
secretbox_xchacha20poly1305_nonce: "943b454a853aa514c63cf99b1e197bbb99da24b2e2d93e47",
secretbox_xchacha20poly1305_ciphertext: "0384276f1cfa5c82c3e58f0f2acc1f821c6f526d2c19557cf8bd270fcde43fba" \
"1d88890663f7b2f5c6b1d7deccf5c91b4df5865dc55cc7e04d6793fc2db8f9e3" \
"b418f95cb796d67a7f3f7e097150cb607c435dacf82eac3d669866e5092ace"
}.freeze
end
# rubocop:enable Metrics/ModuleLength
28 changes: 28 additions & 0 deletions spec/rbnacl/boxes/xchacha20poly1305_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# encoding: binary
# frozen_string_literal: true

RSpec.describe RbNaCl::SecretBoxes::XChaCha20Poly1305 do
let(:key) { vector :secretbox_xchacha20poly1305_key }
let(:box) { described_class.new(key) }

context "new" do
it "accepts strings" do
expect { described_class.new(key) }.to_not raise_error
end

it "raises on a nil key" do
expect { described_class.new(nil) }.to raise_error(TypeError)
end

it "raises on a short key" do
expect { described_class.new("hello") }.to raise_error RbNaCl::LengthError
end
end


include_examples "box" do
let(:nonce) { vector :secretbox_xchacha20poly1305_nonce }
let(:message) { vector :secretbox_xchacha20poly1305_message }
let(:ciphertext) { vector :secretbox_xchacha20poly1305_ciphertext }
end
end
6 changes: 6 additions & 0 deletions spec/shared/box.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@
end.to raise_error(RbNaCl::CryptoError, /Decryption failed. Ciphertext failed verification./)
end

it "raises on a message too short to contain the authentication tag" do
expect do
box.open(nonce, ciphertext[0, 7])
end.to raise_error(RbNaCl::CryptoError, /Decryption failed. Ciphertext failed verification./)
end

it "raises on a corrupt ciphertext" do
expect do
box.open(nonce, corrupt_ciphertext)
Expand Down
Loading