telegabber/e2ee/omemo/libsignal/crypto.go
2026-07-28 00:38:46 -04:00

205 lines
6.4 KiB
Go

package libsignal
/*
#include <signal_protocol.h>
*/
import "C"
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
rand2 "crypto/rand"
"crypto/sha256"
"crypto/sha512"
"errors"
"hash"
"runtime/cgo"
"unsafe"
)
// libomemo-c ships no default signal_crypto_provider (confirmed by reading
// signal_protocol.h - only test-only OpenSSL/CommonCrypto implementations
// exist, never installed). This file implements the five callback groups
// documented there - random, HMAC-SHA256, SHA-512, and AES encrypt/decrypt
// in the two modes the library actually asks for (CTR-nopadding and
// CBC-PKCS5; no GCM) - entirely against Go's standard library.
//
// hmac_context/digest_context are opaque per-operation state the library
// holds between init/update/final/cleanup calls. Since C can't safely hold
// a long-lived Go pointer, each is a runtime/cgo.Handle round-tripped
// through a uintptr, exactly like the store callbacks in callbacks.go.
//export go_crypto_random
func go_crypto_random(data *C.uint8_t, length C.size_t, userData unsafe.Pointer) C.int {
buf := unsafe.Slice((*byte)(unsafe.Pointer(data)), int(length))
if _, err := rand2.Read(buf); err != nil {
return C.int(ErrUnknown)
}
return C.SG_SUCCESS
}
type hmacState struct {
h hash.Hash
}
//export go_crypto_hmac_sha256_init
func go_crypto_hmac_sha256_init(hmacContext *unsafe.Pointer, key *C.uint8_t, keyLen C.size_t, userData unsafe.Pointer) C.int {
keyBytes := cBytesToGo(key, keyLen)
handle := cgo.NewHandle(&hmacState{h: hmac.New(sha256.New, keyBytes)})
// go vet flags this as "possible misuse of unsafe.Pointer" - expected: a
// cgo.Handle is an opaque uintptr token, never dereferenced as a real
// address, and signal_protocol.h's callback shape requires void* here.
*hmacContext = unsafe.Pointer(uintptr(handle))
return C.SG_SUCCESS
}
//export go_crypto_hmac_sha256_update
func go_crypto_hmac_sha256_update(hmacContext unsafe.Pointer, data *C.uint8_t, dataLen C.size_t, userData unsafe.Pointer) C.int {
st, ok := cgo.Handle(uintptr(hmacContext)).Value().(*hmacState)
if !ok {
return C.int(ErrUnknown)
}
st.h.Write(cBytesToGo(data, dataLen))
return C.SG_SUCCESS
}
//export go_crypto_hmac_sha256_final
func go_crypto_hmac_sha256_final(hmacContext unsafe.Pointer, output **C.signal_buffer, userData unsafe.Pointer) C.int {
st, ok := cgo.Handle(uintptr(hmacContext)).Value().(*hmacState)
if !ok {
return C.int(ErrUnknown)
}
*output = bytesToBuffer(st.h.Sum(nil))
st.h.Reset()
return C.SG_SUCCESS
}
//export go_crypto_hmac_sha256_cleanup
func go_crypto_hmac_sha256_cleanup(hmacContext unsafe.Pointer, userData unsafe.Pointer) {
cgo.Handle(uintptr(hmacContext)).Delete()
}
type digestState struct {
h hash.Hash
}
//export go_crypto_sha512_init
func go_crypto_sha512_init(digestContext *unsafe.Pointer, userData unsafe.Pointer) C.int {
handle := cgo.NewHandle(&digestState{h: sha512.New()})
// See the identical vet note in go_crypto_hmac_sha256_init above.
*digestContext = unsafe.Pointer(uintptr(handle))
return C.SG_SUCCESS
}
//export go_crypto_sha512_update
func go_crypto_sha512_update(digestContext unsafe.Pointer, data *C.uint8_t, dataLen C.size_t, userData unsafe.Pointer) C.int {
st, ok := cgo.Handle(uintptr(digestContext)).Value().(*digestState)
if !ok {
return C.int(ErrUnknown)
}
st.h.Write(cBytesToGo(data, dataLen))
return C.SG_SUCCESS
}
//export go_crypto_sha512_final
func go_crypto_sha512_final(digestContext unsafe.Pointer, output **C.signal_buffer, userData unsafe.Pointer) C.int {
st, ok := cgo.Handle(uintptr(digestContext)).Value().(*digestState)
if !ok {
return C.int(ErrUnknown)
}
*output = bytesToBuffer(st.h.Sum(nil))
st.h.Reset()
return C.SG_SUCCESS
}
//export go_crypto_sha512_cleanup
func go_crypto_sha512_cleanup(digestContext unsafe.Pointer, userData unsafe.Pointer) {
cgo.Handle(uintptr(digestContext)).Delete()
}
//export go_crypto_encrypt
func go_crypto_encrypt(output **C.signal_buffer, cipherMode C.int, key *C.uint8_t, keyLen C.size_t, iv *C.uint8_t, ivLen C.size_t, plaintext *C.uint8_t, plaintextLen C.size_t, userData unsafe.Pointer) C.int {
block, err := aes.NewCipher(cBytesToGo(key, keyLen))
if err != nil {
return C.int(ErrUnknown)
}
ivBytes := cBytesToGo(iv, ivLen)
plaintextBytes := cBytesToGo(plaintext, plaintextLen)
var out []byte
switch cipherMode {
case C.SG_CIPHER_AES_CTR_NOPADDING:
out = make([]byte, len(plaintextBytes))
cipher.NewCTR(block, ivBytes).XORKeyStream(out, plaintextBytes)
case C.SG_CIPHER_AES_CBC_PKCS5:
padded := pkcs7Pad(plaintextBytes, aes.BlockSize)
out = make([]byte, len(padded))
cipher.NewCBCEncrypter(block, ivBytes).CryptBlocks(out, padded)
default:
return C.int(ErrInvalidArgument)
}
*output = bytesToBuffer(out)
return C.SG_SUCCESS
}
//export go_crypto_decrypt
func go_crypto_decrypt(output **C.signal_buffer, cipherMode C.int, key *C.uint8_t, keyLen C.size_t, iv *C.uint8_t, ivLen C.size_t, ciphertext *C.uint8_t, ciphertextLen C.size_t, userData unsafe.Pointer) C.int {
block, err := aes.NewCipher(cBytesToGo(key, keyLen))
if err != nil {
return C.int(ErrUnknown)
}
ivBytes := cBytesToGo(iv, ivLen)
ciphertextBytes := cBytesToGo(ciphertext, ciphertextLen)
var out []byte
switch cipherMode {
case C.SG_CIPHER_AES_CTR_NOPADDING:
out = make([]byte, len(ciphertextBytes))
cipher.NewCTR(block, ivBytes).XORKeyStream(out, ciphertextBytes)
case C.SG_CIPHER_AES_CBC_PKCS5:
if len(ciphertextBytes) == 0 || len(ciphertextBytes)%aes.BlockSize != 0 {
return C.int(ErrInvalidMessage)
}
decrypted := make([]byte, len(ciphertextBytes))
cipher.NewCBCDecrypter(block, ivBytes).CryptBlocks(decrypted, ciphertextBytes)
unpadded, err := pkcs7Unpad(decrypted)
if err != nil {
return C.int(ErrInvalidMessage)
}
out = unpadded
default:
return C.int(ErrInvalidArgument)
}
*output = bytesToBuffer(out)
return C.SG_SUCCESS
}
func pkcs7Pad(data []byte, blockSize int) []byte {
padLen := blockSize - (len(data) % blockSize)
padded := make([]byte, len(data)+padLen)
copy(padded, data)
for i := len(data); i < len(padded); i++ {
padded[i] = byte(padLen)
}
return padded
}
func pkcs7Unpad(data []byte) ([]byte, error) {
n := len(data)
if n == 0 {
return nil, errors.New("libsignal: cannot unpad empty data")
}
padLen := int(data[n-1])
if padLen == 0 || padLen > n || padLen > aes.BlockSize {
return nil, errors.New("libsignal: invalid PKCS7 padding")
}
for _, b := range data[n-padLen:] {
if int(b) != padLen {
return nil, errors.New("libsignal: invalid PKCS7 padding")
}
}
return data[:n-padLen], nil
}