summaryrefslogtreecommitdiff
path: root/srv
diff options
context:
space:
mode:
Diffstat (limited to 'srv')
-rw-r--r--srv/api/api.go9
-rw-r--r--srv/api/chat.go47
-rw-r--r--srv/chat/chat.go11
-rw-r--r--srv/chat/user.go68
-rw-r--r--srv/chat/user_test.go26
-rw-r--r--srv/cmd/userid-calc-cli/main.go27
-rwxr-xr-xsrv/cmd/userid-calc-cli/userid-calc-clibin0 -> 2711927 bytes
-rw-r--r--srv/go.mod1
-rw-r--r--srv/go.sum9
9 files changed, 183 insertions, 15 deletions
diff --git a/srv/api/api.go b/srv/api/api.go
index 15627f8..39d73d9 100644
--- a/srv/api/api.go
+++ b/srv/api/api.go
@@ -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
diff --git a/srv/api/chat.go b/srv/api/chat.go
index 84fffde..55d9d02 100644
--- a/srv/api/chat.go
+++ b/srv/api/chat.go
@@ -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,
+ })
+ })
+}
diff --git a/srv/chat/chat.go b/srv/chat/chat.go
index 44449cd..ae305ac 100644
--- a/srv/chat/chat.go
+++ b/srv/chat/chat.go
@@ -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"`
diff --git a/srv/chat/user.go b/srv/chat/user.go
new file mode 100644
index 0000000..1279a45
--- /dev/null
+++ b/srv/chat/user.go
@@ -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),
+ }
+}
diff --git a/srv/chat/user_test.go b/srv/chat/user_test.go
new file mode 100644
index 0000000..2169cde
--- /dev/null
+++ b/srv/chat/user_test.go
@@ -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))
+}
diff --git a/srv/cmd/userid-calc-cli/main.go b/srv/cmd/userid-calc-cli/main.go
new file mode 100644
index 0000000..c86ae5d
--- /dev/null
+++ b/srv/cmd/userid-calc-cli/main.go
@@ -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))
+}
diff --git a/srv/cmd/userid-calc-cli/userid-calc-cli b/srv/cmd/userid-calc-cli/userid-calc-cli
new file mode 100755
index 0000000..ca18cc3
--- /dev/null
+++ b/srv/cmd/userid-calc-cli/userid-calc-cli
Binary files differ
diff --git a/srv/go.mod b/srv/go.mod
index e2e43f0..8587186 100644
--- a/srv/go.mod
+++ b/srv/go.mod
@@ -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
)
diff --git a/srv/go.sum b/srv/go.sum
index b5b3455..6c96538 100644
--- a/srv/go.sum
+++ b/srv/go.sum
@@ -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=