carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// Package relay is the platform STUN/TURN relay: the app-facing credential
2// contract (spec §2), key derivation (§4), and the server (§3).
3package relay
4
5import (
6 "crypto/hmac"
7 "crypto/sha256"
8 "encoding/base64"
9 "fmt"
10 "io"
11 "regexp"
12 "strconv"
13 "strings"
14 "time"
15
16 "golang.org/x/crypto/hkdf"
17)
18
19const (
20 MaxCredentialLife = 4 * time.Hour
21 hkdfSalt = "carlos-relay-v1"
22)
23
24var (
25 expiryRe = regexp.MustCompile(`^[0-9]+$`)
26 accountRe = regexp.MustCompile(`^[a-z0-9]+$`)
27 appRe = regexp.MustCompile(`^[a-z][a-z0-9-]{0,31}$`)
28 incarnationRe = regexp.MustCompile(`^[0-9a-f]{16}$`)
29 kidRe = regexp.MustCompile(`^[a-z0-9]{1,8}$`)
30)
31
32// DeriveKey is HKDF-SHA256(master, salt "carlos-relay-v1",
33// info account||0x00||app||0x00||incarnation, 32).
34func DeriveKey(master []byte, account, app, incarnation string) []byte {
35 info := []byte(account + "\x00" + app + "\x00" + incarnation)
36 r := hkdf.New(sha256.New, master, []byte(hkdfSalt), info)
37 key := make([]byte, 32)
38 if _, err := io.ReadFull(r, key); err != nil {
39 panic("relay: hkdf: " + err.Error()) // cannot fail for 32 bytes
40 }
41 return key
42}
43
44// ValidKid reports whether s is usable as a key id: 1-8 lowercase letters or
45// digits, the same alphabet ParseUsername and ParseMasters hold a kid to.
46// Exported so a WRITER of masters — `carlos relay keygen`, and the console
47// parsing its own CARLOS_RELAY_MASTERS_<REGION> — refuses a typo where the
48// operator can still fix it, instead of minting usernames every relay
49// silently declines.
50func ValidKid(s string) bool { return kidRe.MatchString(s) }
51
52func Suffix(account, app, incarnation, kid string) string {
53 return account + ":" + app + ":" + incarnation + ":" + kid
54}
55
56// Credential is base64(HMAC-SHA256(key, username)), the TURN password.
57func Credential(key []byte, username string) string {
58 h := hmac.New(sha256.New, key)
59 h.Write([]byte(username))
60 return base64.StdEncoding.EncodeToString(h.Sum(nil))
61}
62
63type Username struct {
64 Expiry time.Time
65 Account, App, Incarnation, Kid string
66}
67
68// ParseUsername splits "<expiry>:<account>:<app>:<incarnation>:<kid>" and
69// validates each field's alphabet. It does not check expiry against now;
70// the auth handler does that with its own clock.
71func ParseUsername(s string) (Username, error) {
72 f := strings.Split(s, ":")
73 if len(f) != 5 {
74 return Username{}, fmt.Errorf("relay: username has %d fields, want 5", len(f))
75 }
76 if !expiryRe.MatchString(f[0]) {
77 return Username{}, fmt.Errorf("relay: bad expiry %q", f[0])
78 }
79 exp, err := strconv.ParseInt(f[0], 10, 64)
80 if err != nil {
81 return Username{}, fmt.Errorf("relay: bad expiry %q", f[0])
82 }
83 switch {
84 case !accountRe.MatchString(f[1]):
85 return Username{}, fmt.Errorf("relay: bad account %q", f[1])
86 case !appRe.MatchString(f[2]):
87 return Username{}, fmt.Errorf("relay: bad app %q", f[2])
88 case !incarnationRe.MatchString(f[3]):
89 return Username{}, fmt.Errorf("relay: bad incarnation %q", f[3])
90 case !kidRe.MatchString(f[4]):
91 return Username{}, fmt.Errorf("relay: bad kid %q", f[4])
92 }
93 return Username{Expiry: time.Unix(exp, 0).UTC(), Account: f[1], App: f[2], Incarnation: f[3], Kid: f[4]}, nil
94}
95