XChaCha20

From Crypto++ Wiki
Jump to navigation Jump to search

XChaCha20 is a modified version of ChaCha created by Scott Arciszewski that is hardened against nonce misuse. Crypto++ provides the algorithm by way of the XChaCha20 class. XChaCha20 only offers a 32-byte key with a 24-byte nonce and 20 rounds. Also see Issue 727, XChaCha20 support and draft-arciszewski-xchacha, XChaCha: eXtended-nonce ChaCha and AEAD_XChaCha20_Poly1305.

Crypto++ provides all stream ciphers from eSTREAM Phase 3 for Profile 1. The ciphers are ChaCha, HC-128/256, Rabbit, Salsa20 and Sosemanuk. The IETF's version of ChaCha is specified in RFC 8439, ChaCha20 and Poly1305 for IETF Protocols and available as XChaCha20.

The library also provides or ChaCha20Poly1305. ChaCha20Poly1305 is a authenticated encryption scheme and creates an authentication tag over the ciphertext to provide authenticity assurances. ChaCha20Poly1305 is also specified in RFC 8439, ChaCha20 and Poly1305 for IETF Protocols.

If you are used to working in languages like Jave or libraries like OpenSSL, then you might want to visit the Init-Update-Final wiki page. Crypto++ provides the transformation model, but its not obvious because its often shrouded behind Pipelines.

Note: if your project is using encryption alone to secure your data, encryption alone is usually not enough. Please take a moment to read Authenticated Encryption and understand why you should consider using a block cipher and mode like CCM, GCM, EAX, ChaCha20Poly1305 or XChaCha20Poly1305.

Key and IV sizes

The first sample program prints XChaCha20's key and iv sizes.

int main()
{
    using namespace CryptoPP;

    XChaCha20::Encryption enc;
    std::cout << "key length: " << enc.DefaultKeyLength() << std::endl;
    std::cout << "key length (min): " << enc.MinKeyLength () << std::endl;
    std::cout << "key length (max): " << enc.MaxKeyLength () << std::endl;
    std::cout << "iv size: " << enc.IVSize() << std::endl;

    return 0;
}

A typical output is shown below.

$ ./test.exe
key length: 32
key length (min): 32
key length (max): 32
iv size: 24

Encryption and Decryption

The following example shows you how to use XChaCha20::Encryption and XChaCha20::Decryption. &cipher[0] may look odd, but its how to get the non-const pointer from a std::string.

#include "cryptlib.h"
#include "secblock.h"
#include "chacha.h"
#include "osrng.h"
#include "files.h"
#include "hex.h"

#include <iostream>
#include <string>

int main()
{
    using namespace CryptoPP;

    AutoSeededRandomPool prng;
	HexEncoder encoder(new FileSink(std::cout));
    std::string plain("My Plaintext!! My Dear plaintext!!"), cipher, recover;

    SecByteBlock key(32), iv(24);
    prng.GenerateBlock(key, key.size());
    prng.GenerateBlock(iv, iv.size());

    std::cout << "Key: ";
    encoder.Put((const byte*)key.data(), key.size());
    encoder.MessageEnd();
    std::cout << std::endl;

    std::cout << "IV: ";
    encoder.Put((const byte*)iv.data(), iv.size());
    encoder.MessageEnd();
    std::cout << std::endl;

    // Encryption object
    XChaCha20::Encryption enc;    
    enc.SetKeyWithIV(key, key.size(), iv, iv.size());

    // Perform the encryption
    cipher.resize(plain.size());
    enc.ProcessData((byte*)&cipher[0], (const byte*)plain.data(), plain.size());

    std::cout << "Plain: " << plain << std::endl;

    std::cout << "Cipher: ";
    encoder.Put((const byte*)cipher.data(), cipher.size());
    encoder.MessageEnd();
    std::cout << std::endl;

    XChaCha20::Decryption dec;
    dec.SetKeyWithIV(key, key.size(), iv, iv.size());

    // Perform the decryption
    recover.resize(cipher.size());
    dec.ProcessData((byte*)&recover[0], (const byte*)cipher.data(), cipher.size());

    std::cout << "Recovered: " << recover << std::endl;

    return 0;
}

A typical output is shown below, including the non-printable characters from encryption.

$ ./test.exe
Key: 5EC58B6D514FE0A56F1E0DEA7BDC095A10F5B618BDB6F2262FCC597BB230B3EF
IV: A345F5CF8023517CC0FCF075748C865F7DE8CA0C7236ABDA
Plain: My Plaintext!! My Dear plaintext!!
Cipher: EEA7C2711910656992E1CED816E20E621B25178236716AE499F29737A72AFCF86C72
Recovered: My Plaintext!! My Dear plaintext!!

Resynchronizing

The XChaCha20 family is self-inverting so you can use the encryption object for decryption (and vice versa). The cipher holds internal state and is resynchronizable. If you want to reuse an encryption or decryption object then you should set the IV with Resynchronize.

#include "cryptlib.h"
#include "secblock.h"
#include "chacha.h"
#include "osrng.h"
#include "files.h"
#include "hex.h"

#include <iostream>
#include <string>

int main()
{
    using namespace CryptoPP;

    AutoSeededRandomPool prng;
    HexEncoder encoder(new FileSink(std::cout));
    std::string plain("My Plaintext!! My Dear plaintext!!"), cipher, recover;

    SecByteBlock key(32), iv(24);
    prng.GenerateBlock(key, key.size());
    prng.GenerateBlock(iv, iv.size());

    std::cout << "Key: ";
    encoder.Put((const byte*)key.data(), key.size());
    encoder.MessageEnd();
    std::cout << std::endl;

    std::cout << "IV: ";
    encoder.Put((const byte*)iv.data(), iv.size());
    encoder.MessageEnd();
    std::cout << std::endl;

    // Encryption object
    XChaCha20::Encryption enc;    
    enc.SetKeyWithIV(key, key.size(), iv, iv.size());

    // Perform the encryption
    cipher.resize(plain.size());
    enc.ProcessData((byte*)&cipher[0], (const byte*)plain.data(), plain.size());

    std::cout << "Plain: " << plain << std::endl;

    std::cout << "Cipher: ";
    encoder.Put((const byte*)cipher.data(), cipher.size());
    encoder.MessageEnd();
    std::cout << std::endl;

    // XChaCha20::Decryption dec;
    // dec.SetKeyWithIV(key, key.size(), iv, iv.size());

    std::cout << "Self inverting: " << enc.IsSelfInverting() << std::endl;
    std::cout << "Resynchronizable: " << enc.IsResynchronizable() << std::endl;

    enc.Resynchronize(iv, iv.size());

    // Perform the decryption
    // recover.resize(cipher.size());
    // dec.ProcessData((byte*)&recover[0], (const byte*)cipher.data(), cipher.size());

    // Perform the decryption with the encryptor
    recover.resize(cipher.size());
    enc.ProcessData((byte*)&recover[0], (const byte*)cipher.data(), cipher.size());

    std::cout << "Recovered: " << recover << std::endl;

    return 0;
}

A typical output is shown below, including the non-printable characters from encryption.

$ ./test.exe
Key: 9BD672B0E9DF0C0F6607308B1E03A9C2CFE4FBC3A0877A62B6D2E6C83CD38A69
IV: 2F17A67D963B935CB8F39032A2DC46AF16C4C04A20539921
Plain: My Plaintext!! My Dear plaintext!!
Cipher: 148307FB553C121598CC07C20A99DDFC466E229C6FCA3FC664557078480B5F931AA2
Self inverting: 1
Resynchronizable: 1
Recovered: My Plaintext!! My Dear plaintext!!

The following C++11 program demonstrates resynchronizing without the additional operations like printing a key or iv. The library was built with CXXFLAGS="-DNDEBUG -g2 -O3 -std=c++11.

#include "cryptlib.h"
#include "chacha.h"

#include <iostream>
#include <array>
#include <cstdint>

