I am able to successfully encrypt and decrypt a string using Chacha20-Poly1305 in python without using the tag (or mac) as follows (using pycryptodome library):
from Crypto.Cipher import ChaCha20_Poly1305
key = '20d821e770a6d3e4fc171fd3a437c7841d58463cb1bc7f7cce6b4225ae1dd900' #random generated key
nonce = '18c02beda4f8b22aa782444a' #random generated nonce
def decode(ciphertext):
cipher = ChaCha20_Poly1305.new(key=bytes.fromhex(key), nonce=bytes.fromhex(nonce))
plaintext = cipher.decrypt(bytes.fromhex(ciphertext))
return plaintext.decode('utf-8')
def encode(plaintext):
cipher = ChaCha20_Poly1305.new(key=bytes.fromhex(key), nonce=bytes.fromhex(nonce))
ciphertext = cipher.encrypt(plaintext.encode('utf-8'))
return ciphertext.hex()
encrypted = encode('abcdefg123')
print(encrypted)
# will print: ab6cf9f9e0cf73833194
print(decode(encrypted))
# will print: abcdefg123
However, taking this to Javascript, I cannot find a library that will decrypt this without requiring the mac (that I would have gotten if I had used encrypt_and_digest()).
I tried virtually every library I could find (Npm, as this would be used in a React application), they all require the mac part for the decryption. for example: libsodium-wrappers, js-chacha20 etc.
How can I overcome this?
P.S. I know it is less safe to not use the mac part, this is for educational purposes.
=== EDIT ===
The string was encrypted with Chacha20-Poly1305 and its encrypted output is already given. It cannot be re-encrypted using a different algorithm.
I cannot encrypt using Chacha20-Poly1305 and decrypt with Chacha20 or vice versa because encrypting with the same key and nonce using only Chacha20 (rather than Chacha20-Poly1305) gives me a different encrypted output and this it is not helping.
The issue is caused by different values for the initial counter regarding encryption/decryption in the libraries applied.
Background: ChaCha20 is operated in counter mode to derive a key stream that is XORed with the plaintext. Thereby an (increment-by-one) counter counts through a sequence of input blocks for the ChaCha20 block function. The initial counter is the start value of that counter. For more details on ChaCha20, see here and RFC 8439.
A similar situation regarding different initial counters can be found in the PyCryptodome library itself:
With ChaCha20-Poly1305, the counter 0 is used to determine the authentication tag, so the counter 1 is the first value applied for encryption, s. here.
PyCryptodome follows this logic within the
ChaCha20_Poly1305
implementation forencrypt()
as well, to ensure thatencrypt()
anddigest()
give the same result asencrypt_and_digest()
.However, within the
ChaCha20
implementation, the initial counter for encryption is 0 by default (and not 1).Therefore, to allow decryption of the ciphertext generated with
ChaCha20_Poly1305#encrypt()
usingChaCha205#decrypt()
, the counter must be explicitly set to 1 for the latter.This can be achieved with the
seek()
method (note thatseek()
requires the position in bytes; since a ChaCha20 block is 64 bytes in size, the counter value 1 corresponds to byte index 64 in the key stream):The same applies to js-chacha20, a ChaCha20 implementation for JavaScript. By default, 0 is applied as the initial counter for encryption, s. here. Thus, to be compatible with ciphertexts created with PyCryptodome's
ChaCha20_Poly1305#encrypt()
, the initial counter must be explicitly set to 1:With this change, the ciphertext is decrypted correctly.
For completeness: The ChaCha20 specification allows any value for the initial counter and explicitly lists the values 1 (e.g. in the context of an AEAD algorithm) and 0 as the usual values.
In chapter 2.4. The ChaCha20 Encryption Algorithm of RFC 8439, ChaCha20 and Poly1305 for IETF Protocols it states regarding the counter: