Skip to content

Commit

Permalink
Merge pull request #4 from xconnio/joiner
Browse files Browse the repository at this point in the history
Client: Add authenticators and session joiner
  • Loading branch information
om26er authored May 25, 2024
2 parents aba0b69 + 415e1ed commit f5d660b
Show file tree
Hide file tree
Showing 10 changed files with 487 additions and 0 deletions.
41 changes: 41 additions & 0 deletions auth/anonymous.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package auth

import (
"errors"

"github.com/xconnio/wampproto-go/messages"
)

const MethodAnonymous = "anonymous"

type anonymousAuthenticator struct {
authID string
authExtra map[string]any
}

func NewAnonymousAuthenticator(authID string, authExtra map[string]any) ClientAuthenticator {
if authExtra == nil {
authExtra = map[string]any{}
}

return &anonymousAuthenticator{
authID: authID,
authExtra: authExtra,
}
}

func (a *anonymousAuthenticator) AuthMethod() string {
return MethodAnonymous
}

func (a *anonymousAuthenticator) AuthID() string {
return a.authID
}

func (a *anonymousAuthenticator) AuthExtra() map[string]any {
return a.authExtra
}

func (a *anonymousAuthenticator) Authenticate(_ messages.Challenge) (messages.Authenticate, error) {
return nil, errors.New("func Authenticate() must not be called for anonymous authentication")
}
10 changes: 10 additions & 0 deletions auth/authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package auth

import "github.com/xconnio/wampproto-go/messages"

type ClientAuthenticator interface {
AuthMethod() string
AuthID() string
AuthExtra() map[string]any
Authenticate(challenge messages.Challenge) (messages.Authenticate, error)
}
97 changes: 97 additions & 0 deletions auth/cra.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package auth

import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"

"golang.org/x/crypto/pbkdf2"

"github.com/xconnio/wampproto-go/messages"
)

const MethodCRA = "wampcra"

type craAuthenticator struct {
authID string
authExtra map[string]any

secret string
}

func NewCRAAuthenticator(authID string, authExtra map[string]any, secret string) ClientAuthenticator {
return &craAuthenticator{
authID: authID,
authExtra: authExtra,
secret: secret,
}
}

func (a *craAuthenticator) AuthMethod() string {
return MethodCRA
}

func (a *craAuthenticator) AuthID() string {
return a.authID
}

func (a *craAuthenticator) AuthExtra() map[string]any {
return a.authExtra
}

func (a *craAuthenticator) Authenticate(challenge messages.Challenge) (messages.Authenticate, error) {
ch, _ := challenge.Extra()["challenge"].(string)
// If the client needed to look up a user's key, this would require decoding
// the JSON-encoded challenge string and getting the authid. For this
// example assume that client only operates as one user and knows the key
// to use.

var rawSecret []byte
saltStr, _ := challenge.Extra()["salt"].(string)
// If no salt given, use raw password as key.
if saltStr != "" {
// If salting info give, then compute a derived key using PBKDF2.
iters, _ := messages.AsInt64(challenge.Extra()["iterations"])
keylen, _ := messages.AsInt64(challenge.Extra()["keylen"])

rawSecret = DeriveCRAKey(saltStr, a.secret, int(iters), int(keylen))
} else {
rawSecret = []byte(a.secret)
}

challengeStr := SignCRAChallenge(ch, rawSecret)
return messages.NewAuthenticate(challengeStr, map[string]any{}), nil
}

// SignCRAChallengeBytes computes the HMAC-SHA256, using the given key, over the
// challenge string, and returns the result.
func SignCRAChallengeBytes(ch string, key []byte) []byte {
sig := hmac.New(sha256.New, key)
sig.Write([]byte(ch))
return sig.Sum(nil)
}

// SignCRAChallenge computes the HMAC-SHA256, using the given key, over the
// challenge string, and returns the result as a base64-encoded string.
func SignCRAChallenge(ch string, key []byte) string {
return base64.StdEncoding.EncodeToString(SignCRAChallengeBytes(ch, key))
}

func DeriveCRAKey(saltStr string, secret string, iterations int, keyLength int) []byte {
// If salting info give, then compute a derived key using PBKDF2.
salt := []byte(saltStr)

if iterations == 0 {
iterations = 1000
}
if keyLength == 0 {
keyLength = 32
}

// Compute derived key.
dk := pbkdf2.Key([]byte(secret), salt, iterations, keyLength, sha256.New)
// Get base64 bytes. see https://github.com/gammazero/nexus/issues/252
derivedKey := []byte(base64.StdEncoding.EncodeToString(dk))

return derivedKey
}
80 changes: 80 additions & 0 deletions auth/cryptosign.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package auth