int main(int argc, char *argv[])
{
    using namespace CryptoPP;

    const uint8_t chachaKey[32] = "0123456789012345678901234";
    const uint8_t chachaIV[24] = "01234567890123456789012";

    XChaCha20::Encryption enc;
    XChaCha20::Decryption dec;
    enc.SetKeyWithIV(chachaKey, 32, chachaIV, 24);
    dec.SetKeyWithIV(chachaKey, 32, chachaIV, 24);

    std::array<byte, 3> origin = { 1,2,3 };
    std::array<byte, 3> encrpyt;
    enc.ProcessData(encrpyt.data(), origin.data(), origin.size());

    std::array<byte, 3> decrypt;
    dec.ProcessData(decrypt.data(), encrpyt.data(), encrpyt.size());

    dec.Resynchronize(chachaIV, sizeof(chachaIV));
    dec.ProcessData(decrypt.data(), encrpyt.data(), encrpyt.size());

    dec.Resynchronize(chachaIV, sizeof(chachaIV));
    dec.ProcessData(decrypt.data(), encrpyt.data(), encrpyt.size());
    
    std::cout << (int)decrypt[0] << " " << (int)decrypt[1] << " ";
    std::cout << (int)decrypt[2] << std::endl;
    
    return 0;
}

It produces the following result.

$ g++ -DNDEBUG -g2 -O3 -std=c++11 test.cxx -o test.exe ./libcryptopp.a
$ ./test.exe
1 2 3

Pipelines

You can also use stream ciphers in a Pipeline. Below is an example of XChaCha20 participating in a pipeline. Internally, StreamTransformationFilter calls ProcessData on the incoming data stream. The filter also buffers output if there is no attached transformation or sink.

#include "cryptlib.h"
#include "secblock.h"
#include "filters.h"
#include "chacha.h"
#include "osrng.h"
#include "files.h"
#include "hex.h"

#include <iostream>
#include <string>

int main()
{
    using namespace CryptoPP;

    AutoSeededRandomPool prng;
    HexEncoder encoder(new FileSink(std::cout));
    std::string plain("My Plaintext!! My Dear plaintext!!"), cipher, recover;

    SecByteBlock key(32), iv(24);
    prng.GenerateBlock(key, key.size());
    prng.GenerateBlock(iv, iv.size());

    std::cout << "Key: ";
    encoder.Put(key.data(), key.size());
    encoder.MessageEnd();
    std::cout << std::endl;

    std::cout << "IV: ";
    encoder.Put(iv.data(), iv.size());
    encoder.MessageEnd();
    std::cout << std::endl;

    // Encryption object
    XChaCha20::Encryption enc;    
    enc.SetKeyWithIV(key, key.size(), iv, iv.size());

    // Decryption object
    XChaCha20::Decryption dec;    
    dec.SetKeyWithIV(key, key.size(), iv, iv.size());

    StringSource ss1(plain, true, new StreamTransformationFilter(enc, new StringSink(cipher)));
    StringSource ss2(cipher, true, new StreamTransformationFilter(dec, new StringSink(recover)));

    std::cout << "Plain: " << plain << std::endl;

    std::cout << "Cipher: ";
    encoder.Put((const byte*)cipher.data(), cipher.size());
    encoder.MessageEnd();
    std::cout << std::endl;

    std::cout << "Recovered: " << recover << std::endl;

    return 0;
}

The program produces the expected output:

$ ./test.exe
Key: 2FD5F4C4BD617301492D5BDF01E3FEDD2627E6806B7D93233AE2F01288184623
IV: 1BE6F894559E1F60137ED8448BC465C80E9890EFD70C17DB
Plain: My Plaintext!! My Dear plaintext!!
Cipher: 18267F6CA1478E59DC9DD6D210B6BB7AB5648422DBA7509A7725F92AFAF0E0775F32
Recovered: My Plaintext!! My Dear plaintext!!

Initial Block Counter

The examples above use XChaCha20 with and initial block counter of 1. A counter of 1 is the default configuration and nothing special needs to be done for it. The XChaCha draft does not state what the permissible values for the initial block counter are. However, RFC 8439, Section 2.4 allows an arbitrary initial counter block. Since the counter block is 32-bits, the value can be 1 to 0xffffffff.

To use a different initial block counter then pass a InitialBlock parameter using a NameValuePairs when configuring the XChaCha20 object.

#include "cryptlib.h"
#include "secblock.h"
#include "algparam.h"
#include "argnames.h"
#include "filters.h"
#include "chacha.h"
#include "osrng.h"
#include "files.h"
#include "hex.h"

#include <iostream>
#include <string>

int main()
{
    using namespace CryptoPP;

    AutoSeededRandomPool prng;
    HexEncoder encoder(new FileSink(std::cout));
    std::string plain("My Plaintext!! My Dear plaintext!!"), cipher, recover;

    SecByteBlock key(32), iv(24);
    prng.GenerateBlock(key, key.size());
    prng.GenerateBlock(iv, iv.size());

    std::cout << "Key: ";
    encoder.Put(key.data(), key.size());
    encoder.MessageEnd();
    std::cout << std::endl;

    std::cout << "IV: ";
    encoder.Put(iv.data(), iv.size());
    encoder.MessageEnd();
    std::cout << std::endl;

    // Additional parameters to configure object
    const AlgorithmParameters params = MakeParameters("InitialBlock", 0x0000ffff)
                             (Name::IV(), ConstByteArrayParameter(iv, iv.size()));

    // Encryption object
    XChaCha20::Encryption enc;    
    enc.SetKey(key, key.size(), params);

    // Decryption object
    XChaCha20::Decryption dec;    
    dec.SetKey(key, key.size(), params);

    StringSource ss1(plain, true, new StreamTransformationFilter(enc, new StringSink(cipher)));
    StringSource ss2(cipher, true, new StreamTransformationFilter(dec, new StringSink(recover)));

    std::cout << "Plain: " << plain << std::endl;

    std::cout << "Cipher: ";
    encoder.Put((const byte*)cipher.data(), cipher.size());
    encoder.MessageEnd();
    std::cout << std::endl;

    std::cout << "Recovered: " << recover << std::endl;

    return 0;
}

The initial counter block is latched as the default value for the object. If you call Resynchronize then the counter block is reset to the value passed in during SetKey. If you want to change the default value, then you need to call SetKey again.

Sharp Edges

XChaCha20 has some sharp edges, just like other ciphers. ChaCha, ChaChaTLS and XChaCha20 require the application rekey at 232 blocks or 256 GB. However, unlike ChaCha, ChaChaTLS allows (1) the user to use an arbitrary initial block counter; and (2) uses a single 32-bit block counter. Combined they wander into areas that are unspecified by the RFC.

Bernstein's ChaCha uses a 64-bit word for the counter block and it cannot overflow for all intents and purposes. In addition Bernstein did not specify an initial counter block value other than 0. XChaCha20 can overflow quite easily by setting the initial counter block to 0xfffffffe and then encrypting or decrypting 256-bytes. The problem is, the RFC does not say what is supposed to happen in this case.

Several things could happen in this case. They include wrapping the block counter, setting the counter to 0 and carrying into the nonce, setting the counter to 0 and re-keying, or throwing an exception. The Crypto++ library silently allows the counter to wrap in Release builds, and fires an assert in Debug builds. The application is responsible for managing the parameters. If the application does not want to wrap then it must perform the book keeping and take the appropriate action.

Another sharp edge is, we did not have a reference implementation to work from. When we needed additional test vectors we hacked Bernstein's reference implementation of ChaCha. Our hacks included a wrap on the block counter without the carry, and it could be wrong. We hope the IETF will publish a reference implementation so we can use a program with a pedigree that will provide provenance for the additional test vectors.

Also see Issue 790, XChaCha20 results when counter block wraps and How to handle block counter wrap in IETF's ChaCha algorithm? on the CFRG mailing list.