Skip to content

Instantly share code, notes, and snippets.

@mattwildig
Last active September 29, 2021 09:27
Show Gist options
  • Save mattwildig/e900c6db5a9daaca575c0a5f194a7ba4 to your computer and use it in GitHub Desktop.
Save mattwildig/e900c6db5a9daaca575c0a5f194a7ba4 to your computer and use it in GitHub Desktop.
require 'rbnacl'
# Libsodium provides "raw" ChaCha20, but it’s not in RbNaCl. We can work round that with
# this little class.
class ChaCha20Stream
extend RbNaCl::Sodium
sodium_function :get_stream,
:crypto_stream_chacha20,
%i[pointer ulong_long pointer pointer]
# Provides the first length bytes of the keystream for the given key and nonce.
def self.keystream(key, nonce, length)
stream = RbNaCl::Util.zeros(length)
stream_len = length
get_stream(stream, stream_len, nonce, key)
stream
end
end
# Utility for displaying raw data nicely.
def to_hex(data)
data.unpack('H*')[0]
end
# Bytewise XOR of the two strings, which must be the same length.
def xor(data, keystream)
data.bytes.zip(keystream.bytes).map {|a,b| ((a ^ b) & 0xff).chr}.join
end
# The length of buffers for the MAC are formatted as 8 bytes little endian.
def formatted_length(data)
[data.length].pack('Q<')
end
# In the IETF variant associated data and cipher text are both zero padded to
# a multiple of 16 bytes.
def pad16(data)
return data if data.length % 16 == 0
padding_len = 16 - data.length % 16
data + "\x00" * padding_len
end
# Data from the question.
nonce = ['0000000000000001'].pack('H*') # 8 bytes
key = ['b78b94bdf407e2fb0c4cb01e74fee7db743d4d5ab636fe4c181511137dedfc46'].pack('H*')
plain_text = ['0000000000000001'].pack('H*')
associated_data = ""
# First 32 bytes of the keystream are used as the auth key, the next 32 bytes
# are thrown away, and the rest is used for the encryption.
keystream = ChaCha20Stream.keystream(key, nonce, 64 + plain_text.length)
auth_key = keystream[0...32]
encryption_stream = keystream[64..-1]
# RbNaCl does provide "raw" access to Poly1305.
auth = RbNaCl::OneTimeAuths::Poly1305.new(auth_key)
# XOR the plaintext to the cipher stream after the 64th byte.
ciphertext = xor(plain_text, encryption_stream)
puts "Ciphertext: #{to_hex(ciphertext)}"
# The legacy variant calculates the authentication tag over
#
#. AD | len(AD) | CT | len(CT)
#
# (AD = associated data, CT = ciphertext).
legacy_data = associated_data + formatted_length(associated_data) + ciphertext + formatted_length(ciphertext)
legacy_tag = auth.auth(legacy_data)
puts "Legacy tag: #{to_hex(legacy_tag)}"
# The IETF variant calculates the authentication tag over
#
#. pad(AD) | pad(CT) | len(AD) | len(CT)
#
# Note both the padding and the order compared to the original variant.
ietf_data = pad16(associated_data) + pad16(ciphertext) + formatted_length(associated_data) + formatted_length(ciphertext)
ietf_tag = auth.auth(ietf_data)
puts "IETF tag: #{to_hex(ietf_tag)}"
Ciphertext: 78260b2aca088071
Legacy tag: 3c8eea6f05b671ed72f1bc61fee7cc22
IETF tag: 4d888c3b8fe1a4ab8a28d5e593fe7a25
require 'rbnacl'
key = ['b78b94bdf407e2fb0c4cb01e74fee7db743d4d5ab636fe4c181511137dedfc46'].pack('H*')
nonce_legacy = ['0000000000000001'].pack('H*') # 8 bytes
nonce_ietf = ['000000000000000000000001'].pack('H*') # 12 bytes
plain_text = ['0000000000000001'].pack('H*')
associated_data = ""
legacy = RbNaCl::AEAD::ChaCha20Poly1305Legacy.new(key)
ietf = RbNaCl::AEAD::ChaCha20Poly1305IETF.new(key)
ciphertext_legacy = legacy.encrypt(nonce_legacy, plain_text, associated_data)
ciphertext_ietf = ietf.encrypt(nonce_ietf, plain_text, associated_data)
puts "legacy: #{ciphertext_legacy[-16..-1].unpack('H*')[0]} #{ciphertext_legacy[0...-16].unpack('H*')[0]}"
puts "ietf: #{ciphertext_ietf[-16..-1].unpack('H*')[0]} #{ciphertext_ietf[0...-16].unpack('H*')[0]}"
legacy: 3c8eea6f05b671ed72f1bc61fee7cc22 78260b2aca088071
ietf: 4d888c3b8fe1a4ab8a28d5e593fe7a25 78260b2aca088071
@gi097
Copy link

gi097 commented Aug 2, 2019

Thanks a lot man! 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment