XChaCha20 decryption issue.

133 views
Skip to first unread message

Lucas Marchetti

unread,
Nov 4, 2023, 2:39:31 PM11/4/23
to Crypto++ Users
Good evening.

I'm building a client-server application and I want to implement a XChaCha20 communication over TCP after performing key exchange.

What I'm issuing is a bad decryption output like the one shown in the pic.

Screenshot 2023-11-04 193625.png

I'm currently using crypto++ 8.9 in the client-side and https://pkg.go.dev/golang.org/x/crypto/chacha20 in the server-side.

Is that something related to sealing or authentication implemented in the Golang library?

Functions that I'm using:

Screenshot 2023-11-04 193751.png

Screenshot 2023-11-04 193832.png

Thanks in advance.

Lucas Marchetti

unread,
Nov 5, 2023, 4:10:44 AM11/5/23
to Crypto++ Users
I've just made a test encrypting the string "Hello World!" with both client and server functions and these are the results.

Screenshot 2023-11-05 100312.png

Both green-highlighted bytes corresponds to the input string but, as you can see, there is a different padding that I'm 100% sure is the source of the problem.

Lucas Marchetti

unread,
Nov 5, 2023, 4:38:45 AM11/5/23
to Crypto++ Users
Sorry, seeing now that I've linked the wrong Golang library, here it is: https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305

Jeffrey Walton

unread,
Nov 5, 2023, 2:26:27 PM11/5/23
to cryptop...@googlegroups.com
If you want help, then you should provide source code and post a link to a minimal reproducer. Pictures are not helpful.

The wiki is full of little working examples. For example, <https://www.cryptopp.com/wiki/XChaCha20> and <https://www.cryptopp.com/wiki/XChaCha20Poly1305>.

You should also probably start with test vectors, and then move onto arbitrary messages once things work with test vectors. Here are the ones Crypto++ uses for XChaCha: <https://github.com/weidai11/cryptopp/blob/master/TestVectors/chacha.txt>. And here are the ones for ChaCha20/Poly1305: <https://github.com/weidai11/cryptopp/blob/master/TestVectors/chacha20poly1305.txt#L4669>.

I'm just guessing, but the 16-bytes of garbage at the end of the [encrypted] message may be a Poly1305 authentication tag. But it is just a guess. The go documentation should tell you what you have.

Jeff
Message has been deleted
Message has been deleted

Lucas Marchetti

unread,
Nov 8, 2023, 10:25:07 AM11/8/23
to Crypto++ Users
Hi Jeff, thanks for the reply.

I've deleted the other messages cause I've found the solution by my own.

So, I've tested the whole thing with a test vector as you said and I found out, looking to Golang library's source code, that the output given to me was: Nonce + Ciphertext + MAC.
You were right: the last one was actually an authentication tag.

I decided to reimplement manually the Seal and Open functions removing the MAC generation and append.
I think I'm gonna implement it back in a bit: I prefer to go without authenticated encryption at the moment.

With a custom function I'm now able to remove the Nonce too and get the clear ciphertext that I can finally decrypt using Crypto++.

Here is the source code of the changed functions.

func sliceForAppend(in []byte, n int) (head, tail []byte) {

if total := len(in) + n; cap(in) >= total {

head = in[:total]

} else {

head = make([]byte, total)
copy(head, in)

}

tail = head[len(in):]
return

}


func sealGeneric(dst, nonce, plaintext []byte) []byte {

ret, out := sliceForAppend(dst, len(plaintext))
ciphertext, _ := out[:len(plaintext)], out[len(plaintext):]

var polyKey [32]byte
s, _ := chacha20.NewUnauthenticatedCipher(key[:], nonce)
s.XORKeyStream(polyKey[:], polyKey[:])
s.SetCounter(1) // set the counter to 1, skipping 32 bytes
s.XORKeyStream(ciphertext, plaintext)

return ret

}

func openGeneric(dst, nonce, ciphertext []byte) ([]byte, error) {

var polyKey [32]byte
s, _ := chacha20.NewUnauthenticatedCipher(key[:], nonce)
s.XORKeyStream(polyKey[:], polyKey[:])
s.SetCounter(1) // set the counter to 1, skipping 32 bytes

ret, out := sliceForAppend(dst, len(ciphertext))

s.XORKeyStream(out, ciphertext)

return ret, nil

}

That's how I call encryption and decryption:

func EncrytionWithChaChaPoly() {

msg := make([]byte, 0)
msg = append(msg,

0x4c,0x61,0x64,0x69,0x65,0x73,0x20,0x61,0x6e,0x64,0x20,0x47,0x65,0x6e,0x74,0x6c,
        0x65,0x6d,0x65,0x6e,0x20,0x6f,0x66,0x20,0x74,0x68,0x65,0x20,0x63,0x6c,0x61,0x73,
        0x73,0x20,0x6f,0x66,0x20,0x27,0x39,0x39,0x3a,0x20,0x49,0x66,0x20,0x49,0x20,0x63,
        0x6f,0x75,0x6c,0x64,0x20,0x6f,0x66,0x66,0x65,0x72,0x20,0x79,0x6f,0x75,0x20,0x6f,
        0x6e,0x6c,0x79,0x20,0x6f,0x6e,0x65,0x20,0x74,0x69,0x70,0x20,0x66,0x6f,0x72,0x20,
        0x74,0x68,0x65,0x20,0x66,0x75,0x74,0x75,0x72,0x65,0x2c,0x20,0x73,0x75,0x6e,0x73,
        0x63,0x72,0x65,0x65,0x6e,0x20,0x77,0x6f,0x75,0x6c,0x64,0x20,0x62,0x65,0x20,0x69,
        0x74,0x2e,

)

nonce := make([]byte, 0)
nonce = append(nonce, 0x07,0x00,0x00,0x00,0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x07,0x00,0x00,0x00,0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47)

fmt.Println("Nonce:")
fmt.Println(nonce)

// Encrypt the message and append the ciphertext to the nonce.
encryptedMsg = sealGeneric(nonce, nonce, msg)

fmt.Println("\nCipher:")
fmt.Println(encryptedMsg)
fmt.Println(len(encryptedMsg))

}

func DecryptionWithChaChaPoly(){

if len(encryptedMsg) < aead.NonceSize() {
panic("ciphertext too short")
}

// Split nonce and ciphertext.
nonce, ciphertext := encryptedMsg[:aead.NonceSize()], encryptedMsg[aead.NonceSize():]

// Decrypt the message and check it wasn't tampered with.
plaintext, err := openGeneric(nil, nonce, ciphertext)

if err != nil {
panic(err)
}

fmt.Println("\nRecover:")
fmt.Printf("%s\n", plaintext)

}


Tell me if I did something wrong.

Thanks in advance.

Lucas Marchetti

unread,
Nov 8, 2023, 10:33:11 AM11/8/23
to Crypto++ Users
And if you're interested about original functions here you are:


func writeWithPadding(p *poly1305.MAC, b []byte) {
p.Write(b)
if rem := len(b) % 16; rem != 0 {
var buf [16]byte
padLen := 16 - rem
p.Write(buf[:padLen])
}
}
func writeUint64(p *poly1305.MAC, n int) {
var buf [8]byte
binary.LittleEndian.PutUint64(buf[:], uint64(n))
p.Write(buf[:])
}
func (c *chacha20poly1305) sealGeneric(dst, nonce, plaintext, additionalData []byte) []byte {
ret, out := sliceForAppend(dst, len(plaintext)+poly1305.TagSize)
ciphertext, tag := out[:len(plaintext)], out[len(plaintext):]
if alias.InexactOverlap(out, plaintext) {
panic("chacha20poly1305: invalid buffer overlap")
}
var polyKey [32]byte
s, _ := chacha20.NewUnauthenticatedCipher(c.key[:], nonce)
s.XORKeyStream(polyKey[:], polyKey[:])
s.SetCounter(1) // set the counter to 1, skipping 32 bytes
s.XORKeyStream(ciphertext, plaintext)
p := poly1305.New(&polyKey)
writeWithPadding(p, additionalData)
writeWithPadding(p, ciphertext)
writeUint64(p, len(additionalData))
writeUint64(p, len(plaintext))
p.Sum(tag[:0])
return ret
}
func (c *chacha20poly1305) openGeneric(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
tag := ciphertext[len(ciphertext)-16:]
ciphertext = ciphertext[:len(ciphertext)-16]
var polyKey [32]byte
s, _ := chacha20.NewUnauthenticatedCipher(c.key[:], nonce)
s.XORKeyStream(polyKey[:], polyKey[:])
s.SetCounter(1) // set the counter to 1, skipping 32 bytes
p := poly1305.New(&polyKey)
writeWithPadding(p, additionalData)
writeWithPadding(p, ciphertext)
writeUint64(p, len(additionalData))
writeUint64(p, len(ciphertext))
ret, out := sliceForAppend(dst, len(ciphertext))
if alias.InexactOverlap(out, ciphertext) {
panic("chacha20poly1305: invalid buffer overlap")
}
if !p.Verify(tag) {
for i := range out {
out[i] = 0
}
return nil, errOpen
}
s.XORKeyStream(out, ciphertext)
return ret, nil
}


Reply all
Reply to author
Forward
0 new messages