diff --git a/.gitignore b/.gitignore index 51be839..66147be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *~ data/*.pem sfu +sfu-password-generator/sfu-password-generator passwd groups/*.json static/*.d.ts diff --git a/README b/README index 7f9febf..dd1c216 100644 --- a/README +++ b/README @@ -55,10 +55,10 @@ options are described below. vi groups/public.json { - "public":true, - "op":[{"username":"jch","password":"1234"}], - "presenter":[{}], - "max-users":100 + "public": true, + "op":i [{"username":"jch","password":"1234"}], + "presenter": [{}], + "max-users": 100 } ## Copy the necessary files to your server: @@ -128,23 +128,31 @@ A user definition is a dictionary with the following fields: - `username`: the username of the user; if omitted, any username is allowed; - - `password`: the password of the user; if omitted, then any password - (including the empty paassword) is allowed. + - `password`: if omitted, then no password is required. Otherwise, this + can either be a string, specifying a plain text password, or + a dictionary generated by the `sfu-password-generator` utility. -For example +For example, - {"username":"jch", "password":"topsecret"} + {"username": "jch", "password": "topsecret"} specifies user *jch* with password *topsecret*, while - {"password":"topsecret"} + {"password": "topsecret"} -specifies that any username will do. The empty dictionary - - {} - -specifies that any username will do and that passwords are not verified. +specifies that any username will do. An entry with a hashed password +looks like this: + { + "username": "jch", + "password": { + "type": "pbkdf2", + "hash": "sha-256", + "key": "f591c35604e6aef572851d9c3543c812566b032b6dc083c81edd15cc24449913", + "salt": "92bff2ace56fe38f", + "iterations": 4096 + } + } # Commands diff --git a/diskwriter/diskwriter.go b/diskwriter/diskwriter.go index 03b54b7..889f40b 100644 --- a/diskwriter/diskwriter.go +++ b/diskwriter/diskwriter.go @@ -49,8 +49,12 @@ func (client *Client) Id() string { return client.id } -func (client *Client) Credentials() group.ClientCredentials { - return group.ClientCredentials{"RECORDING", ""} +func (client *Client) Username() string { + return "RECORDING" +} + +func (client *Client) Challenge(group string, cred group.ClientCredentials) bool { + return true } func (client *Client) OverridePermissions(g *group.Group) bool { diff --git a/go.mod b/go.mod index 6a1dd6d..b811d16 100644 --- a/go.mod +++ b/go.mod @@ -9,4 +9,5 @@ require ( github.com/pion/rtcp v1.2.4 github.com/pion/rtp v1.6.1 github.com/pion/webrtc/v3 v3.0.0-beta.7 + golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a ) diff --git a/group/client.go b/group/client.go index 13e291d..d8dd830 100644 --- a/group/client.go +++ b/group/client.go @@ -1,12 +1,83 @@ package group import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "hash" "sfu/conn" + + "golang.org/x/crypto/pbkdf2" ) +type RawPassword struct { + Type string `json:"type,omitempty"` + Hash string `json:"hash,omitempty"` + Key string `json:"key"` + Salt string `json:"salt,omitempty"` + Iterations int `json:"iterations,omitempty"` +} + +type Password RawPassword + +func (p Password) Match(pw string) (bool, error) { + switch p.Type { + case "": + return p.Key == pw, nil + case "pbkdf2": + key, err := hex.DecodeString(p.Key) + if err != nil { + return false, err + } + salt, err := hex.DecodeString(p.Salt) + if err != nil { + return false, err + } + var h func() hash.Hash + switch p.Hash { + case "sha-256": + h = sha256.New + default: + return false, errors.New("unknown hash type") + } + theirKey := pbkdf2.Key( + []byte(pw), salt, p.Iterations, len(key), h, + ) + return bytes.Compare(key, theirKey) == 0, nil + default: + return false, errors.New("unknown password type") + } +} + +func (p *Password) UnmarshalJSON(b []byte) error { + var k string + err := json.Unmarshal(b, &k) + if err == nil { + *p = Password{ + Key: k, + } + return nil + } + var r RawPassword + err = json.Unmarshal(b, &r) + if err == nil { + *p = Password(r) + } + return err +} + +func (p Password) MarshalJSON() ([]byte, error) { + if p.Type == "" && p.Hash == "" && p.Salt == "" && p.Iterations == 0 { + return json.Marshal(p.Key) + } + return json.Marshal(RawPassword(p)) +} + type ClientCredentials struct { - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` + Username string `json:"username,omitempty"` + Password *Password `json:"password,omitempty"` } type ClientPermissions struct { @@ -15,10 +86,15 @@ type ClientPermissions struct { Record bool `json:"record,omitempty"` } +type Challengeable interface { + Username() string + Challenge(string, ClientCredentials) bool +} + type Client interface { Group() *Group Id() string - Credentials() ClientCredentials + Challengeable SetPermissions(ClientPermissions) OverridePermissions(*Group) bool PushConn(id string, conn conn.Up, tracks []conn.UpTrack, label string) error diff --git a/group/client_test.go b/group/client_test.go new file mode 100644 index 0000000..b5fd343 --- /dev/null +++ b/group/client_test.go @@ -0,0 +1,87 @@ +package group + +import ( + "encoding/json" + "log" + "reflect" + "testing" +) + +var pw1 = Password{} +var pw2 = Password{Key: "pass"} +var pw3 = Password{ + Type: "pbkdf2", + Hash: "sha-256", + Key: "fe499504e8f144693fae828e8e371d50e019d0e4c84994fa03f7f445bd8a570a", + Salt: "bcc1717851030776", + Iterations: 4096, +} +var pw4 = Password{ + Type: "bad", +} + +func TestGood(t *testing.T) { + if match, err := pw2.Match("pass"); err != nil || !match { + t.Errorf("pw2 doesn't match (%v)", err) + } + if match, err := pw3.Match("pass"); err != nil || !match { + t.Errorf("pw3 doesn't match (%v)", err) + } +} + +func TestBad(t *testing.T) { + if match, err := pw1.Match("bad"); err != nil || match { + t.Errorf("pw1 matches") + } + if match, err := pw2.Match("bad"); err != nil || match { + t.Errorf("pw2 matches") + } + if match, err := pw3.Match("bad"); err != nil || match { + t.Errorf("pw3 matches") + } + if match, err := pw4.Match("bad"); err == nil || match { + t.Errorf("pw4 matches") + } +} + +func TestJSON(t *testing.T) { + plain, err := json.Marshal(pw2) + if err != nil || string(plain) != `"pass"` { + t.Errorf("Expected \"pass\", got %v", string(plain)) + } + + for _, pw := range []Password{pw1, pw2, pw3, pw4} { + j, err := json.Marshal(pw) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + if testing.Verbose() { + log.Printf("%v", string(j)) + } + var pw2 Password + err = json.Unmarshal(j, &pw2) + if err != nil { + t.Fatalf("Unmarshal: %v", err) + } else if !reflect.DeepEqual(pw, pw2) { + t.Errorf("Expected %v, got %v", pw, pw2) + } + } +} + +func BenchmarkPlain(b *testing.B) { + for i :=0; i < b.N; i++ { + match, err := pw2.Match("bad") + if err != nil || match { + b.Errorf("pw2 matched") + } + } +} + +func BenchmarkPBKDF2(b *testing.B) { + for i :=0; i < b.N; i++ { + match, err := pw3.Match("bad") + if err != nil || match { + b.Errorf("pw3 matched") + } + } +} diff --git a/group/group.go b/group/group.go index f4cc58b..a40d305 100644 --- a/group/group.go +++ b/group/group.go @@ -263,8 +263,8 @@ func delGroupUnlocked(name string) bool { return true } -func AddClient(name string, c Client) (*Group, error) { - g, err := Add(name, nil) +func AddClient(group string, c Client) (*Group, error) { + g, err := Add(group, nil) if err != nil { return nil, err } @@ -273,7 +273,7 @@ func AddClient(name string, c Client) (*Group, error) { defer g.mu.Unlock() if(!c.OverridePermissions(g)) { - perms, err := g.description.GetPermission(c.Credentials()) + perms, err := g.description.GetPermission(group, c) if err != nil { return nil, err } @@ -302,10 +302,10 @@ func AddClient(name string, c Client) (*Group, error) { g.clients[c.Id()] = c go func(clients []Client) { - u := c.Credentials().Username + u := c.Username() c.PushClient(c.Id(), u, true) for _, cc := range clients { - uu := cc.Credentials().Username + uu := cc.Username() c.PushClient(cc.Id(), uu, true) cc.PushClient(c.Id(), u, true) } @@ -330,7 +330,7 @@ func DelClient(c Client) { go func(clients []Client) { for _, cc := range clients { - cc.PushClient(c.Id(), c.Credentials().Username, false) + cc.PushClient(c.Id(), c.Username(), false) } }(g.getClientsUnlocked(nil)) } @@ -453,15 +453,18 @@ func (g *Group) GetChatHistory() []ChatHistoryEntry { return h } -func matchUser(user ClientCredentials, users []ClientCredentials) (bool, bool) { +func matchClient(group string, c Challengeable, users []ClientCredentials) (bool, bool) { for _, u := range users { if u.Username == "" { - if u.Password == "" || u.Password == user.Password { + if c.Challenge(group, u) { return true, true } - } else if u.Username == user.Username { - return true, - (u.Password == "" || u.Password == user.Password) + } else if u.Username == c.Username() { + if c.Challenge(group, u) { + return true, true + } else { + return true, false + } } } return false, false @@ -568,12 +571,12 @@ func GetDescription(name string) (*description, error) { return &desc, nil } -func (desc *description) GetPermission(creds ClientCredentials) (ClientPermissions, error) { +func (desc *description) GetPermission(group string, c Challengeable) (ClientPermissions, error) { var p ClientPermissions - if !desc.AllowAnonymous && creds.Username == "" { + if !desc.AllowAnonymous && c.Username() == "" { return p, UserError("anonymous users not allowed in this group, please choose a username") } - if found, good := matchUser(creds, desc.Op); found { + if found, good := matchClient(group, c, desc.Op); found { if good { p.Op = true p.Present = true @@ -584,14 +587,14 @@ func (desc *description) GetPermission(creds ClientCredentials) (ClientPermissio } return p, UserError("not authorised") } - if found, good := matchUser(creds, desc.Presenter); found { + if found, good := matchClient(group, c, desc.Presenter); found { if good { p.Present = true return p, nil } return p, UserError("not authorised") } - if found, good := matchUser(creds, desc.Other); found { + if found, good := matchClient(group, c, desc.Other); found { if good { return p, nil } diff --git a/rtpconn/webclient.go b/rtpconn/webclient.go index bdd589f..76a3e04 100644 --- a/rtpconn/webclient.go +++ b/rtpconn/webclient.go @@ -44,7 +44,8 @@ func isWSNormalError(err error) bool { type webClient struct { group *group.Group id string - credentials group.ClientCredentials + username string + password string permissions group.ClientPermissions requested map[string]uint32 done chan struct{} @@ -65,8 +66,20 @@ func (c *webClient) Id() string { return c.id } -func (c *webClient) Credentials() group.ClientCredentials { - return c.credentials +func (c *webClient) Username() string { + return c.username +} + +func (c *webClient) Challenge(group string, creds group.ClientCredentials) bool { + if creds.Password == nil { + return true + } + m, err := creds.Password.Match(c.password) + if err != nil { + log.Printf("Password match: %v", err) + return false + } + return m } func (c *webClient) SetPermissions(perms group.ClientPermissions) { @@ -452,7 +465,7 @@ func gotOffer(c *webClient, id string, offer webrtc.SessionDescription, renegoti return err } - if u := c.Credentials().Username; u != "" { + if u := c.Username(); u != "" { up.label = u } err = up.pc.SetRemoteDescription(offer) @@ -645,11 +658,9 @@ func StartClient(conn *websocket.Conn) (err error) { } c := &webClient{ - id: m.Id, - credentials: group.ClientCredentials{ - m.Username, - m.Password, - }, + id: m.Id, + username: m.Username, + password: m.Password, actionCh: make(chan interface{}, 10), done: make(chan struct{}), } diff --git a/sfu-password-generator/sfu-password-generator.go b/sfu-password-generator/sfu-password-generator.go new file mode 100644 index 0000000..fca9138 --- /dev/null +++ b/sfu-password-generator/sfu-password-generator.go @@ -0,0 +1,55 @@ +package main + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "flag" + "log" + "os" + + "golang.org/x/crypto/pbkdf2" + + "sfu/group" +) + +func main() { + var iterations int + var length int + var saltLen int + flag.IntVar(&iterations, "iterations", 4096, "number of iterations") + flag.IntVar(&length, "key length", 32, "key length") + flag.IntVar(&saltLen, "salt", 8, "salt length") + flag.Parse() + + if len(flag.Args()) == 0 { + flag.Usage() + os.Exit(2) + } + + salt := make([]byte, saltLen) + + for _, pw := range flag.Args() { + _, err := rand.Read(salt) + if err != nil { + log.Fatalf("Salt: %v", err) + } + key := pbkdf2.Key( + []byte(pw), salt, iterations, length, sha256.New, + ) + + p := group.Password{ + Type: "pbkdf2", + Hash: "sha-256", + Key: hex.EncodeToString(key), + Salt: hex.EncodeToString(salt), + Iterations: iterations, + } + e := json.NewEncoder(os.Stdout) + err = e.Encode(p) + if err != nil { + log.Fatalf("Encode: %v", err) + } + } +} diff --git a/webserver/webserver.go b/webserver/webserver.go index 198faf2..7bc4244 100644 --- a/webserver/webserver.go +++ b/webserver/webserver.go @@ -526,6 +526,27 @@ func handleGroupAction(w http.ResponseWriter, r *http.Request, group string) { } } +type httpClient struct { + username string + password string +} + +func (c httpClient) Username() string { + return c.username +} + +func (c httpClient) Challenge(group string, creds group.ClientCredentials) bool { + if creds.Password == nil { + return true + } + m, err := creds.Password.Match(c.password) + if err != nil { + log.Printf("Password match: %v", err) + return false + } + return m +} + func checkGroupPermissions(w http.ResponseWriter, r *http.Request, groupname string) bool { desc, err := group.GetDescription(groupname) if err != nil { @@ -537,7 +558,8 @@ func checkGroupPermissions(w http.ResponseWriter, r *http.Request, groupname str return false } - p, err := desc.GetPermission(group.ClientCredentials{user, pass}) + p, err := desc.GetPermission(groupname, httpClient{user, pass}) + if err != nil || !p.Record { return false }