mirror of
https://github.com/jech/galene.git
synced 2024-11-09 18:25:58 +01:00
Implement hashed passwords.
This commit is contained in:
parent
c178c28b60
commit
4c0fd01258
10 changed files with 313 additions and 45 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
|||
*~
|
||||
data/*.pem
|
||||
sfu
|
||||
sfu-password-generator/sfu-password-generator
|
||||
passwd
|
||||
groups/*.json
|
||||
static/*.d.ts
|
||||
|
|
36
README
36
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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
1
go.mod
1
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
|
||||
)
|
||||
|
|
|
@ -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"`
|
||||
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
|
||||
|
|
87
group/client_test.go
Normal file
87
group/client_test.go
Normal file
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
@ -646,10 +659,8 @@ func StartClient(conn *websocket.Conn) (err error) {
|
|||
|
||||
c := &webClient{
|
||||
id: m.Id,
|
||||
credentials: group.ClientCredentials{
|
||||
m.Username,
|
||||
m.Password,
|
||||
},
|
||||
username: m.Username,
|
||||
password: m.Password,
|
||||
actionCh: make(chan interface{}, 10),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
|
55
sfu-password-generator/sfu-password-generator.go
Normal file
55
sfu-password-generator/sfu-password-generator.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue