Add Sodium::Cipher::SecretStream

master
Didactic Drunk 2019-07-08 23:39:00 -07:00
parent 65ad5987d4
commit 96b215cf54
4 changed files with 304 additions and 1 deletions

View File

@ -24,6 +24,7 @@ Crystal bindings for the [libsodium API](https://libsodium.gitbook.io/doc/)
- Secret Box
- [x] [Combined mode](https://libsodium.gitbook.io/doc/secret-key_cryptography/authenticated_encryption)
- [ ] [Detached mode](https://libsodium.gitbook.io/doc/secret-key_cryptography/authenticated_encryption)
- [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
@ -34,7 +35,7 @@ Crystal bindings for the [libsodium API](https://libsodium.gitbook.io/doc/)
- [x] Complete libsodium implementation including `key`, `salt`, `personal` and fully selectable output sizes.
- [ ] [SipHash](https://libsodium.gitbook.io/doc/hashing/short-input_hashing)
- [Password Hashing](https://libsodium.gitbook.io/doc/password_hashing)
- [x] [x] [Argon2](https://libsodium.gitbook.io/doc/password_hashing/the_argon2i_function) (Use for new applications)
- [x] [Argon2](https://libsodium.gitbook.io/doc/password_hashing/the_argon2i_function) (Use for new applications)
- [ ] [Scrypt](https://libsodium.gitbook.io/doc/advanced/scrypt) (For compatibility with older applications)
- Other
- [x] [Key Derivation](https://libsodium.gitbook.io/doc/key_derivation)

View File

@ -0,0 +1,82 @@
require "../../spec_helper"
require "../../../src/sodium/cipher/secret_stream"
private def new_ciphers
cipher1 = Sodium::Cipher::SecretStream::XChaCha20Poly1305.new
cipher2 = Sodium::Cipher::SecretStream::XChaCha20Poly1305.new
cipher1.encrypt
cipher2.decrypt
{cipher1, cipher2}
end
private def new_ciphers_with_data
data = Bytes.new(100)
data.bytesize.times do |i|
data[i] = (i % 256).to_u8
end
cipher1, cipher2 = new_ciphers
key = cipher1.random_key
cipher2.key = key
header = cipher1.header
cipher2.header = header
{cipher1, cipher2, data}
end
# TODO: verify against test vectors.
describe Sodium::Cipher::SecretStream do
it "encrypts/decrypts" do
cipher1, cipher2, data = new_ciphers_with_data
3.times do
output = cipher1.update data
output.should_not eq data
cipher2.update(output).should eq data
end
cipher1.final.should eq Bytes.new(0)
cipher2.final.should eq Bytes.new(0)
end
it "encrypts/decrypts with additional" do
cipher1, cipher2, data = new_ciphers_with_data
["foo", "bar", nil, "baz"].each do |additional|
additional = additional.try &.to_slice
cipher1.additional = additional
output = cipher1.update data
cipher1.additional.should eq nil # Additional reset after encrypt.
output.should_not eq data
cipher2.additional = additional
cipher2.update(output).should eq data
cipher2.additional.should eq nil # Additional reset after encrypt.
end
cipher1.final.should eq Bytes.new(0)
cipher2.final.should eq Bytes.new(0)
end
it "encrypts/decrypts with tags" do
cipher1, cipher2, data = new_ciphers_with_data
[cipher1.tag_push, cipher1.tag_rekey, cipher1.tag_final].each do |tag|
cipher1.tag = tag
output = cipher1.update data
cipher1.tag.should eq 0_u8 # Tag reset after encrypt.
output.should_not eq data
cipher2.update(output).should eq data
cipher2.tag.should eq tag # Tag set on decrypt.
end
cipher1.final.should eq Bytes.new(0)
cipher2.final.should eq Bytes.new(0)
end
end

View File

@ -0,0 +1,176 @@
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

View File

@ -72,6 +72,50 @@ module Sodium
key : Pointer(LibC::UChar)
) : LibC::Int
{% for name in ["_xchacha20poly1305"] %}
{% for name2 in %w(keybytes headerbytes statebytes abytes) %}
fun crypto_secretstream{{ name.id }}_{{ name2.id }} : LibC::SizeT
{% end %}
{% for name2 in %w(tag_rekey tag_push tag_final) %}
fun crypto_secretstream{{ name.id }}_{{ name2.id }} : LibC::UChar
{% end %}
fun crypto_secretstream{{ name.id }}_init_push(
state : Pointer(LibC::UChar),
header : Pointer(LibC::UChar),
key : Pointer(LibC::UChar),
) : LibC::Int
fun crypto_secretstream{{ name.id }}_init_pull(
state : Pointer(LibC::UChar),
header : Pointer(LibC::UChar),
key : Pointer(LibC::UChar),
) : LibC::Int
fun crypto_secretstream{{ name.id }}_push(
state : Pointer(LibC::UChar),
c : Pointer(LibC::UChar),
clen : Pointer(LibC::ULongLong),
m : Pointer(LibC::UChar),
mlen : LibC::ULongLong,
ad : Pointer(LibC::UChar),
adlen : LibC::ULongLong,
tag : LibC::UChar,
) : LibC::Int
fun crypto_secretstream{{ name.id }}_pull(
state : Pointer(LibC::UChar),
m : Pointer(LibC::UChar),
mlen : Pointer(LibC::ULongLong),
tag : Pointer(LibC::UChar),
c : Pointer(LibC::UChar),
clen : LibC::ULongLong,
ad : Pointer(LibC::UChar),
adlen : LibC::ULongLong,
) : 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