summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authorraven <citrons@mondecitronne.com>2025-10-22 16:28:22 -0500
committerraven <citrons@mondecitronne.com>2026-02-09 13:13:55 -0600
commitff0f6ba724748dbe340187fdd831a4d4f7f0ae5e (patch)
treeab27a0c9f40d803c2bf130ef7c82cdf6540e9018 /server
parent4b54a1d11fd0fa355b244637612a3fd0af18c60c (diff)
passwords
Diffstat (limited to 'server')
-rw-r--r--server/passwords/passwords.go38
-rw-r--r--server/server/command.go57
-rw-r--r--server/session/session.go1
-rw-r--r--server/user/command.go37
-rw-r--r--server/user/user.go51
-rw-r--r--server/validate/validate.go12
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