diff options
Diffstat (limited to 'server')
| -rw-r--r-- | server/passwords/passwords.go | 38 | ||||
| -rw-r--r-- | server/server/command.go | 57 | ||||
| -rw-r--r-- | server/session/session.go | 1 | ||||
| -rw-r--r-- | server/user/command.go | 37 | ||||
| -rw-r--r-- | server/user/user.go | 51 | ||||
| -rw-r--r-- | server/validate/validate.go | 12 |
6 files changed, 185 insertions, 11 deletions
diff --git a/server/passwords/passwords.go b/server/passwords/passwords.go new file mode 100644 index 0000000..0524f24 --- /dev/null +++ b/server/passwords/passwords.go @@ -0,0 +1,38 @@ +package passwords + +import ( + "golang.org/x/crypto/argon2" + "crypto/subtle" + "crypto/rand" + "bytes" + "log" +) + +const version = 0 + +func doHash(ver int, password string, salt []byte) []byte { + return argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32) +} + +func Hash(password string) []byte { + var buf bytes.Buffer + buf.WriteByte(version) + + salt := make([]byte, 32) + _, err := rand.Read(salt) + if err != nil { + log.Fatal("error generating password hash:", err) + } + buf.Write(doHash(version, password, salt)) + buf.Write(salt) + + return buf.Bytes() +} + +func Check(password string, hash []byte) bool { + ver := int(hash[0]) + hashData := hash[1:33] + salt := hash[33:] + check := doHash(ver, password, salt) + return subtle.ConstantTimeCompare(hashData, check) == 1 +} diff --git a/server/server/command.go b/server/server/command.go index 75f3bc0..9c57fc1 100644 --- a/server/server/command.go +++ b/server/server/command.go @@ -11,28 +11,71 @@ func (s *server) SendRequest(r session.Request) { switch (r.Cmd.Kind) { case "auth": - if r.From.UserId != "" { - r.ReplyInvalid() - return - } if len(r.Cmd.Args) != 1 { r.ReplyInvalid() return } auth := r.Cmd.Args[0] switch auth.Kind { + case "anonymous": + if r.From.UserId != "" { + r.ReplyInvalid() + return + } if (auth.Fields[""] == "") { r.ReplyInvalid() return } - user, err := s.userKind.CreateUser(auth.Fields[""]) + u, err := s.userKind.CreateUser(auth.Fields[""]) if err != nil { r.Reply(err.Cmd()) return } - r.Reply(proto.NewCmd("you-are", "", user.InfoFor(r.From.UserId))) - r.From.UserId = user.Id() + r.Reply(proto.NewCmd("you-are", "", u.InfoFor(u.Id()))) + r.From.UserId = u.Id() + r.From.Subscribe(&u.PrivateStream) + + case "password": + var id, password string + for k, v := range auth.Fields { + switch k { + case "": + password = v + case "id": + id = v + default: + r.ReplyInvalid() + return + } + } + if id == "" || password == "" { + r.ReplyInvalid() + return + } + if r.From.UserId != "" && id != r.From.UserId { + r.Reply(proto.Fail{"bad-auth-id", "", nil}.Cmd()) + return + } + + switch u := s.world.GetObject(id).(type) { + case *user.User: + if u.IsAnonymous() { + r.Reply(proto.Fail{"bad-auth-id", "", nil}.Cmd()) + return + } + if u.CheckPassword(password) { + r.Reply(proto.NewCmd("you-are", "", u.InfoFor(u.Id()))) + r.From.UserId = u.Id() + r.From.PasswordAuthed = true + r.From.Subscribe(&u.PrivateStream) + } else { + r.Reply(proto.Fail{"incorrect-password", "", nil}.Cmd()) + } + default: + r.Reply(proto.Fail{"bad-auth-id", "", nil}.Cmd()) + } + default: r.ReplyInvalid() } diff --git a/server/session/session.go b/server/session/session.go index df7ab65..bb6749a 100644 --- a/server/session/session.go +++ b/server/session/session.go @@ -7,6 +7,7 @@ import ( type Session struct { send chan<- proto.Line UserId string + PasswordAuthed bool subscribedTo map[*Stream]bool sendError error } diff --git a/server/user/command.go b/server/user/command.go index 47a3efb..b4c6d5f 100644 --- a/server/user/command.go +++ b/server/user/command.go @@ -51,6 +51,43 @@ func (u *User) SendRequest(r session.Request) { u.Stream.Event(r.Cmd) r.ReplyOk() + case "auth-update": + if r.From.UserId != u.Id() { + r.Reply(proto.Fail{"forbidden", "", nil}.Cmd()) + return + } + if !u.anonymous && !r.From.PasswordAuthed { + r.Reply(proto.Fail{"password-required", "", nil}.Cmd()) + return + } + if len(r.Cmd.Args) != 1 { + r.ReplyInvalid() + return + } + upd := r.Cmd.Args[0] + switch upd.Kind { + case "password": + var password string + for k, v := range upd.Fields { + switch k { + case "": + password = v + default: + r.ReplyInvalid() + } + } + if password == "" { + r.ReplyInvalid() + return + } + u.SetPassword(password) + r.From.PasswordAuthed = true + r.ReplyOk() + default: + r.ReplyInvalid() + return + } + case "i": r.Reply(proto.NewCmd("i", "", u.InfoFor(r.From.UserId))) diff --git a/server/user/user.go b/server/user/user.go index 311a1b8..d2d5724 100644 --- a/server/user/user.go +++ b/server/user/user.go @@ -4,6 +4,7 @@ import ( "citrons.xyz/talk/server/object" "citrons.xyz/talk/server/session" "citrons.xyz/talk/server/validate" + "citrons.xyz/talk/server/passwords" "citrons.xyz/talk/proto" bolt "go.etcd.io/bbolt" "log" @@ -30,9 +31,16 @@ func Kind(world *object.World) *UserKind { } func (us *UserKind) CreateUser(name string) (*User, *proto.Fail) { - if us.ByName(name) != nil { + existing := us.ByName(name) + if existing != nil { + anon := "no" + if existing.anonymous { + anon = "yes" + } return nil, &proto.Fail { - "name-taken", "", map[string]string {"": name}, + "name-taken", "", map[string]string { + "": name, "anonymous": anon, "id": existing.Id(), + }, } } if !validate.Name(name) { @@ -157,6 +165,8 @@ func (u *User) Delete() { anons.Delete([]byte(u.id)) channels, _ := tx.CreateBucketIfNotExists([]byte("user channels")) channels.DeleteBucket([]byte(u.id)) + auth, _ := tx.CreateBucketIfNotExists([]byte("auth")) + auth.DeleteBucket([]byte(u.id)) return nil }) if err != nil { @@ -180,3 +190,40 @@ func (u *User) InfoFor(uid string) proto.Object { func (u *User) IsAnonymous() bool { return u.anonymous } + +func (u *User) CheckPassword(password string) bool { + var hash []byte + err := u.kind.db.View(func(tx *bolt.Tx) error { + auth := tx.Bucket([]byte("auth")) + if auth == nil { + return nil + } + userData := auth.Bucket([]byte(u.id)) + if userData == nil { + return nil + } + hash = userData.Get([]byte("password hash")) + return nil + }) + if err != nil { + log.Fatal("error reading database: ", err) + } + if hash == nil { + return false + } + return passwords.Check(password, hash) +} + +func (u *User) SetPassword(password string) { + err := u.kind.db.Update(func(tx *bolt.Tx) error { + auth, _ := tx.CreateBucketIfNotExists([]byte("auth")) + userData, _ := auth.CreateBucketIfNotExists([]byte(u.id)) + userData.Put([]byte("password hash"), passwords.Hash(password)) + return nil + }) + if err != nil { + log.Fatal("error updating database: ", err) + } + u.anonymous = false + u.Save() +} diff --git a/server/validate/validate.go b/server/validate/validate.go index 7aa7db0..7251895 100644 --- a/server/validate/validate.go +++ b/server/validate/validate.go @@ -21,12 +21,20 @@ func Fold(s string) string { var sb strings.Builder var wasSpace bool for _, r := range s { - for r < unicode.SimpleFold(r) { - r = unicode.SimpleFold(r) + for { + f := unicode.SimpleFold(r) + if f <= r { + r = f + break + } + r = f } + r = unicode.ToLower(r) + if !unicode.IsPrint(r) { continue } + if r == ' ' { if wasSpace { continue |
