Sodium::Cipher::Aead::Xchacha20Poly1305Ietf Add class and basic specs.
parent
c3a9fe178a
commit
192044b27a
|
@ -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)
|
||||
- [AEAD](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead)
|
||||
- [ ] AES256-GCM (Requires hardware acceleration)
|
||||
- [ ] XChaCha20-Poly1305-IETF
|
||||
- [ ] ChaCha20-Poly1305-IETF
|
||||
- [ ] ChaCha20-Poly1305
|
||||
- [x] [XChaCha20-Poly1305-IETF](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/xchacha20-poly1305_construction)
|
||||
- [ ] [ChaCha20-Poly1305-IETF](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/ietf_chacha20-poly1305_construction)
|
||||
- [ ] [ChaCha20-Poly1305](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305)
|
||||
- [Hashing](https://libsodium.gitbook.io/doc/hashing)
|
||||
- [x] ☑ [Blake2b](https://libsodium.gitbook.io/doc/hashing/generic_hashing)
|
||||
- [x] Complete libsodium implementation including `key`, `salt`, `personal` and fully selectable output sizes.
|
||||
|
|
|
@ -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 %}
|
|
@ -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
|
|
@ -121,6 +121,38 @@ module Sodium
|
|||
) : LibC::Int
|
||||
{% 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.
|
||||
{% for name in ["_chacha20", "_chacha20_ietf", "_xchacha20", "_salsa20", "_xsalsa20"] %}
|
||||
fun crypto_stream{{ name.id }}_keybytes() : LibC::SizeT
|
||||
|
|
|
@ -18,6 +18,8 @@ module Sodium
|
|||
# Returns bytes
|
||||
delegate to_slice, to: @bytes
|
||||
|
||||
delegate bytesize, to: @bytes
|
||||
|
||||
def initialize(@bytes : Bytes)
|
||||
if bytes.bytesize != NONCE_SIZE
|
||||
raise ArgumentError.new("Nonce must be #{NONCE_SIZE} bytes, got #{bytes.bytesize}")
|
||||
|
|
Loading…
Reference in New Issue