import (
"crypto/ed25519"
"encoding/hex"
"errors"
"fmt"

"github.com/xconnio/wampproto-go/messages"
)

const MethodCryptoSign = "cryptosign"

type cryptoSignAuthenticator struct {
authID string
authExtra map[string]any

privateKey ed25519.PrivateKey
}

func NewCryptoSignAuthenticator(authID string, authExtra map[string]any,
privateKeyHex string) (ClientAuthenticator, error) {

privateKeyRaw, err := hex.DecodeString(privateKeyHex)
if err != nil {
return nil, errors.New("invalid private key")
}

privateKey := ed25519.NewKeyFromSeed(privateKeyRaw)
publicKey := privateKey.Public().(ed25519.PublicKey)
publicKeyHex := hex.EncodeToString(publicKey)

if authExtra == nil {
authExtra = map[string]any{"pubkey": publicKeyHex}
} else if val, ok := authExtra["pubkey"].(string); ok {
if val != publicKeyHex {
return nil, errors.New("provided pubkey does not correspond to the private key")
}
}

return &cryptoSignAuthenticator{
authID: authID,
authExtra: authExtra,
privateKey: privateKey,
}, nil
}

func (a *cryptoSignAuthenticator) AuthMethod() string {
return MethodCryptoSign
}

func (a *cryptoSignAuthenticator) AuthID() string {
return a.authID
}

func (a *cryptoSignAuthenticator) AuthExtra() map[string]any {
return a.authExtra
}

func (a *cryptoSignAuthenticator) Authenticate(challenge messages.Challenge) (messages.Authenticate, error) {
challengeHex, _ := challenge.Extra()["challenge"].(string)
result, err := SignCryptoSignChallenge(challengeHex, a.privateKey)
if err != nil {
return nil, fmt.Errorf("failed to sign challenge")
}

return messages.NewAuthenticate(result, map[string]any{}), nil
}

func SignCryptoSignChallenge(challenge string, privateKey ed25519.PrivateKey) (string, error) {
challengeRaw, err := hex.DecodeString(challenge)
if err != nil {
return "", fmt.Errorf("failed to decode challenge: %w", err)
}

signedRaw := ed25519.Sign(privateKey, challengeRaw)
signedHex := hex.EncodeToString(signedRaw)

return signedHex + challenge, nil
}
38 changes: 38 additions & 0 deletions auth/ticket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package auth

import (
"github.com/xconnio/wampproto-go/messages"
)

const MethodTicket = "ticket"

type ticketAuthenticator struct {
authID string
authExtra map[string]any

ticket string
}

func NewTicketAuthenticator(authID string, authExtra map[string]any, ticket string) ClientAuthenticator {
return &ticketAuthenticator{
authID: authID,
authExtra: authExtra,
ticket: ticket,
}
}

func (a *ticketAuthenticator) AuthMethod() string {
return MethodTicket
}

func (a *ticketAuthenticator) AuthID() string {
return a.authID
}

func (a *ticketAuthenticator) AuthExtra() map[string]any {
return a.authExtra
}

func (a *ticketAuthenticator) Authenticate(_ messages.Challenge) (messages.Authenticate, error) {
return messages.NewAuthenticate(a.ticket, map[string]any{}), nil
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ require (
github.com/fxamacker/cbor/v2 v2.6.0
github.com/stretchr/testify v1.6.1
github.com/vmihailenco/msgpack/v5 v5.4.1
golang.org/x/crypto v0.23.0
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d
)

require (
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4=
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
Expand Down
37 changes: 37 additions & 0 deletions idgen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package wampproto

import (
"sync"
"time"

"golang.org/x/exp/rand"
)

const maxID int64 = 1 << 53

func init() {
source := rand.NewSource(uint64(time.Now().UnixNano()))
rand.New(source)
}

// GenerateID generates a random WAMP ID.
func GenerateID() int64 {
return rand.Int63n(maxID)
}

type SessionScopeIDGenerator struct {
id int64
sync.Mutex
}

func (s *SessionScopeIDGenerator) NextID() int64 {
s.Lock()
defer s.Unlock()

if s.id == maxID {
s.id = 0
}

s.id++
return s.id
}
Loading

0 comments on commit f5d660b

Please sign in to comment.