Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 1 addition & 66 deletions pkg/twittermeow/crypto/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"encoding/base64"
"errors"
"fmt"
"math/big"
"time"
)

Expand Down Expand Up @@ -93,42 +92,7 @@
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)

Check failure on line 95 in pkg/twittermeow/crypto/keys.go

View workflow job for this annotation

GitHub Actions / Lint (old)

undefined: ecdsa.ParseRawPrivateKey (compile)

Check failure on line 95 in pkg/twittermeow/crypto/keys.go

View workflow job for this annotation

GitHub Actions / Lint (old)

undefined: ecdsa.ParseRawPrivateKey

Check failure on line 95 in pkg/twittermeow/crypto/keys.go

View workflow job for this annotation

GitHub Actions / Lint (old)

undefined: ecdsa.ParseRawPrivateKey (compile)

Check failure on line 95 in pkg/twittermeow/crypto/keys.go

View workflow job for this annotation

GitHub Actions / Lint (old)

undefined: ecdsa.ParseRawPrivateKey
}

// EncodePublicKeySPKI encodes an ECDSA public key to base64 SPKI format.
Expand All @@ -140,35 +104,6 @@
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)
Expand Down
69 changes: 26 additions & 43 deletions pkg/twittermeow/crypto/xchat.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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...)
Expand All @@ -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")
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
}
Loading