implemented basic userID generation

pull/15/head
Brian Picciano 3 years ago
parent bec64827d1
commit a9d8aa2591
  1. 9
      srv/api/api.go
  2. 47
      srv/api/chat.go
  3. 11
      srv/chat/chat.go
  4. 68
      srv/chat/user.go
  5. 26
      srv/chat/user_test.go
  6. 27
      srv/cmd/userid-calc-cli/main.go
  7. BIN
      srv/cmd/userid-calc-cli/userid-calc-cli
  8. 1
      srv/go.mod
  9. 9
      srv/go.sum

@ -22,10 +22,11 @@ import (
// Params are used to instantiate a new API instance. All fields are required
// unless otherwise noted.
type Params struct {
Logger *mlog.Logger
PowManager pow.Manager
MailingList mailinglist.MailingList
GlobalRoom chat.Room
Logger *mlog.Logger
PowManager pow.Manager
MailingList mailinglist.MailingList
GlobalRoom chat.Room
UserIDCalculator chat.UserIDCalculator
// ListenProto and ListenAddr are passed into net.Listen to create the
// API's listener. Both "tcp" and "unix" protocols are explicitly

@ -4,6 +4,8 @@ import (
"errors"
"fmt"
"net/http"
"strings"
"unicode"
"github.com/mediocregopher/blog.mediocregopher.com/srv/chat"
)
@ -39,3 +41,48 @@ func (a *api) chatHistoryHandler() http.Handler {
})
})
}
func (a *api) getUserID(r *http.Request) (chat.UserID, error) {
name := r.PostFormValue("name")
if l := len(name); l == 0 {
return chat.UserID{}, errors.New("name is required")
} else if l > 16 {
return chat.UserID{}, errors.New("name too long")
}
nameClean := strings.Map(func(r rune) rune {
if !unicode.IsPrint(r) {
return -1
}
return r
}, name)
if nameClean != name {
return chat.UserID{}, errors.New("name contains invalid characters")
}
password := r.PostFormValue("password")
if l := len(password); l == 0 {
return chat.UserID{}, errors.New("password is required")
} else if l > 128 {
return chat.UserID{}, errors.New("password too long")
}
return a.params.UserIDCalculator.Calculate(name, password), nil
}
func (a *api) getUserIDHandler() http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
userID, err := a.getUserID(r)
if err != nil {
badRequest(rw, r, err)
return
}
jsonResult(rw, r, struct {
UserID chat.UserID `json:"userID"`
}{
UserID: userID,
})
})
}

@ -29,17 +29,6 @@ var (
errInvalidMessageID = ErrInvalidArg{Err: errors.New("invalid Message ID")}
)
// UserID uniquely identifies an individual user who has posted a message in a
// Room.
type UserID struct {
// Name will be the user's chosen display name.
Name string `json:"name"`
// Hash will be a hex string generated from a secret only the user knows.
Hash string `json:"id"`
}
// Message describes a message which has been posted to a Room.
type Message struct {
ID string `json:"id"`

@ -0,0 +1,68 @@
package chat
import (
"encoding/hex"
"fmt"
"sync"
"golang.org/x/crypto/argon2"
)
// UserID uniquely identifies an individual user who has posted a message in a
// Room.
type UserID struct {
// Name will be the user's chosen display name.
Name string `json:"name"`
// Hash will be a hex string generated from a secret only the user knows.
Hash string `json:"id"`
}
// UserIDCalculator is used to calculate UserIDs.
type UserIDCalculator struct {
// Secret is used when calculating UserID Hash salts.
Secret []byte
// TimeCost, MemoryCost, and Threads are used as inputs to the Argon2id
// algorithm which is used to generate the Hash.
TimeCost, MemoryCost uint32
Threads uint8
// HashLen specifies the number of bytes the Hash should be.
HashLen uint32
// Lock, if set, forces concurrent Calculate calls to occur sequentially.
Lock *sync.Mutex
}
// NewUserIDCalculator returns a UserIDCalculator with sane defaults.
func NewUserIDCalculator(secret []byte) UserIDCalculator {
return UserIDCalculator{
Secret: secret,
TimeCost: 15,
MemoryCost: 128 * 1024,
Threads: 2,
HashLen: 16,
Lock: new(sync.Mutex),
}
}
// Calculate accepts a name and password and returns the calculated UserID.
func (c UserIDCalculator) Calculate(name, password string) UserID {
input := fmt.Sprintf("%q:%q", name, password)
hashB := argon2.IDKey(
[]byte(input),
c.Secret, // salt
c.TimeCost, c.MemoryCost, c.Threads,
c.HashLen,
)
return UserID{
Name: name,
Hash: hex.EncodeToString(hashB),
}
}

@ -0,0 +1,26 @@
package chat
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUserIDCalculator(t *testing.T) {
const name, password = "name", "password"
c := NewUserIDCalculator([]byte("foo"))
// calculating with same params twice should result in same UserID
userID := c.Calculate(name, password)
assert.Equal(t, userID, c.Calculate(name, password))
// changing either name or password should result in a different Hash
assert.NotEqual(t, userID.Hash, c.Calculate(name+"!", password).Hash)
assert.NotEqual(t, userID.Hash, c.Calculate(name, password+"!").Hash)
// changing the secret should change the UserID
c.Secret = []byte("bar")
assert.NotEqual(t, userID, c.Calculate(name, password))
}

@ -0,0 +1,27 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"github.com/mediocregopher/blog.mediocregopher.com/srv/chat"
)
func main() {
secret := flag.String("secret", "", "Secret to use when calculating UserIDs")
name := flag.String("name", "", "")
password := flag.String("password", "", "")
flag.Parse()
calc := chat.NewUserIDCalculator([]byte(*secret))
userID := calc.Calculate(*name, *password)
b, err := json.Marshal(userID)
if err != nil {
panic(err)
}
fmt.Println(string(b))
}

@ -13,4 +13,5 @@ require (
github.com/stretchr/testify v1.7.0
github.com/tilinna/clock v1.1.0
github.com/ziutek/mymysql v1.5.4 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
)

@ -176,6 +176,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@ -187,6 +189,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -201,9 +204,15 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=

Loading…
Cancel
Save