Sodium::Cipher::Aead::Xchacha20Poly1305Ietf Add class and basic specs.
This commit is contained in:
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)
|
- [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.
|
||||||
|
96
spec/sodium/cipher/aead/chalsa_spec.cr
Normal file
96
spec/sodium/cipher/aead/chalsa_spec.cr
Normal 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 %}
|
98
src/sodium/cipher/aead/chalsa.cr
Normal file
98
src/sodium/cipher/aead/chalsa.cr
Normal 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
|
@ -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
|
||||||
|
@ -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}")
|
||||||
|
Loading…
Reference in New Issue
Block a user