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:
Niko Marmeladkov 2026-06-30 13:43:54 +03:00
parent 4f88a7eab1
commit dbb2782dd7
Signed by untrusted user who does not match committer: Niko
GPG key ID: E3B955F9442D44E3
5 changed files with 735 additions and 8 deletions

17
go.mod
View file

@ -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
View file

@ -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=

View file

@ -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
}

View file

@ -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
View 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
}