首次推送
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"crypto/sha512"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidIdentityFormat = errors.New("invalid identity format")
|
||||
errRSAChallengeOutRange = errors.New("RSA challenge level out of range")
|
||||
errInvalidPublicPoint = errors.New("invalid public key point encoding")
|
||||
errSharedSecretCompute = errors.New("failed to compute ECDH shared secret")
|
||||
)
|
||||
|
||||
const (
|
||||
decimalBase = 10
|
||||
identityPartCount = 2
|
||||
p256ScalarSize = 32
|
||||
p256PointPrefix = 0x04
|
||||
p256UncompressedKeySize = 65
|
||||
rsaChallengeBlockSize = 64
|
||||
maxRSAChallengeLevel = 1000000
|
||||
packetTypeMask = 0x0F
|
||||
generationIDShift = 32
|
||||
fromServerShift = 40
|
||||
fakeSignatureSize = 8
|
||||
ivAlphaSize = 10
|
||||
sha1NumBufSize = 20
|
||||
bitsPerByte = 8
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
PrivateKey *ecdsa.PrivateKey
|
||||
Offset uint64
|
||||
}
|
||||
|
||||
func (id *Identity) PublicKeyBase64() string {
|
||||
pubBytes, err := id.PrivateKey.PublicKey.Bytes()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if len(pubBytes) != p256UncompressedKeySize || pubBytes[0] != p256PointPrefix {
|
||||
return ""
|
||||
}
|
||||
|
||||
x := new(big.Int).SetBytes(pubBytes[1 : 1+p256ScalarSize])
|
||||
y := new(big.Int).SetBytes(pubBytes[1+p256ScalarSize : p256UncompressedKeySize])
|
||||
|
||||
data := struct {
|
||||
BitInfo asn1.BitString
|
||||
Size int
|
||||
X *big.Int
|
||||
Y *big.Int
|
||||
}{
|
||||
BitInfo: asn1.BitString{Bytes: []byte{0x00}, BitLength: 1},
|
||||
Size: p256ScalarSize,
|
||||
X: x,
|
||||
Y: y,
|
||||
}
|
||||
bytes, _ := asn1.Marshal(data)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func (id *Identity) String() string {
|
||||
d, err := id.PrivateKey.Bytes()
|
||||
if err != nil {
|
||||
// Keep String side-effect free; invalid key should not crash callers.
|
||||
return fmt.Sprintf(":%d", id.Offset)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%d", base64.StdEncoding.EncodeToString(d), id.Offset)
|
||||
}
|
||||
|
||||
func IdentityFromString(s string) (*Identity, error) {
|
||||
parts := strings.Split(s, ":")
|
||||
if len(parts) != identityPartCount {
|
||||
return nil, errInvalidIdentityFormat
|
||||
}
|
||||
dBytes, err := base64.StdEncoding.DecodeString(parts[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset, err := strconv.ParseUint(parts[1], decimalBase, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
priv, err := ecdsa.ParseRawPrivateKey(elliptic.P256(), dBytes)
|
||||
if err != nil {
|
||||
// Backward compatibility: historical identity strings might store
|
||||
// non-padded scalars; normalize to SEC 1 fixed-size raw key.
|
||||
if len(dBytes) >= p256ScalarSize {
|
||||
return nil, err
|
||||
}
|
||||
padded := make([]byte, p256ScalarSize)
|
||||
copy(padded[p256ScalarSize-len(dBytes):], dBytes)
|
||||
priv, err = ecdsa.ParseRawPrivateKey(elliptic.P256(), padded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &Identity{PrivateKey: priv, Offset: offset}, nil
|
||||
}
|
||||
|
||||
func GetUidFromPublicKey(publicKey string) string {
|
||||
sum := sha1.Sum([]byte(publicKey))
|
||||
|
||||
return base64.StdEncoding.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
type Crypt struct {
|
||||
Identity *Identity
|
||||
CachedKeys map[uint64]KeyNonce
|
||||
IvStruct []byte
|
||||
FakeSignature []byte
|
||||
AlphaTmp []byte
|
||||
keyMu sync.Mutex
|
||||
CryptoInitComplete bool
|
||||
}
|
||||
|
||||
type KeyNonce struct {
|
||||
Key []byte
|
||||
Nonce []byte
|
||||
Gen uint32
|
||||
}
|
||||
|
||||
// makeCacheKey packs (fromServer, packetType, generationID) into a map key without allocating.
|
||||
func makeCacheKey(fromServer bool, packetType byte, generationID uint32) uint64 {
|
||||
var key uint64
|
||||
if fromServer {
|
||||
key = 1 << fromServerShift
|
||||
}
|
||||
key |= uint64(packetType&packetTypeMask) << generationIDShift
|
||||
key |= uint64(generationID)
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func NewCrypt(id *Identity) *Crypt {
|
||||
return &Crypt{
|
||||
Identity: id,
|
||||
FakeSignature: make([]byte, fakeSignatureSize),
|
||||
CachedKeys: make(map[uint64]KeyNonce),
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *Crypt) SolveRsaChallenge(data []byte, offset int, level int) ([]byte, error) {
|
||||
if level < 0 || level > maxRSAChallengeLevel {
|
||||
return nil, errRSAChallengeOutRange
|
||||
}
|
||||
x := new(big.Int).SetBytes(data[offset : offset+rsaChallengeBlockSize])
|
||||
n := new(big.Int).SetBytes(data[offset+rsaChallengeBlockSize : offset+2*rsaChallengeBlockSize])
|
||||
|
||||
// y = x^(2^level) mod n via repeated squaring.
|
||||
y := new(big.Int).Set(x)
|
||||
for range level {
|
||||
y.Mul(y, y)
|
||||
y.Mod(y, n)
|
||||
}
|
||||
|
||||
res := y.Bytes()
|
||||
if len(res) < rsaChallengeBlockSize {
|
||||
aligned := make([]byte, rsaChallengeBlockSize)
|
||||
copy(aligned[rsaChallengeBlockSize-len(res):], res)
|
||||
res = aligned
|
||||
} else if len(res) > rsaChallengeBlockSize {
|
||||
res = res[len(res)-rsaChallengeBlockSize:]
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (tc *Crypt) InitCrypto(alpha, beta, omega string) error {
|
||||
alphaBytes, err := base64.StdEncoding.DecodeString(alpha)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid alpha: %w", err)
|
||||
}
|
||||
betaBytes, err := base64.StdEncoding.DecodeString(beta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid beta: %w", err)
|
||||
}
|
||||
omegaBytes, err := base64.StdEncoding.DecodeString(omega)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid omega: %w", err)
|
||||
}
|
||||
serverPubKey, err := ImportPublicKey(omegaBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sharedSecret := tc.getSharedSecret(serverPubKey)
|
||||
if len(sharedSecret) == 0 {
|
||||
return errSharedSecretCompute
|
||||
}
|
||||
|
||||
return tc.SetSharedSecret(alphaBytes, betaBytes, sharedSecret)
|
||||
}
|
||||
|
||||
func (tc *Crypt) SetSharedSecret(alpha, beta, sharedKey []byte) error {
|
||||
tc.IvStruct = make([]byte, ivAlphaSize+len(beta))
|
||||
for i := range alpha {
|
||||
tc.IvStruct[i] = sharedKey[i] ^ alpha[i]
|
||||
}
|
||||
for i := range beta {
|
||||
tc.IvStruct[ivAlphaSize+i] = sharedKey[ivAlphaSize+i] ^ beta[i]
|
||||
}
|
||||
|
||||
h := sha1.New()
|
||||
h.Write(tc.IvStruct)
|
||||
copy(tc.FakeSignature, h.Sum(nil)[:fakeSignatureSize])
|
||||
|
||||
tc.CryptoInitComplete = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *Crypt) DebugCryptoState() (int, string) {
|
||||
if len(tc.IvStruct) == 0 {
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
return len(tc.IvStruct), hex.EncodeToString(tc.FakeSignature)
|
||||
}
|
||||
|
||||
func (tc *Crypt) getSharedSecret(pub *ecdsa.PublicKey) []byte {
|
||||
privECDH, err := tc.Identity.PrivateKey.ECDH()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
pubECDH, err := pub.ECDH()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
keyArr, err := privECDH.ECDH(pubECDH)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if len(keyArr) > p256ScalarSize {
|
||||
keyArr = keyArr[len(keyArr)-p256ScalarSize:]
|
||||
} else if len(keyArr) < p256ScalarSize {
|
||||
aligned := make([]byte, p256ScalarSize)
|
||||
copy(aligned[p256ScalarSize-len(keyArr):], keyArr)
|
||||
keyArr = aligned
|
||||
}
|
||||
h := sha1.New()
|
||||
h.Write(keyArr)
|
||||
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func Hash512(data []byte) []byte {
|
||||
sum := sha512.Sum512(data)
|
||||
|
||||
return sum[:]
|
||||
}
|
||||
|
||||
func ImportPublicKey(data []byte) (*ecdsa.PublicKey, error) {
|
||||
// Canonical format (TS5/TS6): {BitString, Size, X, Y}
|
||||
var canonical struct {
|
||||
BitInfo asn1.BitString
|
||||
Size int
|
||||
X *big.Int
|
||||
Y *big.Int
|
||||
}
|
||||
_, canonicalErr := asn1.Unmarshal(data, &canonical)
|
||||
if canonicalErr == nil {
|
||||
encoded, err := encodeUncompressedP256Point(canonical.X, canonical.Y)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ecdsa.ParseUncompressedPublicKey(elliptic.P256(), encoded)
|
||||
}
|
||||
|
||||
// Legacy format (TeamSpeak): {X, Y, BitString, Size}
|
||||
var legacy struct {
|
||||
X *big.Int
|
||||
Y *big.Int
|
||||
BitInfo asn1.BitString
|
||||
Size int
|
||||
}
|
||||
_, err := asn1.Unmarshal(data, &legacy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encoded, err := encodeUncompressedP256Point(legacy.X, legacy.Y)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ecdsa.ParseUncompressedPublicKey(elliptic.P256(), encoded)
|
||||
}
|
||||
|
||||
func encodeUncompressedP256Point(x, y *big.Int) ([]byte, error) {
|
||||
if x == nil || y == nil {
|
||||
return nil, errInvalidPublicPoint
|
||||
}
|
||||
xBytes := x.Bytes()
|
||||
yBytes := y.Bytes()
|
||||
const fieldSize = 32
|
||||
if len(xBytes) > fieldSize || len(yBytes) > fieldSize {
|
||||
return nil, errInvalidPublicPoint
|
||||
}
|
||||
|
||||
point := make([]byte, 1+fieldSize+fieldSize)
|
||||
point[0] = p256PointPrefix
|
||||
copy(point[1+fieldSize-len(xBytes):1+fieldSize], xBytes)
|
||||
copy(point[1+2*fieldSize-len(yBytes):], yBytes)
|
||||
|
||||
return point, nil
|
||||
}
|
||||
|
||||
func (id *Identity) SecurityLevel() int {
|
||||
h := sha1.New()
|
||||
h.Write([]byte(id.PublicKeyBase64()))
|
||||
var numBuf [sha1NumBufSize]byte
|
||||
h.Write(strconv.AppendUint(numBuf[:0], id.Offset, decimalBase))
|
||||
|
||||
return countLeadingZeros(h.Sum(nil))
|
||||
}
|
||||
|
||||
// UpgradeToLevel increments Offset until SecurityLevel reaches targetLevel.
|
||||
func (id *Identity) UpgradeToLevel(targetLevel int, ctx context.Context) error {
|
||||
prefix := []byte(id.PublicKeyBase64())
|
||||
h := sha1.New()
|
||||
var numBuf [sha1NumBufSize]byte
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
h.Reset()
|
||||
h.Write(prefix)
|
||||
h.Write(strconv.AppendUint(numBuf[:0], id.Offset, decimalBase))
|
||||
if countLeadingZeros(h.Sum(nil)) >= targetLevel {
|
||||
return nil
|
||||
}
|
||||
id.Offset++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateIdentity(targetLevel int) (*Identity, error) {
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id := &Identity{PrivateKey: priv}
|
||||
|
||||
prefix := []byte(id.PublicKeyBase64())
|
||||
h := sha1.New()
|
||||
var numBuf [sha1NumBufSize]byte
|
||||
for {
|
||||
h.Reset()
|
||||
h.Write(prefix)
|
||||
h.Write(strconv.AppendUint(numBuf[:0], id.Offset, decimalBase))
|
||||
if countLeadingZeros(h.Sum(nil)) >= targetLevel {
|
||||
return id, nil
|
||||
}
|
||||
id.Offset++
|
||||
}
|
||||
}
|
||||
|
||||
func countLeadingZeros(data []byte) int {
|
||||
zeros := 0
|
||||
for _, b := range data {
|
||||
if b == 0 {
|
||||
zeros += bitsPerByte
|
||||
} else {
|
||||
// Security level counts trailing zero bits in SHA1(prefix||offset), LSB-first.
|
||||
for i := range bitsPerByte {
|
||||
if (b & (1 << uint(i))) == 0 {
|
||||
zeros++
|
||||
} else {
|
||||
return zeros
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return zeros
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
keySizeBytes = 16
|
||||
init1PacketType = 8
|
||||
hashInputMetaSize = 6
|
||||
clientSaltByte = 0x31
|
||||
serverSaltByte = 0x30
|
||||
)
|
||||
|
||||
// keyPool reuses 16-byte AES key buffers for packet crypto.
|
||||
var keyPool = sync.Pool{
|
||||
New: func() any {
|
||||
buf := make([]byte, keySizeBytes)
|
||||
|
||||
return &buf
|
||||
},
|
||||
}
|
||||
|
||||
// AcquireKeyBuffer returns a 16-byte buffer from keyPool or allocates one.
|
||||
func AcquireKeyBuffer() []byte {
|
||||
bufPtr, ok := keyPool.Get().(*[]byte)
|
||||
if !ok || bufPtr == nil {
|
||||
return make([]byte, keySizeBytes)
|
||||
}
|
||||
|
||||
return *bufPtr
|
||||
}
|
||||
|
||||
// ReleaseKeyBuffer returns buf to keyPool only if len(buf) is the AES key size.
|
||||
func ReleaseKeyBuffer(buf []byte) {
|
||||
if len(buf) == keySizeBytes {
|
||||
keyPool.Put(&buf)
|
||||
}
|
||||
}
|
||||
|
||||
// Precomputed dummy key/nonce matching the TS3 client pre-crypto placeholder.
|
||||
var (
|
||||
dummyKey = []byte("c:\\windows\\syste")
|
||||
dummyNonce = []byte("m\\firewall32.cpl")
|
||||
)
|
||||
|
||||
func (tc *Crypt) GetKeyNonce(
|
||||
fromServer bool,
|
||||
packetID uint16,
|
||||
generationID uint32,
|
||||
packetType byte,
|
||||
dummy bool,
|
||||
) ([]byte, []byte) {
|
||||
if dummy {
|
||||
key := AcquireKeyBuffer()
|
||||
copy(key, dummyKey)
|
||||
|
||||
return key, dummyNonce
|
||||
}
|
||||
|
||||
cacheKey := makeCacheKey(fromServer, packetType, generationID)
|
||||
|
||||
tc.keyMu.Lock()
|
||||
kn, ok := tc.CachedKeys[cacheKey]
|
||||
if !ok {
|
||||
tmpToHash := make([]byte, hashInputMetaSize+len(tc.IvStruct))
|
||||
if fromServer {
|
||||
tmpToHash[0] = serverSaltByte
|
||||
} else {
|
||||
tmpToHash[0] = clientSaltByte
|
||||
}
|
||||
tmpToHash[1] = packetType & packetTypeMask
|
||||
binary.BigEndian.PutUint32(tmpToHash[2:6], generationID)
|
||||
copy(tmpToHash[6:], tc.IvStruct)
|
||||
|
||||
hash := sha256.Sum256(tmpToHash)
|
||||
kn = KeyNonce{
|
||||
Key: append([]byte(nil), hash[0:keySizeBytes]...),
|
||||
Nonce: append([]byte(nil), hash[keySizeBytes:2*keySizeBytes]...),
|
||||
Gen: generationID,
|
||||
}
|
||||
tc.CachedKeys[cacheKey] = kn
|
||||
}
|
||||
tc.keyMu.Unlock()
|
||||
|
||||
key := AcquireKeyBuffer()
|
||||
copy(key, kn.Key)
|
||||
var packetIDBytes [2]byte
|
||||
binary.BigEndian.PutUint16(packetIDBytes[:], packetID)
|
||||
key[0] ^= packetIDBytes[0]
|
||||
key[1] ^= packetIDBytes[1]
|
||||
|
||||
return key, kn.Nonce
|
||||
}
|
||||
|
||||
var init1MAC = []byte("TS3INIT1")
|
||||
|
||||
var ErrFakeSignatureMismatch = errors.New("fake signature mismatch")
|
||||
|
||||
// Encrypt returns (ciphertext, MAC, err). Init1 and unencrypted packet types bypass EAX.
|
||||
func (tc *Crypt) Encrypt(
|
||||
packetType byte,
|
||||
packetID uint16,
|
||||
generationID uint32,
|
||||
header, plaintext []byte,
|
||||
dummy bool,
|
||||
unencrypted bool,
|
||||
) ([]byte, []byte, error) {
|
||||
if packetType == init1PacketType {
|
||||
return plaintext, init1MAC, nil
|
||||
}
|
||||
|
||||
if unencrypted {
|
||||
return plaintext, tc.FakeSignature, nil
|
||||
}
|
||||
|
||||
key, nonce := tc.GetKeyNonce(false, packetID, generationID, packetType, dummy)
|
||||
defer ReleaseKeyBuffer(key)
|
||||
|
||||
eax, err := NewEAX(key)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ciphertext, mac, err := eax.Encrypt(nonce, header, plaintext)
|
||||
|
||||
return ciphertext, mac, err
|
||||
}
|
||||
|
||||
// Decrypt verifies and decrypts ciphertext; Init1 and unencrypted types pass through.
|
||||
func (tc *Crypt) Decrypt(
|
||||
packetType byte,
|
||||
packetID uint16,
|
||||
generationID uint32,
|
||||
header, ciphertext, tag []byte,
|
||||
dummy bool,
|
||||
unencrypted bool,
|
||||
) ([]byte, error) {
|
||||
if packetType == init1PacketType {
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
if unencrypted {
|
||||
if subtle.ConstantTimeCompare(tag[:fakeSignatureSize], tc.FakeSignature) != 1 {
|
||||
return nil, ErrFakeSignatureMismatch
|
||||
}
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
key, nonce := tc.GetKeyNonce(true, packetID, generationID, packetType, dummy)
|
||||
defer ReleaseKeyBuffer(key)
|
||||
|
||||
eax, err := NewEAX(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return eax.Decrypt(nonce, header, ciphertext, tag)
|
||||
}
|
||||
@@ -0,0 +1,640 @@
|
||||
package crypto_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/honeybbq/teamspeak-go/crypto"
|
||||
"github.com/honeybbq/teamspeak-go/handshake"
|
||||
"github.com/oasisprotocol/curve25519-voi/curve"
|
||||
"github.com/oasisprotocol/curve25519-voi/curve/scalar"
|
||||
)
|
||||
|
||||
func TestGetSharedSecret2(t *testing.T) {
|
||||
publicKeyHex := "9d93589a4a86cf80d8dc1c1b384555289454021ad2f5dacf29d9938eade940b1"
|
||||
privateKeyHex := "58cd07b6765c3427afcfa64c73a609705a7f1656f40c582c7362080791bcfb68"
|
||||
expectedSharedSecretHex := "8aa100de2e0cde11827c36b5b3ef2758b1a7d52a202c375049cd8a3944d764" +
|
||||
"14b9854db31f781b5b51f37c025e9efee70edcd7189ccb7831a04eb7bc09e5b20b"
|
||||
|
||||
publicKey, _ := hex.DecodeString(publicKeyHex)
|
||||
privateKey, _ := hex.DecodeString(privateKeyHex)
|
||||
|
||||
sharedSecret, err := crypto.GetSharedSecret2(publicKey, privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSharedSecret2 failed: %v", err)
|
||||
}
|
||||
|
||||
actualSharedSecretHex := hex.EncodeToString(sharedSecret)
|
||||
if actualSharedSecretHex != expectedSharedSecretHex {
|
||||
t.Errorf("sharedSecret mismatch:\n expected: %s\n actual: %s", expectedSharedSecretHex, actualSharedSecretHex)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetKeyNonce(t *testing.T) {
|
||||
ivStructHex := "10ea569521d4d88e06a63db265416d780daf03a1ee4d3731ba22e8981e61d292" +
|
||||
"febebc434ce2a2ac36e8e1bd2b6cd9c953f84d7a269cc42f33917de8c47b8bdf"
|
||||
expectedKeyHex := "0659e387b9258c3f4fe32b31bc794dee"
|
||||
expectedNonceHex := "ec4630a6e61e216f61e15788bb42eaec"
|
||||
|
||||
ivStruct, _ := hex.DecodeString(ivStructHex)
|
||||
|
||||
tc := &crypto.Crypt{
|
||||
IvStruct: ivStruct,
|
||||
CryptoInitComplete: true,
|
||||
CachedKeys: make(map[uint64]crypto.KeyNonce),
|
||||
}
|
||||
|
||||
key, nonce := tc.GetKeyNonce(false, 2, 0, 2, false)
|
||||
|
||||
actualKeyHex := hex.EncodeToString(key)
|
||||
actualNonceHex := hex.EncodeToString(nonce)
|
||||
|
||||
if actualKeyHex != expectedKeyHex {
|
||||
t.Errorf("key mismatch:\n expected: %s\n actual: %s", expectedKeyHex, actualKeyHex)
|
||||
}
|
||||
if actualNonceHex != expectedNonceHex {
|
||||
t.Errorf("nonce mismatch:\n expected: %s\n actual: %s", expectedNonceHex, actualNonceHex)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateTemporaryKey(t *testing.T) {
|
||||
pubKey, privKey, err := crypto.GenerateTemporaryKey()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateTemporaryKey failed: %v", err)
|
||||
}
|
||||
|
||||
if len(pubKey) != 32 {
|
||||
t.Errorf("publicKey should be 32 bytes, got %d", len(pubKey))
|
||||
}
|
||||
if len(privKey) != 32 {
|
||||
t.Errorf("privateKey should be 32 bytes, got %d", len(privKey))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSharedSecret2WithCSharpData(t *testing.T) {
|
||||
publicKeyHex := "a878824253ba90c33297d0e44fa52439d9a35e316200e712d9d9e0efcd11dc0a"
|
||||
privateKeyHex := "b8985f89031ee1adf325fb5595fe5810f232fa33c5629eb4632969e17e69717f"
|
||||
expectedSharedSecretHex := "91478e774dc13a156cc2019c6c6ebe63d220381a2a914a6bedd49058685fdc" +
|
||||
"55a02c79569a62d4d71926899c8e45fb56122ef86a445cfb461689c945c826e707"
|
||||
|
||||
publicKey, _ := hex.DecodeString(publicKeyHex)
|
||||
privateKey, _ := hex.DecodeString(privateKeyHex)
|
||||
expectedSharedSecret, _ := hex.DecodeString(expectedSharedSecretHex)
|
||||
|
||||
sharedSecret, err := crypto.GetSharedSecret2(publicKey, privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSharedSecret2 failed: %v", err)
|
||||
}
|
||||
|
||||
if hex.EncodeToString(sharedSecret) != hex.EncodeToString(expectedSharedSecret) {
|
||||
t.Errorf("sharedSecret mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemporaryKeyWithFixedPrivate(t *testing.T) {
|
||||
privateKeyHex := "a02708b21598ae10932dc8eac25cf70bdd033c1f36f14a2caf24036dd8010d5b"
|
||||
expectedPublicKeyHex := "f67d0b5b0db004ab4f5df21d9e92f184e32aa45d90f483889912f95e7071ad79"
|
||||
|
||||
privateKey, _ := hex.DecodeString(privateKeyHex)
|
||||
|
||||
sc, err := scalar.NewFromBits(privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFromBits failed: %v", err)
|
||||
}
|
||||
publicKey, err := curve.NewEdwardsPoint().MulBasepoint(curve.ED25519_BASEPOINT_TABLE, sc).MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("MulBasepoint failed: %v", err)
|
||||
}
|
||||
|
||||
if hex.EncodeToString(publicKey) != expectedPublicKeyHex {
|
||||
t.Errorf("publicKey mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFullLicenseParseAndDerive(t *testing.T) {
|
||||
licenseBase64 := "AQBgjAAqtcBUrw5futTtkl3+EM3OW4Lal6OTPlwuv4xV/gIRFlEAG0Nl" +
|
||||
"AAcAAAAgQW5vbnltb3VzAACWSZf+Mjl5RT5mu4rvf8nhAZp9TjXO10XfGHQ9HQPtHiAYiqjtGItRrQ=="
|
||||
licenseBytes, _ := base64.StdEncoding.DecodeString(licenseBase64)
|
||||
|
||||
chain, err := handshake.ParseLicenses(licenseBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseLicenses failed: %v", err)
|
||||
}
|
||||
|
||||
if len(chain.Blocks) != 2 {
|
||||
t.Errorf("expected 2 blocks, got %d", len(chain.Blocks))
|
||||
}
|
||||
|
||||
key, err := chain.DeriveKey()
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKey failed: %v", err)
|
||||
}
|
||||
|
||||
if len(key) != 32 {
|
||||
t.Errorf("expected 32-byte key, got %d bytes", len(key))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityStringRoundtrip(t *testing.T) {
|
||||
id, err := crypto.GenerateIdentity(0)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateIdentity failed: %v", err)
|
||||
}
|
||||
s := id.String()
|
||||
id2, err := crypto.IdentityFromString(s)
|
||||
if err != nil {
|
||||
t.Fatalf("IdentityFromString failed: %v", err)
|
||||
}
|
||||
// Compare via serialised form to avoid accessing deprecated D field directly.
|
||||
if id.String() != id2.String() {
|
||||
t.Errorf("serialized identity mismatch: %q vs %q", id.String(), id2.String())
|
||||
}
|
||||
if id.Offset != id2.Offset {
|
||||
t.Errorf("Offset mismatch: %d vs %d", id.Offset, id2.Offset)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityFromStringErrors(t *testing.T) {
|
||||
cases := []string{
|
||||
"",
|
||||
"notvalidnocodon",
|
||||
"invalid==base64:0",
|
||||
"dGVzdA==:notanumber",
|
||||
}
|
||||
for _, s := range cases {
|
||||
_, err := crypto.IdentityFromString(s)
|
||||
if err == nil {
|
||||
t.Errorf("IdentityFromString(%q) expected error, got nil", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentitySecurityLevel(t *testing.T) {
|
||||
id, err := crypto.GenerateIdentity(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lvl := id.SecurityLevel()
|
||||
if lvl < 0 {
|
||||
t.Errorf("SecurityLevel should be >= 0, got %d", lvl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityUpgradeToLevel(t *testing.T) {
|
||||
id, err := crypto.GenerateIdentity(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = id.UpgradeToLevel(1, context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("UpgradeToLevel(1) failed: %v", err)
|
||||
}
|
||||
if id.SecurityLevel() < 1 {
|
||||
t.Errorf("SecurityLevel after upgrade = %d, want >= 1", id.SecurityLevel())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityUpgradeCtxCancelled(t *testing.T) {
|
||||
id, err := crypto.GenerateIdentity(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // cancel immediately
|
||||
err = id.UpgradeToLevel(100, ctx)
|
||||
if err == nil {
|
||||
t.Error("expected context cancellation error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicKeyBase64RoundtrippableWithImport(t *testing.T) {
|
||||
id, err := crypto.GenerateIdentity(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pubB64 := id.PublicKeyBase64()
|
||||
pubBytes, err := base64.StdEncoding.DecodeString(pubB64)
|
||||
if err != nil {
|
||||
t.Fatalf("base64 decode failed: %v", err)
|
||||
}
|
||||
// Verify round-trip: import the bytes and check the UID is consistent.
|
||||
_, err = crypto.ImportPublicKey(pubBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("ImportPublicKey failed: %v", err)
|
||||
}
|
||||
// GetUidFromPublicKey verifies the public key serialisation is stable.
|
||||
uid1 := crypto.GetUidFromPublicKey(pubB64)
|
||||
uid2 := crypto.GetUidFromPublicKey(pubB64)
|
||||
if uid1 != uid2 {
|
||||
t.Error("UID is not stable after import")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUidFromPublicKey(t *testing.T) {
|
||||
id, err := crypto.GenerateIdentity(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
uid1 := crypto.GetUidFromPublicKey(id.PublicKeyBase64())
|
||||
uid2 := crypto.GetUidFromPublicKey(id.PublicKeyBase64())
|
||||
if uid1 != uid2 {
|
||||
t.Error("GetUidFromPublicKey is not deterministic")
|
||||
}
|
||||
// SHA-1 produces 20 bytes → base64 = 28 chars
|
||||
if len(uid1) != 28 {
|
||||
t.Errorf("UID length = %d, want 28", len(uid1))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHash512Length(t *testing.T) {
|
||||
out := crypto.Hash512([]byte("hello"))
|
||||
if len(out) != 64 {
|
||||
t.Errorf("Hash512 output length = %d, want 64", len(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHash512Deterministic(t *testing.T) {
|
||||
data := []byte("determinism test")
|
||||
if !bytes.Equal(crypto.Hash512(data), crypto.Hash512(data)) {
|
||||
t.Error("Hash512 is not deterministic")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHash512EmptyInput(t *testing.T) {
|
||||
// SHA-512 of empty string is a well-known constant.
|
||||
const emptyHex = "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce" +
|
||||
"47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
|
||||
out := crypto.Hash512(nil)
|
||||
if hex.EncodeToString(out) != emptyHex {
|
||||
t.Errorf("Hash512(nil) = %x, want %s", out, emptyHex)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampScalar(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
key[0] = 0xFF
|
||||
key[31] = 0xFF
|
||||
crypto.ClampScalar(key)
|
||||
if key[0]&0x07 != 0 {
|
||||
t.Errorf("key[0] low 3 bits should be 0 after clamping, got 0x%02X", key[0])
|
||||
}
|
||||
if key[31]&0x80 != 0 {
|
||||
t.Errorf("key[31] high bit should be 0 after clamping, got 0x%02X", key[31])
|
||||
}
|
||||
if key[31]&0x40 == 0 {
|
||||
t.Errorf("key[31] bit 6 should be 1 after clamping, got 0x%02X", key[31])
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampScalarTooShort(t *testing.T) {
|
||||
key := []byte{0xFF, 0xFF}
|
||||
crypto.ClampScalar(key) // should not panic
|
||||
}
|
||||
|
||||
// Sign / VerifySign
|
||||
|
||||
func TestSignAndVerify(t *testing.T) {
|
||||
id, err := crypto.GenerateIdentity(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data := []byte("the message to sign")
|
||||
sig, err := crypto.Sign(id.PrivateKey, data)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign failed: %v", err)
|
||||
}
|
||||
if !crypto.VerifySign(&id.PrivateKey.PublicKey, data, sig) {
|
||||
t.Error("VerifySign returned false for valid signature")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifySignFailsOnTamperedData(t *testing.T) {
|
||||
id, err := crypto.GenerateIdentity(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data := []byte("original")
|
||||
sig, _ := crypto.Sign(id.PrivateKey, data)
|
||||
if crypto.VerifySign(&id.PrivateKey.PublicKey, append(data, 'X'), sig) {
|
||||
t.Error("VerifySign should fail for tampered data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifySignFailsOnTamperedSig(t *testing.T) {
|
||||
id, err := crypto.GenerateIdentity(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data := []byte("original")
|
||||
sig, _ := crypto.Sign(id.PrivateKey, data)
|
||||
sig[0] ^= 0xFF
|
||||
if crypto.VerifySign(&id.PrivateKey.PublicKey, data, sig) {
|
||||
t.Error("VerifySign should fail for tampered signature")
|
||||
}
|
||||
}
|
||||
|
||||
func makeSolveData(x, n byte) []byte {
|
||||
data := make([]byte, 128)
|
||||
data[63] = x // x as 64-byte big-endian
|
||||
data[127] = n // n as 64-byte big-endian
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func TestSolveRsaChallengeLevel0(t *testing.T) {
|
||||
// level=0: no squarings, result = x (padded to 64 bytes)
|
||||
tc := crypto.NewCrypt(nil)
|
||||
data := makeSolveData(5, 100) // x=5, n=100
|
||||
result, err := tc.SolveRsaChallenge(data, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("SolveRsaChallenge failed: %v", err)
|
||||
}
|
||||
if len(result) != 64 {
|
||||
t.Errorf("result length = %d, want 64", len(result))
|
||||
}
|
||||
if result[63] != 5 {
|
||||
t.Errorf("result[63] = %d, want 5", result[63])
|
||||
}
|
||||
for i := range 63 {
|
||||
if result[i] != 0 {
|
||||
t.Errorf("result[%d] = %d, want 0", i, result[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSolveRsaChallengeLevel2(t *testing.T) {
|
||||
// x=2, n=17, level=2: y = ((2^2)^2) mod 17 = 4^2 mod 17 = 16
|
||||
tc := crypto.NewCrypt(nil)
|
||||
data := makeSolveData(2, 17)
|
||||
result, err := tc.SolveRsaChallenge(data, 0, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("SolveRsaChallenge failed: %v", err)
|
||||
}
|
||||
if result[63] != 16 {
|
||||
t.Errorf("result[63] = %d, want 16", result[63])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSolveRsaChallengeNegativeLevel(t *testing.T) {
|
||||
tc := crypto.NewCrypt(nil)
|
||||
_, err := tc.SolveRsaChallenge(make([]byte, 128), 0, -1)
|
||||
if err == nil {
|
||||
t.Error("expected error for level < 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSolveRsaChallengeLevelTooHigh(t *testing.T) {
|
||||
tc := crypto.NewCrypt(nil)
|
||||
_, err := tc.SolveRsaChallenge(make([]byte, 128), 0, 1000001)
|
||||
if err == nil {
|
||||
t.Error("expected error for level > 1000000")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCrypt(t *testing.T) {
|
||||
id, _ := crypto.GenerateIdentity(0)
|
||||
tc := crypto.NewCrypt(id)
|
||||
if tc == nil {
|
||||
t.Fatal("expected non-nil Crypt")
|
||||
}
|
||||
if len(tc.FakeSignature) != 8 {
|
||||
t.Errorf("FakeSignature length = %d, want 8", len(tc.FakeSignature))
|
||||
}
|
||||
if tc.CachedKeys == nil {
|
||||
t.Error("CachedKeys should be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSharedSecret(t *testing.T) {
|
||||
tc := crypto.NewCrypt(nil)
|
||||
alpha := make([]byte, 10)
|
||||
beta := make([]byte, 10)
|
||||
sharedKey := make([]byte, 20)
|
||||
err := tc.SetSharedSecret(alpha, beta, sharedKey)
|
||||
if err != nil {
|
||||
t.Fatalf("SetSharedSecret failed: %v", err)
|
||||
}
|
||||
if !tc.CryptoInitComplete {
|
||||
t.Error("CryptoInitComplete should be true")
|
||||
}
|
||||
if len(tc.IvStruct) != 20 {
|
||||
t.Errorf("IvStruct length = %d, want 20", len(tc.IvStruct))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugCryptoStateEmpty(t *testing.T) {
|
||||
tc := crypto.NewCrypt(nil)
|
||||
length, hexStr := tc.DebugCryptoState()
|
||||
if length != 0 || hexStr != "" {
|
||||
t.Errorf("empty Crypt: DebugCryptoState() = (%d, %q), want (0, \"\")", length, hexStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugCryptoStateAfterSetSharedSecret(t *testing.T) {
|
||||
tc := crypto.NewCrypt(nil)
|
||||
_ = tc.SetSharedSecret(make([]byte, 10), make([]byte, 10), make([]byte, 20))
|
||||
length, hexStr := tc.DebugCryptoState()
|
||||
if length == 0 {
|
||||
t.Error("IvStruct should be non-empty after SetSharedSecret")
|
||||
}
|
||||
if len(hexStr) == 0 {
|
||||
t.Error("FakeSignature hex should be non-empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptInit1Passthrough(t *testing.T) {
|
||||
// PacketTypeInit1 (type=8): plaintext is returned unchanged, MAC = "TS3INIT1"
|
||||
tc := crypto.NewCrypt(nil)
|
||||
plaintext := []byte{0x01, 0x02, 0x03}
|
||||
header := []byte{0x00, 0x65, 0x00, 0x00, 0x08}
|
||||
ct, mac, err := tc.Encrypt(8, 0, 0, header, plaintext, false, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt Init1 failed: %v", err)
|
||||
}
|
||||
if !bytes.Equal(ct, plaintext) {
|
||||
t.Error("Init1: ciphertext should equal plaintext")
|
||||
}
|
||||
if string(mac) != "TS3INIT1" {
|
||||
t.Errorf("Init1 MAC = %q, want TS3INIT1", mac)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecryptInit1Passthrough(t *testing.T) {
|
||||
tc := crypto.NewCrypt(nil)
|
||||
data := []byte{0xAA, 0xBB}
|
||||
header := []byte{0x00}
|
||||
pt, err := tc.Decrypt(8, 0, 0, header, data, nil, false, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Decrypt Init1 failed: %v", err)
|
||||
}
|
||||
if !bytes.Equal(pt, data) {
|
||||
t.Error("Init1: decrypted should equal original data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDecryptUnencrypted(t *testing.T) {
|
||||
tc := &crypto.Crypt{
|
||||
FakeSignature: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},
|
||||
CachedKeys: make(map[uint64]crypto.KeyNonce),
|
||||
}
|
||||
plaintext := []byte("voice packet data")
|
||||
header := []byte{0x00, 0x01, 0x02}
|
||||
ct, mac, err := tc.Encrypt(0, 5, 0, header, plaintext, false, true)
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt unencrypted failed: %v", err)
|
||||
}
|
||||
if !bytes.Equal(ct, plaintext) {
|
||||
t.Error("unencrypted: ciphertext should equal plaintext")
|
||||
}
|
||||
if !bytes.Equal(mac, tc.FakeSignature) {
|
||||
t.Error("unencrypted: MAC should equal FakeSignature")
|
||||
}
|
||||
pt, err := tc.Decrypt(0, 5, 0, header, ct, mac, false, true)
|
||||
if err != nil {
|
||||
t.Fatalf("Decrypt unencrypted failed: %v", err)
|
||||
}
|
||||
if !bytes.Equal(pt, plaintext) {
|
||||
t.Error("unencrypted decrypt mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecryptUnencryptedBadSignature(t *testing.T) {
|
||||
tc := &crypto.Crypt{
|
||||
FakeSignature: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},
|
||||
CachedKeys: make(map[uint64]crypto.KeyNonce),
|
||||
}
|
||||
badTag := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}
|
||||
_, err := tc.Decrypt(0, 1, 0, nil, []byte("data"), badTag, false, true)
|
||||
if err == nil {
|
||||
t.Error("expected ErrFakeSignatureMismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDecryptEAXRoundtripDummy(t *testing.T) {
|
||||
// dummy=true uses fixed key/nonce; both Encrypt and Decrypt use the same
|
||||
// dummy key so the roundtrip is self-consistent.
|
||||
tc := &crypto.Crypt{
|
||||
FakeSignature: make([]byte, 8),
|
||||
CachedKeys: make(map[uint64]crypto.KeyNonce),
|
||||
}
|
||||
plaintext := []byte("hello encrypted world")
|
||||
header := []byte{0x00, 0x01, 0x02, 0x03, 0x04}
|
||||
ct, mac, err := tc.Encrypt(2, 42, 0, header, plaintext, true, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt EAX failed: %v", err)
|
||||
}
|
||||
if bytes.Equal(ct, plaintext) {
|
||||
t.Error("ciphertext should differ from plaintext")
|
||||
}
|
||||
pt, err := tc.Decrypt(2, 42, 0, header, ct, mac, true, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Decrypt EAX failed: %v", err)
|
||||
}
|
||||
if !bytes.Equal(pt, plaintext) {
|
||||
t.Errorf("decrypted = %q, want %q", pt, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcquireReleaseKeyBuffer(t *testing.T) {
|
||||
buf := crypto.AcquireKeyBuffer()
|
||||
if len(buf) != 16 {
|
||||
t.Errorf("buffer len = %d, want 16", len(buf))
|
||||
}
|
||||
crypto.ReleaseKeyBuffer(buf)
|
||||
// Acquire again — should get a buffer of the correct length
|
||||
buf2 := crypto.AcquireKeyBuffer()
|
||||
if len(buf2) != 16 {
|
||||
t.Errorf("recycled buffer len = %d, want 16", len(buf2))
|
||||
}
|
||||
crypto.ReleaseKeyBuffer(buf2)
|
||||
}
|
||||
|
||||
func TestReleaseKeyBufferWrongLength(t *testing.T) {
|
||||
// Wrong-length buffers should not be returned to the pool (no panic)
|
||||
crypto.ReleaseKeyBuffer(make([]byte, 15))
|
||||
crypto.ReleaseKeyBuffer(make([]byte, 0))
|
||||
}
|
||||
|
||||
// TestInitCrypto verifies that InitCrypto performs ECDH with a freshly generated
|
||||
// server key pair and marks crypto as initialized.
|
||||
func TestInitCrypto(t *testing.T) {
|
||||
clientID, err := crypto.IdentityFromString(
|
||||
"W2OSGpWxkzBPJjt8iyJFsMnqnwHCnxOlmE9gWFOFnKs=:0",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("IdentityFromString: %v", err)
|
||||
}
|
||||
tc := crypto.NewCrypt(clientID)
|
||||
|
||||
// Generate a fresh server identity to use as the server public key.
|
||||
serverID, err := crypto.IdentityFromString(
|
||||
"W2OSGpWxkzBPJjt8iyJFsMnqnwHCnxOlmE9gWFOFnKs=:0",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("IdentityFromString server: %v", err)
|
||||
}
|
||||
omega := serverID.PublicKeyBase64()
|
||||
|
||||
// In the clientinitiv handshake path, alpha and beta are both 10 bytes.
|
||||
// SetSharedSecret uses SHA-1 (20 bytes) as the shared key, so beta must be <= 10 bytes.
|
||||
alpha := base64.StdEncoding.EncodeToString(make([]byte, 10))
|
||||
beta := base64.StdEncoding.EncodeToString(make([]byte, 10))
|
||||
|
||||
err = tc.InitCrypto(alpha, beta, omega)
|
||||
if err != nil {
|
||||
t.Fatalf("InitCrypto failed: %v", err)
|
||||
}
|
||||
if !tc.CryptoInitComplete {
|
||||
t.Error("expected CryptoInitComplete to be true")
|
||||
}
|
||||
// IvStruct = 10 (alpha len) + len(betaBytes)
|
||||
betaBytes, _ := base64.StdEncoding.DecodeString(beta)
|
||||
expectedLen := 10 + len(betaBytes)
|
||||
if len(tc.IvStruct) != expectedLen {
|
||||
t.Errorf("IvStruct length = %d, want %d", len(tc.IvStruct), expectedLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitCrypto_InvalidAlpha(t *testing.T) {
|
||||
clientID, _ := crypto.IdentityFromString("W2OSGpWxkzBPJjt8iyJFsMnqnwHCnxOlmE9gWFOFnKs=:0")
|
||||
tc := crypto.NewCrypt(clientID)
|
||||
err := tc.InitCrypto("not-base64!!!", "", "")
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid alpha base64")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitCrypto_InvalidBeta(t *testing.T) {
|
||||
clientID, _ := crypto.IdentityFromString("W2OSGpWxkzBPJjt8iyJFsMnqnwHCnxOlmE9gWFOFnKs=:0")
|
||||
tc := crypto.NewCrypt(clientID)
|
||||
err := tc.InitCrypto(base64.StdEncoding.EncodeToString([]byte("alpha")), "not-base64!!!", "")
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid beta base64")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitCrypto_InvalidOmega(t *testing.T) {
|
||||
clientID, _ := crypto.IdentityFromString("W2OSGpWxkzBPJjt8iyJFsMnqnwHCnxOlmE9gWFOFnKs=:0")
|
||||
tc := crypto.NewCrypt(clientID)
|
||||
alpha := base64.StdEncoding.EncodeToString(make([]byte, 10))
|
||||
beta := base64.StdEncoding.EncodeToString(make([]byte, 10))
|
||||
err := tc.InitCrypto(alpha, beta, "not-base64!!!")
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid omega base64")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitCrypto_InvalidOmegaKey(t *testing.T) {
|
||||
clientID, _ := crypto.IdentityFromString("W2OSGpWxkzBPJjt8iyJFsMnqnwHCnxOlmE9gWFOFnKs=:0")
|
||||
tc := crypto.NewCrypt(clientID)
|
||||
alpha := base64.StdEncoding.EncodeToString(make([]byte, 10))
|
||||
beta := base64.StdEncoding.EncodeToString(make([]byte, 10))
|
||||
badOmega := base64.StdEncoding.EncodeToString([]byte("this is not a valid public key"))
|
||||
err := tc.InitCrypto(alpha, beta, badOmega)
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid omega key bytes")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
cryptosubtle "crypto/subtle"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/tink-crypto/tink-go/v2/mac/subtle"
|
||||
)
|
||||
|
||||
const (
|
||||
eaxTagByte0 = 0
|
||||
eaxTagByte1 = 1
|
||||
eaxTagByte2 = 2
|
||||
eaxTagSize = 8
|
||||
eaxBlockSize = 16
|
||||
eaxPoolBufferSize = 528
|
||||
)
|
||||
|
||||
// cmacInputPool sizes buffers for nonce + header + ciphertext (TS3 wire limits).
|
||||
var cmacInputPool = sync.Pool{
|
||||
New: func() any {
|
||||
buf := make([]byte, eaxPoolBufferSize)
|
||||
|
||||
return &buf
|
||||
},
|
||||
}
|
||||
|
||||
// EAX implementation for TeamSpeak 3 (64-bit tag).
|
||||
type EAX struct {
|
||||
block cipher.Block
|
||||
cmacHasher *subtle.AESCMAC
|
||||
}
|
||||
|
||||
func NewEAX(key []byte) (*EAX, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmacHasher, err := subtle.NewAESCMAC(key, eaxBlockSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &EAX{block: block, cmacHasher: cmacHasher}, nil
|
||||
}
|
||||
|
||||
var ErrEAXTagMismatch = errors.New("EAX tag mismatch")
|
||||
|
||||
func (e *EAX) Encrypt(nonce, header, plaintext []byte) ([]byte, []byte, error) {
|
||||
nStar, _ := e.cmac(eaxTagByte0, nonce)
|
||||
hStar, _ := e.cmac(eaxTagByte1, header)
|
||||
|
||||
// CTR encryption
|
||||
stream := cipher.NewCTR(e.block, nStar)
|
||||
ciphertext := make([]byte, len(plaintext))
|
||||
stream.XORKeyStream(ciphertext, plaintext)
|
||||
|
||||
cStar, _ := e.cmac(eaxTagByte2, ciphertext)
|
||||
|
||||
tag := make([]byte, eaxTagSize)
|
||||
for i := range eaxTagSize {
|
||||
tag[i] = nStar[i] ^ hStar[i] ^ cStar[i]
|
||||
}
|
||||
|
||||
return ciphertext, tag, nil
|
||||
}
|
||||
|
||||
func (e *EAX) Decrypt(nonce, header, ciphertext, tag []byte) ([]byte, error) {
|
||||
nStar, _ := e.cmac(eaxTagByte0, nonce)
|
||||
hStar, _ := e.cmac(eaxTagByte1, header)
|
||||
cStar, _ := e.cmac(eaxTagByte2, ciphertext)
|
||||
|
||||
var expected [eaxTagSize]byte
|
||||
for i := range eaxTagSize {
|
||||
expected[i] = nStar[i] ^ hStar[i] ^ cStar[i]
|
||||
}
|
||||
if cryptosubtle.ConstantTimeCompare(expected[:], tag[:eaxTagSize]) != 1 {
|
||||
return nil, ErrEAXTagMismatch
|
||||
}
|
||||
|
||||
// CTR decryption (same as encryption)
|
||||
stream := cipher.NewCTR(e.block, nStar)
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
stream.XORKeyStream(plaintext, ciphertext)
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
func (e *EAX) cmac(tag byte, data []byte) ([]byte, error) {
|
||||
inputLen := eaxBlockSize + len(data)
|
||||
|
||||
inputBufPtr, ok := cmacInputPool.Get().(*[]byte)
|
||||
if !ok || inputBufPtr == nil {
|
||||
buf := make([]byte, inputLen)
|
||||
inputBufPtr = &buf
|
||||
}
|
||||
inputBuf := *inputBufPtr
|
||||
if cap(inputBuf) < inputLen {
|
||||
inputBuf = make([]byte, inputLen)
|
||||
} else {
|
||||
inputBuf = inputBuf[:inputLen]
|
||||
}
|
||||
|
||||
for i := range eaxBlockSize - 1 {
|
||||
inputBuf[i] = 0
|
||||
}
|
||||
inputBuf[eaxBlockSize-1] = tag
|
||||
copy(inputBuf[eaxBlockSize:], data)
|
||||
|
||||
result, err := e.cmacHasher.ComputeMAC(inputBuf)
|
||||
|
||||
cmacInputPool.Put(&inputBuf)
|
||||
|
||||
return result, err
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package crypto_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/honeybbq/teamspeak-go/crypto"
|
||||
"github.com/tink-crypto/tink-go/v2/mac/subtle"
|
||||
)
|
||||
|
||||
func TestEAXEncrypt(t *testing.T) {
|
||||
key := []byte("c:\\windows\\syste") // 16 bytes
|
||||
nonce := []byte("m\\firewall32.cpl") // 16 bytes
|
||||
header := []byte{0x00, 0x65, 0x00, 0x00, 0x08} // Init1 header
|
||||
plaintext := []byte{
|
||||
0x01, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x35, 0xfc, 0x54, 0x2f,
|
||||
}
|
||||
|
||||
eax, err := crypto.NewEAX(key)
|
||||
if err != nil {
|
||||
t.Fatalf("NewEAX failed: %v", err)
|
||||
}
|
||||
|
||||
ciphertext, tag, err := eax.Encrypt(nonce, header, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt failed: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Ciphertext: %x", ciphertext)
|
||||
t.Logf("Tag: %x", tag)
|
||||
|
||||
// Decrypt back
|
||||
decrypted, err := eax.Decrypt(nonce, header, ciphertext, tag)
|
||||
if err != nil {
|
||||
t.Fatalf("Decrypt failed: %v", err)
|
||||
}
|
||||
|
||||
if hex.EncodeToString(decrypted) != hex.EncodeToString(plaintext) {
|
||||
t.Errorf("Decrypted data mismatch!\nExpected: %x\nActual: %x", plaintext, decrypted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCMACAlignment(t *testing.T) {
|
||||
key := []byte("c:\\windows\\syste")
|
||||
data := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}
|
||||
|
||||
mac, err := subtle.NewAESCMAC(key, 16)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAESCMAC failed: %v", err)
|
||||
}
|
||||
sum, err := mac.ComputeMAC(data)
|
||||
if err != nil {
|
||||
t.Fatalf("ComputeMAC failed: %v", err)
|
||||
}
|
||||
// AES-CMAC output must be exactly 16 bytes and not all-zero for non-trivial input.
|
||||
if len(sum) != 16 {
|
||||
t.Errorf("CMAC length = %d, want 16", len(sum))
|
||||
}
|
||||
if bytes.Equal(sum, make([]byte, 16)) {
|
||||
t.Error("CMAC should not be all zeros for non-trivial input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEAXDecryptTagMismatch(t *testing.T) {
|
||||
key := []byte("c:\\windows\\syste")
|
||||
nonce := []byte("m\\firewall32.cpl")
|
||||
header := []byte{0x00, 0x01, 0x02}
|
||||
plaintext := []byte{0x01, 0x02, 0x03, 0x04}
|
||||
|
||||
eax, err := crypto.NewEAX(key)
|
||||
if err != nil {
|
||||
t.Fatalf("NewEAX failed: %v", err)
|
||||
}
|
||||
ciphertext, tag, err := eax.Encrypt(nonce, header, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt failed: %v", err)
|
||||
}
|
||||
|
||||
// Corrupt the tag
|
||||
corruptTag := make([]byte, len(tag))
|
||||
copy(corruptTag, tag)
|
||||
corruptTag[0] ^= 0xFF
|
||||
|
||||
_, err = eax.Decrypt(nonce, header, ciphertext, corruptTag)
|
||||
if err == nil {
|
||||
t.Error("expected tag mismatch error with corrupted tag")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEAXDecryptCiphertextTampered(t *testing.T) {
|
||||
key := []byte("c:\\windows\\syste")
|
||||
nonce := []byte("m\\firewall32.cpl")
|
||||
header := []byte{0x00, 0x01}
|
||||
plaintext := []byte{0xDE, 0xAD, 0xBE, 0xEF}
|
||||
|
||||
eax, err := crypto.NewEAX(key)
|
||||
if err != nil {
|
||||
t.Fatalf("NewEAX failed: %v", err)
|
||||
}
|
||||
ciphertext, tag, err := eax.Encrypt(nonce, header, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt failed: %v", err)
|
||||
}
|
||||
|
||||
// Corrupt the ciphertext
|
||||
tampered := make([]byte, len(ciphertext))
|
||||
copy(tampered, ciphertext)
|
||||
tampered[0] ^= 0x01
|
||||
|
||||
_, err = eax.Decrypt(nonce, header, tampered, tag)
|
||||
if err == nil {
|
||||
t.Error("expected tag mismatch error with tampered ciphertext")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEAXEmptyPlaintext(t *testing.T) {
|
||||
key := []byte("c:\\windows\\syste")
|
||||
nonce := []byte("m\\firewall32.cpl")
|
||||
header := []byte{0x00}
|
||||
plaintext := []byte{}
|
||||
|
||||
eax, err := crypto.NewEAX(key)
|
||||
if err != nil {
|
||||
t.Fatalf("NewEAX failed: %v", err)
|
||||
}
|
||||
ct, tag, err := eax.Encrypt(nonce, header, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt failed: %v", err)
|
||||
}
|
||||
pt, err := eax.Decrypt(nonce, header, ct, tag)
|
||||
if err != nil {
|
||||
t.Fatalf("Decrypt failed: %v", err)
|
||||
}
|
||||
if len(pt) != 0 {
|
||||
t.Errorf("expected empty plaintext, got %d bytes", len(pt))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEAXTagIs8Bytes(t *testing.T) {
|
||||
key := []byte("c:\\windows\\syste")
|
||||
nonce := []byte("m\\firewall32.cpl")
|
||||
eax, _ := crypto.NewEAX(key)
|
||||
_, tag, err := eax.Encrypt(nonce, nil, []byte("test"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(tag) != 8 {
|
||||
t.Errorf("tag length = %d, want 8 (TeamSpeak 64-bit tag)", len(tag))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEAXKnownVector(t *testing.T) {
|
||||
// Encrypt with known key/nonce/plaintext, then verify decrypt produces original.
|
||||
// The exact ciphertext is implementation-specific; we validate the roundtrip
|
||||
// and check that ciphertext differs from plaintext.
|
||||
key := []byte("c:\\windows\\syste")
|
||||
nonce := []byte("m\\firewall32.cpl")
|
||||
header := []byte{0x00, 0x65, 0x00, 0x00, 0x08}
|
||||
plaintext := []byte{0x01, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
|
||||
eax, _ := crypto.NewEAX(key)
|
||||
ct, tag, err := eax.Encrypt(nonce, header, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt failed: %v", err)
|
||||
}
|
||||
if bytes.Equal(ct, plaintext) {
|
||||
t.Error("ciphertext should differ from plaintext")
|
||||
}
|
||||
pt, err := eax.Decrypt(nonce, header, ct, tag)
|
||||
if err != nil {
|
||||
t.Fatalf("Decrypt failed: %v", err)
|
||||
}
|
||||
if hex.EncodeToString(pt) != hex.EncodeToString(plaintext) {
|
||||
t.Errorf("decrypted mismatch: got %x, want %x", pt, plaintext)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
|
||||
"github.com/oasisprotocol/curve25519-voi/curve"
|
||||
"github.com/oasisprotocol/curve25519-voi/curve/scalar"
|
||||
)
|
||||
|
||||
var errInvalidKeyLength = errors.New("invalid key length")
|
||||
|
||||
const (
|
||||
curve25519KeySize = 32
|
||||
clampMaskLow = 248
|
||||
clampMaskHigh = 127
|
||||
clampHighBit = 64
|
||||
sharedSignBit = 0x80
|
||||
privateKeyTopMask = 0x7F
|
||||
)
|
||||
|
||||
func GenerateTemporaryKey() ([]byte, []byte, error) {
|
||||
privateKey := make([]byte, curve25519KeySize)
|
||||
_, err := rand.Read(privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ClampScalar(privateKey)
|
||||
sc, err := scalar.NewFromBits(privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
publicKey, err := curve.NewEdwardsPoint().MulBasepoint(curve.ED25519_BASEPOINT_TABLE, sc).MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return publicKey, privateKey, nil
|
||||
}
|
||||
|
||||
func Sign(priv *ecdsa.PrivateKey, data []byte) ([]byte, error) {
|
||||
hash := sha256.Sum256(data)
|
||||
|
||||
return ecdsa.SignASN1(rand.Reader, priv, hash[:])
|
||||
}
|
||||
|
||||
func VerifySign(pub *ecdsa.PublicKey, data, sig []byte) bool {
|
||||
hash := sha256.Sum256(data)
|
||||
|
||||
return ecdsa.VerifyASN1(pub, hash[:], sig)
|
||||
}
|
||||
|
||||
func ClampScalar(key []byte) {
|
||||
if len(key) < curve25519KeySize {
|
||||
return
|
||||
}
|
||||
key[0] &= clampMaskLow
|
||||
key[curve25519KeySize-1] &= clampMaskHigh
|
||||
key[curve25519KeySize-1] |= clampHighBit
|
||||
}
|
||||
|
||||
func GetSharedSecret2(publicKey, privateKey []byte) ([]byte, error) {
|
||||
if len(publicKey) != curve25519KeySize || len(privateKey) != curve25519KeySize {
|
||||
return nil, errInvalidKeyLength
|
||||
}
|
||||
|
||||
privateKeyCpy := make([]byte, curve25519KeySize)
|
||||
copy(privateKeyCpy, privateKey)
|
||||
privateKeyCpy[curve25519KeySize-1] &= privateKeyTopMask
|
||||
sc, err := scalar.NewFromBits(privateKeyCpy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pub := curve.NewEdwardsPoint()
|
||||
err = pub.UnmarshalBinary(publicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pub.Neg(pub)
|
||||
|
||||
sharedPoint := curve.NewEdwardsPoint().Mul(pub, sc)
|
||||
shared, err := sharedPoint.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shared[curve25519KeySize-1] ^= sharedSignBit
|
||||
|
||||
hash := sha512.Sum512(shared)
|
||||
|
||||
return hash[:], nil
|
||||
}
|
||||
Reference in New Issue
Block a user