176 lines
5.7 KiB
Crystal
176 lines
5.7 KiB
Crystal
require "../lib_sodium"
|
|
require "../secure_buffer"
|
|
|
|
module Sodium::Cipher
|
|
abstract class SecretStream
|
|
@state : SecureBuffer
|
|
@encrypt_decrypt = 0
|
|
@initialized = false
|
|
|
|
# * Set tag before encrypting
|
|
# * Tag is set after decrypting
|
|
property tag = 0_u8
|
|
|
|
# Used to authentication but not encrypt additional data.
|
|
#
|
|
# * Set this before encrypting **and** decrypting.
|
|
# * This property is set to nil after calling .update.
|
|
property additional : Bytes? = nil
|
|
|
|
@key : Bytes | SecureBuffer | Nil = nil
|
|
|
|
def initialize
|
|
@state = SecureBuffer.new state_size
|
|
end
|
|
|
|
def encrypt
|
|
@encrypt_decrypt = 1
|
|
end
|
|
|
|
def decrypt
|
|
@encrypt_decrypt = -1
|
|
end
|
|
|
|
def key=(key : Bytes | SecureBuffer)
|
|
raise ArgumentError.new("key must be #{key_size} bytes, got #{key.bytesize}") if key.bytesize != key_size
|
|
@key = key
|
|
key
|
|
end
|
|
|
|
# Returns a random key in a SecureBuffer.
|
|
def random_key
|
|
self.key = SecureBuffer.random key_size
|
|
end
|
|
|
|
# Only used for encryption.
|
|
def header
|
|
raise "only call when encrypting" if @encrypt_decrypt != 1
|
|
buf = Bytes.new header_size
|
|
init_state buf
|
|
buf
|
|
end
|
|
|
|
# Only used for decryption.
|
|
def header=(buf : Bytes)
|
|
raise "only call when decrypting" if @encrypt_decrypt != -1
|
|
init_state buf
|
|
buf
|
|
end
|
|
|
|
def update(src : Bytes) : Bytes
|
|
update src, Bytes.new(src.bytesize + (auth_tag_size * @encrypt_decrypt))
|
|
end
|
|
|
|
# Provided for compatibility with block ciphers.
|
|
# Stream ciphers don't have additional data.
|
|
def final
|
|
Bytes.new(0)
|
|
end
|
|
|
|
abstract def update(src : Bytes, dst : Bytes)
|
|
abstract def init_state(header_buf : Bytes) : Nil
|
|
protected abstract def state_size : Int32
|
|
abstract def key_size : Int32
|
|
abstract def header_size : Int32
|
|
abstract def auth_tag_size : Int32
|
|
end
|
|
|
|
{% for key, val in {"XChaCha20Poly1305" => "xchacha20poly1305"} %}
|
|
# [Libsodium Secret Stream API](https://libsodium.gitbook.io/doc/secret-key_cryptography/secretstream)
|
|
#
|
|
# This class mimicks the OpenSSL::Cipher interface with minor differences.
|
|
# * .header must be called for encryption before calling .update
|
|
# * .header= must be called for decryption with the data returned from .header before calling .update
|
|
# * every .update is it's own authenticated message. Unlike OpenSSL this class doesn't buffer data. You must handle the framing yourself.
|
|
# * A tag may be set before encrypting and is set after calling .update when decrypting.
|
|
# * .additional may be set before encrypting and must be set before decrypting.
|
|
#
|
|
# See `spec/sodium/cipher/secret_stream_spec.cr` for examples on how to use this class.
|
|
#
|
|
# WARNING: Not verified against test vectors.
|
|
class SecretStream::{{ key.id }} < SecretStream
|
|
def update(src : Bytes, dst : Bytes) : Bytes
|
|
raise Sodium::Error.new("must call .header or .header= first") unless @initialized
|
|
min_dst_size = src.bytesize + (auth_tag_size * @encrypt_decrypt)
|
|
raise ArgumentError.new("dst bytesize must at least #{min_dst_size}, got #{dst.bytesize}") if dst.bytesize < min_dst_size
|
|
|
|
ad, ad_size = if a = @additional
|
|
{a.to_unsafe, a.bytesize}
|
|
else
|
|
{Pointer(UInt8).null, 0}
|
|
end
|
|
|
|
case @encrypt_decrypt
|
|
when 1
|
|
if LibSodium.crypto_secretstream_{{ val.id }}_push(@state.to_slice, dst.to_slice, out dst_size, src, src.bytesize, ad, ad_size, @tag) != 0
|
|
raise Sodium::Error.new("crypto_streamsecret_{{ val.id }}_xor_ic")
|
|
end
|
|
@tag = 0
|
|
@additional = nil
|
|
dst[0, dst_size]
|
|
when -1
|
|
if LibSodium.crypto_secretstream_{{ val.id }}_pull(@state.to_slice, dst.to_slice, out dst_size2, out @tag, src, src.bytesize, ad, ad_size) != 0
|
|
raise Sodium::Error.new("crypto_streamsecret_{{ val.id }}_xor_ic")
|
|
end
|
|
@additional = nil
|
|
dst[0, dst_size2]
|
|
else
|
|
abort "invalid encrypt_decrypt state #{@encrypt_decrypt}"
|
|
end
|
|
end
|
|
|
|
protected def init_state(header_buf : Bytes) : Nil
|
|
raise Sodium::Error.new("can't initialize more than once") if @initialized
|
|
|
|
if k = @key
|
|
case @encrypt_decrypt
|
|
when 1
|
|
if LibSodium.crypto_secretstream_xchacha20poly1305_init_push(@state.to_slice, header_buf.to_slice, k.to_slice) != 0
|
|
raise Sodium::Error.new("crypto_secretstream_xchacha20poly1305_init_push")
|
|
end
|
|
when -1
|
|
if LibSodium.crypto_secretstream_xchacha20poly1305_init_pull(@state.to_slice, header_buf.to_slice, k.to_slice) != 0
|
|
raise Sodium::Error.new("crypto_secretstream_xchacha20poly1305_init_push")
|
|
end
|
|
when 0
|
|
raise Sodium::Error.new("must call .encrypt or .decrypt first")
|
|
else
|
|
abort "invalid encrypt_decrypt state #{@encrypt_decrypt}"
|
|
end
|
|
else
|
|
raise Sodium::Error.new("must set an encryption/decryption key")
|
|
end
|
|
|
|
@initialized = true
|
|
end
|
|
|
|
protected def state_size : Int32
|
|
LibSodium.crypto_secretstream_{{ val.id }}_statebytes.to_i
|
|
end
|
|
|
|
def key_size : Int32
|
|
LibSodium.crypto_secretstream_{{ val.id }}_keybytes.to_i
|
|
end
|
|
|
|
def header_size : Int32
|
|
LibSodium.crypto_secretstream_{{ val.id }}_headerbytes.to_i
|
|
end
|
|
|
|
def auth_tag_size : Int32
|
|
LibSodium.crypto_secretstream_{{ val.id }}_abytes.to_i
|
|
end
|
|
|
|
def tag_push
|
|
LibSodium.crypto_secretstream_{{ val.id }}_tag_push
|
|
end
|
|
|
|
def tag_rekey
|
|
LibSodium.crypto_secretstream_{{ val.id }}_tag_rekey
|
|
end
|
|
|
|
def tag_final
|
|
LibSodium.crypto_secretstream_{{ val.id }}_tag_final
|
|
end
|
|
end
|
|
{% end %}
|
|
end
|