首次推送
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
|
||||
}
|
||||
Reference in New Issue
Block a user