Sodium::Cipher::Aead::Xchacha20Poly1305Ietf Add class and basic specs.

This commit is contained in:
Didactic Drunk 2019-09-13 02:04:49 -07:00
parent c3a9fe178a
commit 192044b27a
5 changed files with 231 additions and 3 deletions

View File

@ -29,9 +29,9 @@ Crystal bindings for the [libsodium API](https://libsodium.gitbook.io/doc/)
- [x] [Secret Stream](https://libsodium.gitbook.io/doc/secret-key_cryptography/secretstream) - [x] [Secret Stream](https://libsodium.gitbook.io/doc/secret-key_cryptography/secretstream)
- [AEAD](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead) - [AEAD](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead)
- [ ] AES256-GCM (Requires hardware acceleration) - [ ] AES256-GCM (Requires hardware acceleration)
- [ ] XChaCha20-Poly1305-IETF - [x] [XChaCha20-Poly1305-IETF](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/xchacha20-poly1305_construction)
- [ ] ChaCha20-Poly1305-IETF - [ ] [ChaCha20-Poly1305-IETF](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/ietf_chacha20-poly1305_construction)
- [ ] ChaCha20-Poly1305 - [ ] [ChaCha20-Poly1305](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305)
- [Hashing](https://libsodium.gitbook.io/doc/hashing) - [Hashing](https://libsodium.gitbook.io/doc/hashing)
- [x] ☑ [Blake2b](https://libsodium.gitbook.io/doc/hashing/generic_hashing) - [x] ☑ [Blake2b](https://libsodium.gitbook.io/doc/hashing/generic_hashing)
- [x] Complete libsodium implementation including `key`, `salt`, `personal` and fully selectable output sizes. - [x] Complete libsodium implementation including `key`, `salt`, `personal` and fully selectable output sizes.

View File

@ -0,0 +1,96 @@
require "../../../spec_helper"
require "../../../../src/sodium/cipher/aead/chalsa"
combined_test_vectors = [
{
key: "1b27556473e985d462cd51197a9a46c76009549eac6474f206c4ee0844f68389",
nonce: "69696ee955b62b73cd62bda875fc73d68219e0036b7a0b37",
plaintext: "be075fc53c81f2d5cf141316ebeb0c7b5228c52a4c62cbd44b66849b64244ffce5e" \
"cbaaf33bd751a1ac728d45e6c61296cdc3c01233561f41db66cce314adb310e3be8" \
"250c46f06dceea3a7fa1348057e2f6556ad6b1318a024a838f21af1fde048977eb4" \
"8f59ffd4924ca1c60902e52f0a089bc76897040e082f937763848645e0705",
ciphertext: "f3ffc7703f9400e52a7dfb4b3d3305d98e993b9f48681273c29650ba32fc76ce483" \
"32ea7164d96a4476fb8c531a1186ac0dfc17c98dce87b4da7f011ec48c97271d2c2" \
"0f9b928fe2270d6fb863d51738b48eeee314a7cc8ab932164548e526ae902243685" \
"17acfeabd6bb3732bc0e9da99832b61ca01b6de56244a9e88d5f9b37973f622a43d" \
"14a6599b1f654cb45a74e355a5",
},
]
private def box_from_test_vector(vec)
box = Sodium::SecretBox.new vec[:key].hexbytes
nonce = Sodium::Nonce.new vec[:nonce].hexbytes
plaintext = vec[:plaintext].hexbytes
ciphertext = vec[:ciphertext].hexbytes
{box, nonce, plaintext, ciphertext}
end
{% for name in %w(Xchacha20Poly1305Ietf) %}
# TODO: verify against test vectors.
describe Sodium::Cipher::Aead::{{ name.id }} do
it "encrypts/decrypts" do
box = Sodium::Cipher::Aead::{{ name.id }}.new
message = "foobar"
additional = "foo"
mac, encrypted, nonce = box.encrypt_detached message, additional: additional
decrypted = box.decrypt_detached encrypted, nonce: nonce, mac: mac, additional: additional
message.should eq String.new(decrypted)
# Wrong additional.
expect_raises(Sodium::Error::DecryptionFailed) do
box.decrypt_detached encrypted, nonce: nonce, mac: mac, additional: "bar".to_slice
end
# Missing additional.
expect_raises(Sodium::Error::DecryptionFailed) do
box.decrypt_detached encrypted, nonce: nonce, mac: mac
end
# Wrong data.
expect_raises(Sodium::Error::DecryptionFailed) do
box.decrypt_detached "badmsgbadmsgbadmsgbadmsgbadmsg".to_slice, nonce: nonce, mac: mac
end
end
it "can't encrypt twice using the same nonce" do
box = Sodium::Cipher::Aead::{{ name.id }}.new
message = "foobar"
mac, encrypted, nonce = box.encrypt_detached message
expect_raises(Sodium::Nonce::Error::Reused) do
box.encrypt_detached message.to_slice, nonce: nonce
end
end
end
{% end %}
{% if false %}
describe Sodium::Cipher::Aead do
it "PyNaCl combined test vectors" do
combined_test_vectors.each do |vec|
box, nonce, plaintext, ciphertext = box_from_test_vector vec
encrypted, _ = box.encrypt plaintext, nonce: nonce
encrypted.should eq ciphertext
decrypted = box.decrypt encrypted, nonce: nonce
plaintext.should eq decrypted
end
end
pending "detached test vectors" do
detached_test_vectors.each do |vec|
box, nonce, plaintext, ciphertext = box_from_test_vector vec
encrypted = box.encrypt_detached plaintext, nonce
encrypted.should eq ciphertext
decrypted = box.decrypt_detached encrypted, nonce
plaintext.should eq decrypted
end
end
end
{% end %}

View File

@ -0,0 +1,98 @@
require "../../lib_sodium"
require "../../secure_buffer"
require "../../nonce"
module Sodium::Cipher::Aead
abstract class Chalsa
@key : Bytes | SecureBuffer
def initialize
@key = SecureBuffer.random key_size
end
def initialize(@key)
raise ArgumentError.new("key size mismatch, got #{@key.bytesize}, wanted #{key_size}") if @key.bytesize != key_size
end
# Encrypts data and returns {ciphertext, nonce}
def encrypt(data)
encrypt data.to_slice
end
# Encrypts data and returns {mac, ciphertext, nonce}
def encrypt_detached(data, dst : Bytes? = nil, *, mac : Bytes? = nil, additional = nil)
encrypt_detached data.to_slice, mac: mac, additional: additional
end
# Decrypts data and returns plaintext
# Must supply `mac` and `nonce`
# Must supply `additional` if supplied to #encrypt
def decrypt_detached(data, dst : Bytes? = nil, *, mac : Bytes? = nil, additional = nil)
encrypt_detached data.to_slice, mac: mac, additional: additional
end
abstract def encrypt_detached(src : Bytes, dst : Bytes? = nil, *, nonce : Sodium::Nonce? = nil, mac : Bytes? = nil, additional : String | Bytes | Nil = nil) : {Bytes, Bytes, Sodium::Nonce}
abstract def decrypt_detached(src : Bytes, dst : Bytes? = nil, *, nonce : Sodium::Nonce, mac : Bytes, additional : String | Bytes | Nil = nil) : Bytes
protected abstract def key_size : Int32
end
{% for key, val in {"Xchacha20Poly1305Ietf" => "_xchacha20poly1305_ietf"} %}
# Use like `SecretBox` with optional additional authenticated data.
#
# See [https://libsodium.gitbook.io/doc/secret-key_cryptography/aead](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead)
#
# See `spec/sodium/cipher/aead/chalsa_spec.cr` for examples on how to use this class.
#
# WARNING: Not validated against test vectors. You should probably write some before using this class.
class {{ key.id }} < Chalsa
KEY_SIZE = LibSodium.crypto_aead{{ val.id }}_keybytes.to_i32
MAC_SIZE = LibSodium.crypto_aead{{ val.id }}_abytes.to_i32
NONCE_SIZE = LibSodium.crypto_aead{{ val.id }}_npubbytes.to_i32
# `src` and `dst` may be the same object but should not overlap.
# May supply `mac`, otherwise a new one is returned.
# May supply `additional`
def encrypt_detached(src : Bytes, dst : Bytes? = nil, nonce : Sodium::Nonce? = nil, *, mac : Bytes? = nil, additional : String | Bytes | Nil = nil) : {Bytes, Bytes, Sodium::Nonce}
dst ||= Bytes.new(src.bytesize)
nonce ||= Sodium::Nonce.random
mac ||= Bytes.new MAC_SIZE
raise ArgumentError.new("src and dst bytesize must be identical") if src.bytesize != dst.bytesize
raise ArgumentError.new("nonce size mismatch, got #{nonce.bytesize}, wanted #{NONCE_SIZE}") unless nonce.bytesize == NONCE_SIZE
raise ArgumentError.new("mac size mismatch, got #{mac.bytesize}, wanted #{MAC_SIZE}") unless mac.bytesize == MAC_SIZE
additional = additional.try &.to_slice
ad_len = additional.try(&.bytesize) || 0
nonce.used!
if LibSodium.crypto_aead{{ val.id }}_encrypt_detached(dst, mac, out mac_len, src, src.bytesize, additional, ad_len, nil, nonce.to_slice, @key.to_slice) != 0
raise Sodium::Error.new("crypto_aead_{{ val.id }}_encrypt_detached")
end
raise Sodium::Error.new("crypto_aead_{{ val.id }}_encrypt_detached mac size mismatch") if mac_len != MAC_SIZE
{mac, dst, nonce}
end
# src and dst may be the same object but should not overlap.
# Must supply `mac` and `nonce`
# Must supply `additional` if supplied to #encrypt_detached
def decrypt_detached(src : Bytes, dst : Bytes? = nil, *, nonce : Sodium::Nonce, mac : Bytes, additional : String | Bytes | Nil = nil) : Bytes
dst ||= Bytes.new(src.bytesize)
raise ArgumentError.new("src and dst bytesize must be identical") if src.bytesize != dst.bytesize
raise ArgumentError.new("nonce size mismatch, got #{nonce.bytesize}, wanted #{NONCE_SIZE}") unless nonce.bytesize == NONCE_SIZE
raise ArgumentError.new("mac size mismatch, got #{mac.bytesize}, wanted #{MAC_SIZE}") unless mac.bytesize == MAC_SIZE
ad_len = additional.try(&.bytesize) || 0
if LibSodium.crypto_aead{{ val.id }}_decrypt_detached(dst, nil, src, src.bytesize, mac, additional, ad_len, nonce.to_slice, @key.to_slice) != 0
raise Sodium::Error::DecryptionFailed.new("crypto_aead_{{ val.id }}_decrypt_detached")
end
dst
end
protected def key_size
KEY_SIZE
end
end
{% end %}
end

View File

@ -121,6 +121,38 @@ module Sodium
) : LibC::Int ) : LibC::Int
{% end %} {% end %}
# AEAD
{% for name in ["_chacha20poly1305", "_chacha20poly1305_ietf", "_xchacha20poly1305_ietf"] %}
fun crypto_aead{{ name.id }}_keybytes() : LibC::SizeT
fun crypto_aead{{ name.id }}_abytes() : LibC::SizeT
fun crypto_aead{{ name.id }}_npubbytes() : LibC::SizeT # Nonce
fun crypto_aead{{ name.id }}_encrypt_detached(
c : Pointer(LibC::UChar),
mac : Pointer(LibC::UChar),
mac_len : Pointer(LibC::ULongLong),
m : Pointer(LibC::UChar),
len : LibC::ULongLong,
ad : Pointer(LibC::UChar),
ad_lenlen : LibC::ULongLong,
nsec : Pointer(LibC::UChar),
nonce : Pointer(LibC::UChar),
key : Pointer(LibC::UChar)
) : LibC::Int
fun crypto_aead{{ name.id }}_decrypt_detached(
m : Pointer(LibC::UChar),
nsec : Pointer(LibC::UChar),
c : Pointer(LibC::UChar),
len : LibC::ULongLong,
mac : Pointer(LibC::UChar),
ad : Pointer(LibC::UChar),
ad_lenlen : LibC::ULongLong,
nonce : Pointer(LibC::UChar),
key : Pointer(LibC::UChar)
) : LibC::Int
{% end %}
# TODO: Add reduced round variants. # TODO: Add reduced round variants.
{% for name in ["_chacha20", "_chacha20_ietf", "_xchacha20", "_salsa20", "_xsalsa20"] %} {% for name in ["_chacha20", "_chacha20_ietf", "_xchacha20", "_salsa20", "_xsalsa20"] %}
fun crypto_stream{{ name.id }}_keybytes() : LibC::SizeT fun crypto_stream{{ name.id }}_keybytes() : LibC::SizeT

View File

@ -18,6 +18,8 @@ module Sodium
# Returns bytes # Returns bytes
delegate to_slice, to: @bytes delegate to_slice, to: @bytes
delegate bytesize, to: @bytes
def initialize(@bytes : Bytes) def initialize(@bytes : Bytes)
if bytes.bytesize != NONCE_SIZE if bytes.bytesize != NONCE_SIZE
raise ArgumentError.new("Nonce must be #{NONCE_SIZE} bytes, got #{bytes.bytesize}") raise ArgumentError.new("Nonce must be #{NONCE_SIZE} bytes, got #{bytes.bytesize}")