Fix missing ALPN NextProtos for QUIC handshake
Add 'qcc' ALPN protocol to both ACME and insecure TLS configs. Bump golang.org/x/crypto and transitive x/ deps. Add golang.org/x/mobile for mobile client bindings.
This commit is contained in:
parent
4f88a7eab1
commit
dbb2782dd7
5 changed files with 735 additions and 8 deletions
17
go.mod
17
go.mod
|
|
@ -9,7 +9,7 @@ require (
|
|||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.21.0
|
||||
go.uber.org/zap v1.24.0
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/crypto v0.53.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -24,8 +24,8 @@ require (
|
|||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -34,8 +34,11 @@ require (
|
|||
github.com/mholt/acmez v1.0.4 // indirect
|
||||
github.com/miekg/dns v1.1.50 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
golang.org/x/mobile v0.0.0-20260611195102-4dd8f1dbf5d2 // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/tools v0.46.0 // indirect
|
||||
)
|
||||
|
||||
tool golang.org/x/mobile/cmd/gobind
|
||||
|
|
|
|||
16
go.sum
16
go.sum
|
|
@ -84,10 +84,16 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20260611195102-4dd8f1dbf5d2 h1:zoM1gIKhVkcQNm43kad8OHLgPNoJ12xIqmxHtKr8Mug=
|
||||
golang.org/x/mobile v0.0.0-20260611195102-4dd8f1dbf5d2/go.mod h1:QGMqsqLn6orFQ/ksqYMf+Fa33Soa1vPoHEd0Pj7N+lQ=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
|
@ -96,10 +102,14 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -112,6 +122,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
|
@ -120,6 +132,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
|
|
@ -127,6 +141,8 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk=
|
||||
golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
|
|||
|
|
@ -138,7 +138,8 @@ func (s *Server) setupTLS() (*tls.Config, error) {
|
|||
if len(s.cfg.ACME.Domains) == 0 || s.cfg.ACME.Email == "" {
|
||||
s.logger.Warn("ACME not configured, starting without TLS (insecure)")
|
||||
return &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
MinVersion: tls.VersionTLS13,
|
||||
NextProtos: []string{"qcc"},
|
||||
InsecureSkipVerify: true,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ func NewTLSConfig(domains []string, email, caType string, dnsSolver *certmagic.D
|
|||
|
||||
return &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
NextProtos: []string{"qcc"},
|
||||
GetCertificate: cmCfg.GetCertificate,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
706
mobile/client.go
Normal file
706
mobile/client.go
Normal file
|
|
@ -0,0 +1,706 @@
|
|||
package mobile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/niko/qcc/internal/auth"
|
||||
"github.com/niko/qcc/internal/protocol"
|
||||
"github.com/niko/qcc/pkg/types"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
mu sync.Mutex
|
||||
conn *quic.Conn
|
||||
transport *quic.Transport
|
||||
udpConn net.PacketConn
|
||||
|
||||
privKey ed25519.PrivateKey
|
||||
pubKey ed25519.PublicKey
|
||||
|
||||
number string
|
||||
certDER []byte
|
||||
caCertDER []byte
|
||||
|
||||
serverAddr string
|
||||
|
||||
callbacks Callbacks
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
e2eePriv [32]byte
|
||||
e2eePub [32]byte
|
||||
mediaKey []byte
|
||||
activeCall bool
|
||||
callID uint64
|
||||
|
||||
authOpened bool
|
||||
}
|
||||
|
||||
type Callbacks interface {
|
||||
OnIncomingCall(number string)
|
||||
OnCallAccepted()
|
||||
OnCallEnded()
|
||||
OnCallRejected()
|
||||
OnError(msg string)
|
||||
OnMedia(data []byte)
|
||||
}
|
||||
|
||||
func NewClient() *Client {
|
||||
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
|
||||
return &Client{
|
||||
privKey: priv,
|
||||
pubKey: pub,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SetCallbacks(cbs Callbacks) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.callbacks = cbs
|
||||
}
|
||||
|
||||
func (c *Client) Connect(addr string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.serverAddr = addr
|
||||
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
host = addr
|
||||
}
|
||||
|
||||
tlsConf := &tls.Config{
|
||||
ServerName: host,
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"qcc"},
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve addr: %w", err)
|
||||
}
|
||||
|
||||
udpConn, err := net.ListenUDP("udp", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen udp: %w", err)
|
||||
}
|
||||
c.udpConn = udpConn
|
||||
|
||||
tr := &quic.Transport{Conn: udpConn}
|
||||
c.transport = tr
|
||||
|
||||
ctx := context.Background()
|
||||
conn, err := tr.Dial(ctx, udpAddr, tlsConf, &quic.Config{
|
||||
EnableDatagrams: true,
|
||||
KeepAlivePeriod: 15 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
udpConn.Close()
|
||||
c.transport = nil
|
||||
c.udpConn = nil
|
||||
return fmt.Errorf("quic dial: %w", err)
|
||||
}
|
||||
|
||||
c.conn = conn
|
||||
c.ctx, c.cancel = context.WithCancel(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Authenticate() (string, error) {
|
||||
c.mu.Lock()
|
||||
conn := c.conn
|
||||
c.mu.Unlock()
|
||||
|
||||
if conn == nil {
|
||||
return "", fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
authSt, err := conn.OpenStream()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open auth stream: %w", err)
|
||||
}
|
||||
|
||||
if err := protocol.WriteFrame(authSt, byte(types.OpGetChallenge), nil); err != nil {
|
||||
authSt.Close()
|
||||
return "", fmt.Errorf("send challenge: %w", err)
|
||||
}
|
||||
|
||||
frame, err := protocol.ReadFrame(authSt)
|
||||
if err != nil {
|
||||
authSt.Close()
|
||||
return "", fmt.Errorf("read challenge: %w", err)
|
||||
}
|
||||
|
||||
if types.OpCode(frame.OpCode) != types.OpChallenge {
|
||||
authSt.Close()
|
||||
return "", fmt.Errorf("expected challenge, got op=%x", frame.OpCode)
|
||||
}
|
||||
|
||||
challenge := auth.UnmarshalChallenge(frame.Payload)
|
||||
if challenge == nil {
|
||||
authSt.Close()
|
||||
return "", fmt.Errorf("invalid challenge payload")
|
||||
}
|
||||
|
||||
solution := c.solvePoW(challenge)
|
||||
if solution == nil {
|
||||
authSt.Close()
|
||||
return "", fmt.Errorf("PoW solve failed")
|
||||
}
|
||||
|
||||
solnPayload := make([]byte, 40)
|
||||
copy(solnPayload[0:8], solution.ClientNonce[:])
|
||||
copy(solnPayload[8:40], solution.ClientPubKey[:])
|
||||
|
||||
if err := protocol.WriteFrame(authSt, byte(types.OpSolve), solnPayload); err != nil {
|
||||
authSt.Close()
|
||||
return "", fmt.Errorf("send solution: %w", err)
|
||||
}
|
||||
|
||||
frame, err = protocol.ReadFrame(authSt)
|
||||
if err != nil {
|
||||
authSt.Close()
|
||||
return "", fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
|
||||
if types.OpCode(frame.OpCode) == types.OpError {
|
||||
authSt.Close()
|
||||
errMsg := parseError(frame.Payload)
|
||||
return "", fmt.Errorf("server error: %s", errMsg)
|
||||
}
|
||||
|
||||
if types.OpCode(frame.OpCode) != types.OpIdentity {
|
||||
authSt.Close()
|
||||
return "", fmt.Errorf("expected identity, got op=%x", frame.OpCode)
|
||||
}
|
||||
|
||||
number, certDER, caCertDER := parseIdentityPayload(frame.Payload)
|
||||
if number == "" {
|
||||
authSt.Close()
|
||||
return "", fmt.Errorf("invalid identity payload")
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.number = number
|
||||
c.certDER = certDER
|
||||
c.caCertDER = caCertDER
|
||||
c.authOpened = true
|
||||
c.mu.Unlock()
|
||||
|
||||
go c.handleIncomingStreams()
|
||||
|
||||
return number, nil
|
||||
}
|
||||
|
||||
func (c *Client) solvePoW(challenge *auth.PoWChallenge) *auth.PoWSolution {
|
||||
bits := challenge.Bits
|
||||
var nonceCounter uint64
|
||||
|
||||
var solution auth.PoWSolution
|
||||
copy(solution.ClientPubKey[:], c.pubKey)
|
||||
|
||||
hashInput := make([]byte, 32)
|
||||
binary.BigEndian.PutUint64(hashInput[0:8], uint64(challenge.Timestamp))
|
||||
copy(hashInput[8:16], challenge.ServerNonce[:])
|
||||
|
||||
for {
|
||||
binary.BigEndian.PutUint64(hashInput[16:24], nonceCounter)
|
||||
copy(hashInput[24:], solution.ClientPubKey[:])
|
||||
|
||||
hash := sha256.Sum256(hashInput)
|
||||
|
||||
leadingZeros := 0
|
||||
for i := 0; i < 32; i++ {
|
||||
if hash[i] == 0 {
|
||||
leadingZeros += 8
|
||||
} else {
|
||||
for b := byte(0x80); b != 0; b >>= 1 {
|
||||
if hash[i]&b == 0 {
|
||||
leadingZeros++
|
||||
} else {
|
||||
goto checkDone
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
checkDone:
|
||||
if leadingZeros >= bits {
|
||||
binary.BigEndian.PutUint64(solution.ClientNonce[:], nonceCounter)
|
||||
return &solution
|
||||
}
|
||||
nonceCounter++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) MyNumber() string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.number
|
||||
}
|
||||
|
||||
func (c *Client) ServerAddr() string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.serverAddr
|
||||
}
|
||||
|
||||
func (c *Client) IsConnected() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.conn != nil
|
||||
}
|
||||
|
||||
func (c *Client) Disconnect() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.cancel != nil {
|
||||
c.cancel()
|
||||
}
|
||||
if c.conn != nil {
|
||||
c.conn.CloseWithError(0, "client disconnect")
|
||||
}
|
||||
if c.transport != nil {
|
||||
c.transport.Close()
|
||||
}
|
||||
if c.udpConn != nil {
|
||||
c.udpConn.Close()
|
||||
}
|
||||
c.conn = nil
|
||||
c.transport = nil
|
||||
c.udpConn = nil
|
||||
c.number = ""
|
||||
c.certDER = nil
|
||||
c.caCertDER = nil
|
||||
c.activeCall = false
|
||||
c.callID = 0
|
||||
c.mediaKey = nil
|
||||
c.authOpened = false
|
||||
}
|
||||
|
||||
func (c *Client) handleIncomingStreams() {
|
||||
c.mu.Lock()
|
||||
conn := c.conn
|
||||
ctx := c.ctx
|
||||
c.mu.Unlock()
|
||||
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
st, err := conn.AcceptStream(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go c.handleStream(st)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) handleStream(st *quic.Stream) {
|
||||
defer st.Close()
|
||||
|
||||
for {
|
||||
frame, err := protocol.ReadFrame(st)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch types.OpCode(frame.OpCode) {
|
||||
case types.OpRing:
|
||||
number, callerE2EE := parseRingPayload(frame.Payload)
|
||||
c.mu.Lock()
|
||||
c.e2eePub = callerE2EE
|
||||
c.mu.Unlock()
|
||||
if c.callbacks != nil {
|
||||
c.callbacks.OnIncomingCall(number)
|
||||
}
|
||||
|
||||
case types.OpPeerAccept:
|
||||
var peerE2EE [32]byte
|
||||
if len(frame.Payload) >= 32 {
|
||||
copy(peerE2EE[:], frame.Payload[:32])
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.e2eePub = peerE2EE
|
||||
c.deriveMediaKey()
|
||||
c.activeCall = true
|
||||
c.mu.Unlock()
|
||||
if c.callbacks != nil {
|
||||
c.callbacks.OnCallAccepted()
|
||||
}
|
||||
|
||||
case types.OpPeerEnd:
|
||||
c.mu.Lock()
|
||||
c.activeCall = false
|
||||
c.callID = 0
|
||||
c.mediaKey = nil
|
||||
c.mu.Unlock()
|
||||
if c.callbacks != nil {
|
||||
c.callbacks.OnCallEnded()
|
||||
}
|
||||
|
||||
case types.OpError:
|
||||
errMsg := parseError(frame.Payload)
|
||||
if c.callbacks != nil {
|
||||
c.callbacks.OnError(errMsg)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) handleDatagrams() {
|
||||
c.mu.Lock()
|
||||
conn := c.conn
|
||||
ctx := c.ctx
|
||||
c.mu.Unlock()
|
||||
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
data, err := conn.ReceiveDatagram(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pkt, ok := protocol.UnmarshalMediaPacket(data)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
cid := c.callID
|
||||
key := c.mediaKey
|
||||
c.mu.Unlock()
|
||||
|
||||
if pkt.CallID != cid {
|
||||
continue
|
||||
}
|
||||
|
||||
if key != nil && len(pkt.Payload) > 0 {
|
||||
decrypted, err := decryptE2EE(key, pkt.Payload)
|
||||
if err == nil {
|
||||
if c.callbacks != nil {
|
||||
c.callbacks.OnMedia(decrypted)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Dial(number string) error {
|
||||
c.mu.Lock()
|
||||
conn := c.conn
|
||||
c.mu.Unlock()
|
||||
if conn == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
e2eeKP, err := generateE2EEKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate e2ee key: %w", err)
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.e2eePriv = e2eeKP.PrivateKey
|
||||
c.mu.Unlock()
|
||||
|
||||
payload := make([]byte, 1+len(number)+32)
|
||||
payload[0] = byte(len(number))
|
||||
copy(payload[1:], number)
|
||||
copy(payload[1+len(number):], e2eeKP.PublicKey[:])
|
||||
|
||||
st, err := conn.OpenStream()
|
||||
if err != nil {
|
||||
return fmt.Errorf("open call stream: %w", err)
|
||||
}
|
||||
|
||||
if err := protocol.WriteFrame(st, byte(types.OpDial), payload); err != nil {
|
||||
st.Close()
|
||||
return fmt.Errorf("send dial: %w", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer st.Close()
|
||||
c.handleStream(st)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) AcceptCall() error {
|
||||
e2eeKP, err := generateE2EEKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.e2eePriv = e2eeKP.PrivateKey
|
||||
c.mu.Unlock()
|
||||
|
||||
payload := make([]byte, 32)
|
||||
copy(payload, e2eeKP.PublicKey[:])
|
||||
|
||||
c.mu.Lock()
|
||||
conn := c.conn
|
||||
c.mu.Unlock()
|
||||
if conn == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
st, err := conn.OpenStream()
|
||||
if err != nil {
|
||||
return fmt.Errorf("open stream: %w", err)
|
||||
}
|
||||
|
||||
if err := protocol.WriteFrame(st, byte(types.OpAccept), payload); err != nil {
|
||||
st.Close()
|
||||
return fmt.Errorf("send accept: %w", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer st.Close()
|
||||
c.handleStream(st)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) RejectCall() error {
|
||||
c.mu.Lock()
|
||||
conn := c.conn
|
||||
c.mu.Unlock()
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
st, err := conn.OpenStream()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer st.Close()
|
||||
|
||||
return protocol.WriteFrame(st, byte(types.OpReject), nil)
|
||||
}
|
||||
|
||||
func (c *Client) EndCall() error {
|
||||
c.mu.Lock()
|
||||
conn := c.conn
|
||||
c.activeCall = false
|
||||
c.callID = 0
|
||||
c.mediaKey = nil
|
||||
c.mu.Unlock()
|
||||
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
st, err := conn.OpenStream()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer st.Close()
|
||||
|
||||
return protocol.WriteFrame(st, byte(types.OpEnd), nil)
|
||||
}
|
||||
|
||||
func (c *Client) SendMedia(data []byte) error {
|
||||
c.mu.Lock()
|
||||
if !c.activeCall {
|
||||
c.mu.Unlock()
|
||||
return fmt.Errorf("no active call")
|
||||
}
|
||||
key := c.mediaKey
|
||||
cid := c.callID
|
||||
conn := c.conn
|
||||
c.mu.Unlock()
|
||||
|
||||
if key == nil {
|
||||
return fmt.Errorf("no media key")
|
||||
}
|
||||
if conn == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
encrypted, err := encryptE2EE(key, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt: %w", err)
|
||||
}
|
||||
|
||||
pkt := &protocol.MediaPacket{
|
||||
CallID: cid,
|
||||
Payload: encrypted,
|
||||
}
|
||||
|
||||
raw := protocol.MarshalMediaPacket(pkt)
|
||||
return conn.SendDatagram(raw)
|
||||
}
|
||||
|
||||
func (c *Client) StartMediaLoop() {
|
||||
go c.handleDatagrams()
|
||||
}
|
||||
|
||||
func (c *Client) deriveMediaKey() {
|
||||
shared, err := curve25519.X25519(c.e2eePriv[:], c.e2eePub[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
salt := make([]byte, 32)
|
||||
info := []byte("qcc-e2ee-media-key")
|
||||
kdf := hkdf.New(sha256.New, shared, salt, info)
|
||||
key := make([]byte, 32)
|
||||
if _, err := io.ReadFull(kdf, key); err != nil {
|
||||
return
|
||||
}
|
||||
c.mediaKey = key
|
||||
}
|
||||
|
||||
type e2eeKeypair struct {
|
||||
PrivateKey [32]byte
|
||||
PublicKey [32]byte
|
||||
}
|
||||
|
||||
func generateE2EEKey() (*e2eeKeypair, error) {
|
||||
priv := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, priv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
priv[0] &= 248
|
||||
priv[31] &= 127
|
||||
priv[31] |= 64
|
||||
|
||||
pub, err := curve25519.X25519(priv, curve25519.Basepoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result e2eeKeypair
|
||||
copy(result.PrivateKey[:], priv)
|
||||
copy(result.PublicKey[:], pub)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func encryptE2EE(key, plaintext []byte) ([]byte, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonce := make([]byte, chacha20poly1305.NonceSizeX)
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aead.Seal(nonce, nonce, plaintext, nil), nil
|
||||
}
|
||||
|
||||
func decryptE2EE(key, ciphertext []byte) ([]byte, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ciphertext) < chacha20poly1305.NonceSizeX {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
nonce := ciphertext[:chacha20poly1305.NonceSizeX]
|
||||
return aead.Open(nil, nonce, ciphertext[chacha20poly1305.NonceSizeX:], nil)
|
||||
}
|
||||
|
||||
func parseError(payload []byte) string {
|
||||
if len(payload) < 2 {
|
||||
return "unknown error"
|
||||
}
|
||||
return string(payload[2:])
|
||||
}
|
||||
|
||||
func parseIdentityPayload(data []byte) (string, []byte, []byte) {
|
||||
if len(data) < 3 {
|
||||
return "", nil, nil
|
||||
}
|
||||
off := 0
|
||||
for off < len(data) && data[off] != 0 {
|
||||
off++
|
||||
}
|
||||
if off >= len(data) {
|
||||
return "", nil, nil
|
||||
}
|
||||
number := string(data[:off])
|
||||
off++
|
||||
|
||||
if off+2 > len(data) {
|
||||
return number, nil, nil
|
||||
}
|
||||
certLen := int(binary.BigEndian.Uint16(data[off:]))
|
||||
off += 2
|
||||
if off+certLen > len(data) {
|
||||
return number, nil, nil
|
||||
}
|
||||
certDER := make([]byte, certLen)
|
||||
copy(certDER, data[off:off+certLen])
|
||||
off += certLen
|
||||
|
||||
caCertDER := make([]byte, len(data)-off)
|
||||
copy(caCertDER, data[off:])
|
||||
|
||||
return number, certDER, caCertDER
|
||||
}
|
||||
|
||||
func parseRingPayload(data []byte) (string, [32]byte) {
|
||||
if len(data) < 1 {
|
||||
return "", [32]byte{}
|
||||
}
|
||||
numLen := int(data[0])
|
||||
if len(data) < 1+numLen+32 {
|
||||
return "", [32]byte{}
|
||||
}
|
||||
number := string(data[1 : 1+numLen])
|
||||
var e2eePub [32]byte
|
||||
copy(e2eePub[:], data[1+numLen:1+numLen+32])
|
||||
return number, e2eePub
|
||||
}
|
||||
|
||||
func (c *Client) VerifyCertificate() bool {
|
||||
if len(c.caCertDER) == 0 || len(c.certDER) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
caCert, err := x509.ParseCertificate(c.caCertDER)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(c.certDER)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(caCert)
|
||||
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
}
|
||||
|
||||
_, err = cert.Verify(opts)
|
||||
return err == nil
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue