From c2dc965713fff92d0fbab3bffcc48f57180cf955 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 9 Jan 2026 21:24:54 +0200 Subject: [PATCH] Remove crypto functions that just duplicate the standard library --- pkg/twittermeow/crypto/keys.go | 67 +------------------------------- pkg/twittermeow/crypto/xchat.go | 69 +++++++++++++-------------------- 2 files changed, 27 insertions(+), 109 deletions(-) diff --git a/pkg/twittermeow/crypto/keys.go b/pkg/twittermeow/crypto/keys.go index e43d67b5..84cc3213 100644 --- a/pkg/twittermeow/crypto/keys.go +++ b/pkg/twittermeow/crypto/keys.go @@ -7,7 +7,6 @@ import ( "encoding/base64" "errors" "fmt" - "math/big" "time" ) @@ -93,42 +92,7 @@ func ParsePrivateKeyScalar(scalarB64 string) (*ecdsa.PrivateKey, error) { return nil, fmt.Errorf("decode scalar base64: %w", err) } - if len(scalar) != 32 { - return nil, fmt.Errorf("%w: scalar must be 32 bytes, got %d", ErrInvalidKeyFormat, len(scalar)) - } - - curve := elliptic.P256() - priv := new(ecdsa.PrivateKey) - priv.PublicKey.Curve = curve - priv.D = new(big.Int).SetBytes(scalar) - priv.PublicKey.X, priv.PublicKey.Y = curve.ScalarBaseMult(scalar) - - return priv, nil -} - -// ParsePublicKeyUncompressed parses a 65-byte uncompressed P-256 public key. -// Format: 0x04 || X (32 bytes) || Y (32 bytes) -func ParsePublicKeyUncompressed(data []byte) (*ecdsa.PublicKey, error) { - if len(data) != 65 { - return nil, fmt.Errorf("%w: expected 65 bytes, got %d", ErrInvalidKeyFormat, len(data)) - } - if data[0] != 0x04 { - return nil, fmt.Errorf("%w: expected uncompressed point (0x04 prefix)", ErrInvalidKeyFormat) - } - - curve := elliptic.P256() - x := new(big.Int).SetBytes(data[1:33]) - y := new(big.Int).SetBytes(data[33:65]) - - if !curve.IsOnCurve(x, y) { - return nil, fmt.Errorf("%w: point is not on P-256 curve", ErrInvalidKeyFormat) - } - - return &ecdsa.PublicKey{ - Curve: curve, - X: x, - Y: y, - }, nil + return ecdsa.ParseRawPrivateKey(elliptic.P256(), scalar) } // EncodePublicKeySPKI encodes an ECDSA public key to base64 SPKI format. @@ -140,35 +104,6 @@ func EncodePublicKeySPKI(pub *ecdsa.PublicKey) (string, error) { return base64.StdEncoding.EncodeToString(der), nil } -// EncodePrivateKeyScalar encodes the private scalar as base64. -// The result is a 32-byte scalar in big-endian format, base64-encoded. -func EncodePrivateKeyScalar(priv *ecdsa.PrivateKey) string { - scalar := priv.D.Bytes() - // Pad to 32 bytes if necessary - if len(scalar) < 32 { - padded := make([]byte, 32) - copy(padded[32-len(scalar):], scalar) - scalar = padded - } - return base64.StdEncoding.EncodeToString(scalar) -} - -// EncodePublicKeyUncompressed encodes an ECDSA public key to uncompressed format. -// Returns: 0x04 || X (32 bytes) || Y (32 bytes) = 65 bytes -func EncodePublicKeyUncompressed(pub *ecdsa.PublicKey) []byte { - if pub == nil || pub.Curve == nil || pub.X == nil || pub.Y == nil { - return nil - } - byteLen := (pub.Curve.Params().BitSize + 7) / 8 - out := make([]byte, 1+2*byteLen) - out[0] = 0x04 - xBytes := pub.X.Bytes() - yBytes := pub.Y.Bytes() - copy(out[1+byteLen-len(xBytes):1+byteLen], xBytes) - copy(out[1+2*byteLen-len(yBytes):], yBytes) - return out -} - // LoadSigningKeyPair creates a SigningKeyPair from stored base64 scalar values. // signingKeyB64: the signing key for signing messages (base64 32-byte scalar) // decryptKeyB64: the decryption key for decrypting conversation keys (base64 32-byte scalar) diff --git a/pkg/twittermeow/crypto/xchat.go b/pkg/twittermeow/crypto/xchat.go index c7777505..df71c234 100644 --- a/pkg/twittermeow/crypto/xchat.go +++ b/pkg/twittermeow/crypto/xchat.go @@ -3,13 +3,14 @@ package crypto import ( "crypto/aes" "crypto/cipher" - "crypto/rand" + "crypto/ecdh" "crypto/sha256" "encoding/base64" "errors" "fmt" "strings" + "go.mau.fi/util/random" "golang.org/x/crypto/nacl/secretbox" ) @@ -24,14 +25,10 @@ func SecretboxEncrypt(plaintext, key []byte) ([]byte, error) { if len(key) != secretboxKeySize { return nil, fmt.Errorf("secretbox key must be %d bytes", secretboxKeySize) } - var nonce [secretboxNonceSize]byte - if _, err := rand.Read(nonce[:]); err != nil { - return nil, fmt.Errorf("generate nonce: %w", err) - } - var k [secretboxKeySize]byte - copy(k[:], key) + nonce := (*[secretboxNonceSize]byte)(random.Bytes(secretboxNonceSize)) + k := (*[secretboxKeySize]byte)(key) - ct := secretbox.Seal(nil, plaintext, &nonce, &k) + ct := secretbox.Seal(nil, plaintext, nonce, k) out := make([]byte, 0, len(nonce)+len(ct)) out = append(out, nonce[:]...) out = append(out, ct...) @@ -47,14 +44,11 @@ func SecretboxDecrypt(nonceCiphertext, key []byte) ([]byte, error) { if len(nonceCiphertext) < secretboxNonceSize+secretbox.Overhead { return nil, errors.New("secretbox payload too short") } - var nonce [secretboxNonceSize]byte - copy(nonce[:], nonceCiphertext[:secretboxNonceSize]) + nonce := (*[secretboxNonceSize]byte)(nonceCiphertext[:secretboxNonceSize]) ciphertext := nonceCiphertext[secretboxNonceSize:] + k := (*[secretboxKeySize]byte)(key) - var k [secretboxKeySize]byte - copy(k[:], key) - - plaintext, ok := secretbox.Open(nil, ciphertext, &nonce, &k) + plaintext, ok := secretbox.Open(nil, ciphertext, nonce, k) if !ok { return nil, errors.New("secretbox decrypt failed") } @@ -87,18 +81,17 @@ func UnwrapConversationKey(keyB64, privScalarB64 string) ([]byte, error) { return nil, fmt.Errorf("private scalar must be 32 bytes, got %d", len(privScalar)) } - ephKey, err := ParsePublicKeyUncompressed(ephPub) + ephKey, err := ecdh.P256().NewPublicKey(ephPub) if err != nil { return nil, fmt.Errorf("invalid ephemeral public key: %w", err) } - - // Derive shared secret (x coordinate, 32 bytes big-endian). - sx, _ := ephKey.Curve.ScalarMult(ephKey.X, ephKey.Y, privScalar) - shared := sx.Bytes() - if len(shared) < 32 { - padded := make([]byte, 32) - copy(padded[32-len(shared):], shared) - shared = padded + ecdhPriv, err := ecdh.P256().NewPrivateKey(privScalar) + if err != nil { + return nil, fmt.Errorf("failed to create ecdh private key: %w", err) + } + shared, err := ecdhPriv.ECDH(ephKey) + if err != nil { + return nil, fmt.Errorf("ecdh: %w", err) } kdfOut, err := kdf2SHA256(shared, ephPub, 32) @@ -155,27 +148,17 @@ func kdf2SHA256(shared, other []byte, length int) ([]byte, error) { return out[:length], nil } +var base64Cleaner = strings.NewReplacer( + "\n", "", + "\r", "", + " ", "", + "=", "", + "-", "+", + "_", "/", +) + // decodeBase64Flexible trims whitespace and tries standard and URL-safe base64 // decodings (with and without padding). func decodeBase64Flexible(s string) ([]byte, error) { - clean := strings.TrimSpace(s) - clean = strings.ReplaceAll(clean, "\n", "") - clean = strings.ReplaceAll(clean, "\r", "") - clean = strings.ReplaceAll(clean, " ", "") - - encodings := []*base64.Encoding{ - base64.StdEncoding, - base64.URLEncoding, - base64.RawStdEncoding, - base64.RawURLEncoding, - } - var lastErr error - for _, enc := range encodings { - if dec, err := enc.DecodeString(clean); err == nil { - return dec, nil - } else { - lastErr = err - } - } - return nil, lastErr + return base64.RawStdEncoding.DecodeString(base64Cleaner.Replace(s)) }