Implemented server-side authentication in accordance to the protocol

This commit is contained in:
2025-07-06 14:40:32 +03:00
parent e734449259
commit f80cb4a0fe
7 changed files with 142 additions and 124 deletions

View File

@@ -14,7 +14,7 @@ import (
)
func TestPacketEncodingDecoding(t *testing.T) {
testPacketEncodingDecoding(t, &Error{PktType: 0, Error: ""})
testPacketEncodingDecoding(t, &Error{Error: ""})
node := snowflake.NewNode(1)
id := node.Generate()
@@ -59,7 +59,7 @@ func TestPacketFramer(t *testing.T) {
defer cancel()
framer := NewFramer()
pkt := NewPacket(NewJsonEncoder(&Error{PktType: 0, Error: ""}))
pkt := NewPacket(NewJsonEncoder(&Error{Error: ""}))
length := len(pkt.data)
count := 5
data := make([]byte, length*count)

View File

@@ -51,7 +51,7 @@ The server must then respond in one of the following ways:
- With an Error (Type 0) with any message, indicating failed authentication
- With a UsersInfo (Type 6) indicating success
- Must include exactly one UserID (corresponding to the authenticated user)
- Must include exactly one User (corresponding to the authenticated user)
Note: if the client takes too long between the nonce request, the nonce may have been rotated
and the client will need to redo these steps.

View File

@@ -14,6 +14,7 @@ import (
"github.com/kyren223/eko/internal/data"
"github.com/kyren223/eko/internal/packet"
"github.com/kyren223/eko/internal/server/ctxkeys"
"github.com/kyren223/eko/internal/server/session"
"github.com/kyren223/eko/pkg/assert"
"github.com/kyren223/eko/pkg/snowflake"
@@ -393,12 +394,13 @@ func CreateNetwork(ctx context.Context, sess *session.Session, request *packet.C
}
}
func GetNetworksInfo(ctx context.Context, sess *session.Session) (packet.Payload, error) {
func GetNetworksInfo(ctx context.Context, sess *session.Session) packet.Payload {
var fullNetworks []packet.FullNetwork
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return nil, err
slog.ErrorContext(ctx, "database error", "error", err)
return &ErrInternalError
}
defer func() { _ = tx.Rollback() }()
@@ -407,18 +409,21 @@ func GetNetworksInfo(ctx context.Context, sess *session.Session) (packet.Payload
networks, err := qtx.GetUserNetworks(ctx, sess.ID())
if err != nil {
return nil, err
slog.ErrorContext(ctx, "database error", "error", err)
return &ErrInternalError
}
for _, network := range networks {
frequencies, err := qtx.GetNetworkFrequencies(ctx, network.ID)
if err != nil {
return nil, err
slog.ErrorContext(ctx, "database error", "error", err)
return &ErrInternalError
}
membersAndUsers, err := qtx.GetNetworkMembers(ctx, network.ID)
if err != nil {
return nil, err
slog.ErrorContext(ctx, "database error", "error", err)
return &ErrInternalError
}
members, users := SplitMembersAndUsers(membersAndUsers)
@@ -432,14 +437,15 @@ func GetNetworksInfo(ctx context.Context, sess *session.Session) (packet.Payload
err = tx.Commit()
if err != nil {
return nil, err
slog.ErrorContext(ctx, "database error", "error", err)
return &ErrInternalError
}
return &packet.NetworksInfo{
Networks: fullNetworks,
RemovedNetworks: nil,
Partial: false,
}, nil
}
}
func CreateFrequency(ctx context.Context, sess *session.Session, request *packet.CreateFrequency) packet.Payload {
@@ -1565,3 +1571,99 @@ func GetUsers(ctx context.Context, sess *session.Session, request *packet.GetUse
Users: users,
}
}
func GetNonce(ctx context.Context, sess *session.Session, request *packet.GetNonce) packet.Payload {
return &packet.NonceInfo{
Nonce: sess.Challenge(),
}
}
func Authenticate(ctx context.Context, sess *session.Session, request *packet.Authenticate) packet.Payload {
if sess.IsAuthenticated() {
return &packet.Error{Error: "already authenticated"}
}
if len(request.PubKey) != ed25519.PublicKeySize {
return &packet.Error{Error: fmt.Sprintf(
"public key must be exactly %v bytes", ed25519.PublicKeySize,
)}
}
if len(request.Signature) != ed25519.SignatureSize {
return &packet.Error{Error: fmt.Sprintf(
"signature must be exactly %v bytes", ed25519.SignatureSize,
)}
}
// IMPORTANT
if ok := ed25519.Verify(request.PubKey, sess.Challenge(), request.Signature); !ok {
return &packet.Error{Error: "signature verification failed"}
}
// Authenticated from here on out
queries := data.New(db)
user, err := queries.GetUserByPublicKey(ctx, request.PubKey)
if err == sql.ErrNoRows {
id := sess.Manager().Node().Generate()
user, err = queries.CreateUser(ctx, data.CreateUserParams{
ID: id,
Name: "User" + strconv.FormatInt(id.Time()%1000, 10),
PublicKey: request.PubKey,
})
}
if err != nil {
slog.ErrorContext(ctx, "database error", "error", err)
return &ErrInternalError
}
if user.IsDeleted {
return &packet.Error{Error: "public key is already taken by a deleted user"}
}
sess.Manager().AddSession(sess, user.ID, request.PubKey)
// NOTE: as per the protocol, this must be the first message after auth
payload := &packet.UsersInfo{Users: []data.User{user}}
ok := sess.Write(ctx, WrapPayload(payload))
if !ok {
// Timeout, send at least this payload, client can request the rest
return payload
}
ok = sendInitialAuthPackets(ctx, sess) // Send rest of packets
if !ok {
// Timeout, notify the client at least
return &packet.Error{Error: "timeout: not all initial auth packets were sent"}
}
return nil // manually writing requests to control order
}
func sendInitialAuthPackets(ctx context.Context, sess *session.Session) bool {
payloads := []packet.Payload{}
payloads = append(payloads, GetUserData(ctx, sess, &packet.GetUserData{}))
payloads = append(payloads, GetTrustedUsers(ctx, sess))
payloads = append(payloads, GetBlockedUsers(ctx, sess))
payloads = append(payloads, GetBlockedUsers(ctx, sess))
payloads = append(payloads, GetNetworksInfo(ctx, sess))
payloads = append(payloads, GetNotifications(ctx, sess))
success := true
for _, payload := range payloads {
if payload == &ErrInternalError {
success = false
continue
}
slog.InfoContext(ctx, "sending initial auth payload", ctxkeys.Payload.String(), payload, ctxkeys.PayloadType.String(), payload.Type())
ok := sess.Write(ctx, WrapPayload(payload))
if !ok {
success = false
continue
}
}
return success
}

View File

@@ -79,8 +79,7 @@ func NetworkPropagateWithFilter(
context, cancel := context.WithTimeout(context.Background(), timeout)
go func() {
defer cancel()
pkt := packet.NewPacket(packet.NewMsgPackEncoder(payload))
if ok := session.Write(context, pkt); !ok {
if ok := session.Write(context, WrapPayload(payload)); !ok {
log.Println(sess.Addr(), "propagation to", session.Addr(), "failed")
}
}()
@@ -122,8 +121,7 @@ func UserPropagate(
context, cancel := context.WithTimeout(context.Background(), timeout)
go func() {
defer cancel()
pkt := packet.NewPacket(packet.NewMsgPackEncoder(payload))
if ok := session.Write(context, pkt); !ok {
if ok := session.Write(context, WrapPayload(payload)); !ok {
log.Println(sess.Addr(), "propagation to", session.Addr(), "failed")
}
}()
@@ -191,3 +189,7 @@ func getNotifications(ctx context.Context, userId snowflake.ID) (packet.Notifica
}
return items, nil
}
func WrapPayload(payload packet.Payload) packet.Packet {
return packet.NewPacket(packet.NewMsgPackEncoder(payload))
}

View File

@@ -14,8 +14,8 @@ const (
IpAddr
Evicted
EvictedBy
Request
RequestType
Payload
PayloadType
KeyMax
)
@@ -25,8 +25,8 @@ var keyNames = map[key]string{
IpAddr: "ip_addr",
Evicted: "evicted",
EvictedBy: "evicted_by",
Request: "request",
RequestType: "request_type",
Payload: "payload",
PayloadType: "payload_type",
}
func (k key) String() string {

View File

@@ -3,10 +3,8 @@ package server
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/tls"
"errors"
"fmt"
"io"
"log"
"log/slog"
@@ -104,8 +102,7 @@ func EvictSession(sess *session.Session) {
payload := &packet.Error{
Error: "new connection from another location, closing this one",
}
pkt := packet.NewPacket(packet.NewMsgPackEncoder(payload))
sess.Write(ctx, pkt)
sess.Write(ctx, api.WrapPayload(payload))
sess.Close()
}
@@ -240,9 +237,7 @@ func (server *server) handleConnection(conn net.Conn) {
localCtx := context.WithoutCancel(ctx)
for request := range framer.Out {
response := processPacket(localCtx, sess, request)
ok = sess.Write(localCtx, response)
assert.Assert(ok, "context is never done and write will panic")
processPacket(localCtx, sess, request)
}
}()
@@ -269,10 +264,8 @@ func (server *server) handleConnection(conn net.Conn) {
break
}
if err != nil {
payload := packet.Error{Error: err.Error()}
pkt := packet.NewPacket(packet.NewMsgPackEncoder(&payload))
writerWg.Add(1)
sess.Write(ctx, pkt)
sess.Write(ctx, api.WrapPayload(&packet.Error{Error: err.Error()}))
writerWg.Done()
slog.WarnContext(ctx, "received malformed packet", "error", err)
break
@@ -283,45 +276,7 @@ func (server *server) handleConnection(conn net.Conn) {
<-done
}
func handleAuth(conn net.Conn) (ed25519.PublicKey, error) {
nonce := [32]byte{}
_, err := rand.Read(nonce[:])
assert.NoError(err, "random should always produce a value")
challengePacket := make([]byte, len(nonce)+1)
challengePacket[0] = packet.VERSION
copy(challengePacket[1:], nonce[:])
_, err = conn.Write(challengePacket)
if err != nil {
return nil, fmt.Errorf("error writing challenge: %w", err)
}
challengeResponsePacket := make([]byte, ed25519.PublicKeySize+ed25519.SignatureSize+1)
bytesRead := 0
for bytesRead < len(challengeResponsePacket) {
n, err := conn.Read(challengeResponsePacket[bytesRead:])
if err != nil {
return nil, fmt.Errorf("error reading challenge response: %w", err)
}
bytesRead += n
}
if challengeResponsePacket[0] != packet.VERSION {
return nil, fmt.Errorf("incompatible version: %v", challengeResponsePacket[0])
}
pubKey := ed25519.PublicKey(challengeResponsePacket[1 : 1+ed25519.PublicKeySize])
signature := ed25519.PrivateKey(challengeResponsePacket[1+ed25519.PublicKeySize:])
if ok := ed25519.Verify(pubKey, nonce[:], signature); !ok {
return nil, errors.New("signature verification failed")
}
return pubKey, nil
}
func processPacket(ctx context.Context, sess *session.Session, pkt packet.Packet) packet.Packet {
func processPacket(ctx context.Context, sess *session.Session, pkt packet.Packet) {
var response packet.Payload
request, err := pkt.DecodedPayload()
@@ -331,8 +286,11 @@ func processPacket(ctx context.Context, sess *session.Session, pkt packet.Packet
response = processRequest(ctx, sess, request)
}
assert.NotNil(response, "response must always be assigned to")
return packet.NewPacket(packet.NewMsgPackEncoder(response))
// Nil is ok if responses were handled manually using sess.Write()
if response != nil {
ok := sess.Write(ctx, api.WrapPayload(response))
assert.Assert(ok, "context is never done and write will panic if queue is closed")
}
}
func processRequest(ctx context.Context, sess *session.Session, request packet.Payload) packet.Payload {
@@ -371,16 +329,14 @@ func processRequest(ctx context.Context, sess *session.Session, request packet.P
}
switch request := request.(type) {
// TODO:
// case *packet.GetNonce:
// response = timeout(5*time.Millisecond, api.SetUserData, ctx, sess, request)
// TODO:
// case *packet.Authenticate:
// response = timeout(5*time.Millisecond, api.SetUserData, ctx, sess, request)
case *packet.GetNonce:
response = timeout(5*time.Millisecond, api.GetNonce, ctx, sess, request)
case *packet.Authenticate:
response = timeout(5*time.Millisecond, api.Authenticate, ctx, sess, request)
default:
_ = request // FIXME: remove
response = &packet.Error{Error: "use of disallowed packet type for request"}
}
@@ -485,50 +441,5 @@ func sendTosInfo(ctx context.Context, sess *session.Session) bool {
PrivacyPolicy: privacy,
Date: date,
}
pkt := packet.NewPacket(packet.NewMsgPackEncoder(payload))
return sess.Write(ctx, pkt)
}
func (server *server) sendInitialAuthPackets(ctx context.Context, sess *session.Session) bool {
payload := api.GetUserData(ctx, sess, &packet.GetUserData{})
if payload == &api.ErrInternalError {
return false
}
log.Println(sess.Addr(), "sending", payload.Type(), "payload:", payload)
pkt := packet.NewPacket(packet.NewMsgPackEncoder(payload))
sess.Write(ctx, pkt)
payload = api.GetTrustedUsers(ctx, sess)
if payload == &api.ErrInternalError {
return false
}
log.Println(sess.Addr(), "sending", payload.Type(), "payload:", payload)
pkt = packet.NewPacket(packet.NewMsgPackEncoder(payload))
sess.Write(ctx, pkt)
payload = api.GetBlockedUsers(ctx, sess)
if payload == &api.ErrInternalError {
return false
}
log.Println(sess.Addr(), "sending", payload.Type(), "payload:", payload)
pkt = packet.NewPacket(packet.NewMsgPackEncoder(payload))
sess.Write(ctx, pkt)
payload, err := api.GetNetworksInfo(ctx, sess)
if err != nil {
return false
}
log.Println(sess.Addr(), "sending", payload.Type(), "payload:", payload)
pkt = packet.NewPacket(packet.NewMsgPackEncoder(payload))
sess.Write(ctx, pkt)
payload = api.GetNotifications(ctx, sess)
if payload == &api.ErrInternalError {
return false
}
log.Println(sess.Addr(), "sending", payload.Type(), "payload:", payload)
pkt = packet.NewPacket(packet.NewMsgPackEncoder(payload))
sess.Write(ctx, pkt)
return true
return sess.Write(ctx, api.WrapPayload(payload))
}

View File

@@ -13,7 +13,10 @@ import (
"github.com/kyren223/eko/pkg/snowflake"
)
const WriteQueueSize = 10
const (
WriteQueueSize = 10
NonceSize = 32
)
type SessionManager interface {
AddSession(session *Session, userId snowflake.ID, pubKey ed25519.PublicKey)
@@ -58,7 +61,7 @@ func NewSession(
writerWg: writerWg,
writeMu: sync.RWMutex{},
issuedTime: time.Time{},
challenge: make([]byte, 32),
challenge: make([]byte, NonceSize),
pubKey: ed25519.PublicKey{},
id: snowflake.InvalidID,
mu: sync.Mutex{},