Commit 4c0fd012 authored by Juliusz Chroboczek's avatar Juliusz Chroboczek

Implement hashed passwords.

parent c178c28b
*~
data/*.pem
sfu
sfu-password-generator/sfu-password-generator
passwd
groups/*.json
static/*.d.ts
......@@ -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
......
......@@ -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 {
......
......@@ -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
)
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
......
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")
}
}
}
......@@ -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
}
......
......@@ -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{}),
}
......
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)
}
}
}
......@@ -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
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment