From 0d171f1dd766c7b80e5ce379973ff5de48dd2aab Mon Sep 17 00:00:00 2001 From: Polyneices <62021363+cute-rui@users.noreply.github.com> Date: Tue, 30 Jul 2024 22:30:25 +0800 Subject: [PATCH 1/3] feat: bring in argon2id --- backend/config/config.go | 1 + backend/service/account.go | 43 ++++++++- backend/util/argon2id.go | 179 +++++++++++++++++++++++++++++++++++++ backend/util/crypto.go | 1 + 4 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 backend/util/argon2id.go diff --git a/backend/config/config.go b/backend/config/config.go index 85e1a2b..11b9b4a 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -47,6 +47,7 @@ func SetDefault() { viper.SetDefault("mq.redis.hash.post_publish_status", "campux_post_publish_status") viper.SetDefault("mq.redis.prefix.oauth2_code", "campux_oauth2_code") + viper.SetDefault("experimental.password.hash.argon", true) } // 创建配置文件对象 diff --git a/backend/service/account.go b/backend/service/account.go index 45ee01d..c6a046f 100644 --- a/backend/service/account.go +++ b/backend/service/account.go @@ -2,6 +2,7 @@ package service import ( "errors" + "github.com/spf13/viper" "time" "github.com/RockChinQ/Campux/backend/database" @@ -36,9 +37,18 @@ func (as *AccountService) CreateAccount(uin int64) (string, error) { initPwd := util.GenerateRandomPassword() salt := util.GenerateRandomSalt() + var pwdHash string + if viper.GetBool(`experimental.password.hash.argon`) { + pwdHash, err = util.CreateHash(initPwd, util.DefaultParams) + if err != nil { + return "", err + } + } else { + pwdHash = util.EncryptPassword(initPwd, salt) + } acc := &database.AccountPO{ Uin: uin, - Pwd: util.EncryptPassword(initPwd, salt), + Pwd: pwdHash, UserGroup: database.USER_GROUP_USER, Salt: salt, CreatedAt: util.GetCSTTime(), @@ -61,7 +71,15 @@ func (as *AccountService) CheckAccount(uin int64, pwd string) (string, error) { return "", ErrAccountNotFound } - valid := acc.Pwd == util.EncryptPassword(pwd, acc.Salt) + var valid bool + if viper.GetBool(`experimental.password.hash.argon`) { + valid, err = util.ComparePasswordAndHash(pwd, acc.Pwd) + if err != nil { + return "", err + } + } else { + valid = acc.Pwd == util.EncryptPassword(pwd, acc.Salt) + } if !valid { return "", ErrPasswordIncorrect @@ -88,7 +106,16 @@ func (as *AccountService) ResetPassword(uin int64) (string, error) { newPwd := util.GenerateRandomPassword() salt := util.GenerateRandomSalt() - encryptedPwd := util.EncryptPassword(newPwd, salt) + var encryptedPwd string + + if viper.GetBool(`experimental.password.hash.argon`) { + encryptedPwd, err = util.CreateHash(newPwd, util.DefaultParams) + if err != nil { + return "", err + } + } else { + encryptedPwd = util.EncryptPassword(newPwd, salt) + } // 更新密码 err = as.DB.UpdatePassword(uin, encryptedPwd, salt) @@ -110,7 +137,15 @@ func (as *AccountService) ChangePassword(uin int64, newPwd string) error { salt := util.GenerateRandomSalt() - encryptedPwd := util.EncryptPassword(newPwd, salt) + var encryptedPwd string + if viper.GetBool(`experimental.password.hash.argon`) { + encryptedPwd, err = util.CreateHash(newPwd, util.DefaultParams) + if err != nil { + return err + } + } else { + encryptedPwd = util.EncryptPassword(newPwd, salt) + } // 更新密码 err = as.DB.UpdatePassword(uin, encryptedPwd, salt) diff --git a/backend/util/argon2id.go b/backend/util/argon2id.go new file mode 100644 index 0000000..c7d2b0c --- /dev/null +++ b/backend/util/argon2id.go @@ -0,0 +1,179 @@ +package util + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "errors" + "fmt" + "golang.org/x/crypto/argon2" + "strings" +) + +var ( + // ErrInvalidHash in returned by ComparePasswordAndHash if the provided + // hash isn't in the expected format. + ErrInvalidHash = errors.New("argon2id: hash is not in the correct format") + + // ErrIncompatibleVariant is returned by ComparePasswordAndHash if the + // provided hash was created using a unsupported variant of Argon2. + // Currently only argon2id is supported by this package. + ErrIncompatibleVariant = errors.New("argon2id: incompatible variant of argon2") + + // ErrIncompatibleVersion is returned by ComparePasswordAndHash if the + // provided hash was created using a different version of Argon2. + ErrIncompatibleVersion = errors.New("argon2id: incompatible version of argon2") +) + +// DefaultParams provides some sane default parameters for hashing passwords. +// +// Follows recommendations given by the Argon2 RFC: +// "The Argon2id variant with t=1 and maximum available memory is RECOMMENDED as a +// default setting for all environments. This setting is secure against side-channel +// attacks and maximizes adversarial costs on dedicated bruteforce hardware."" +// +// The default parameters should generally be used for development/testing purposes +// only. Custom parameters should be set for production applications depending on +// available memory/CPU resources and business requirements. +var DefaultParams = &Params{ + Memory: 64 * 1024, + Iterations: 1, + Parallelism: 2, + SaltLength: 16, + KeyLength: 32, +} + +// Params describes the input parameters used by the Argon2id algorithm. The +// Memory and Iterations parameters control the computational cost of hashing +// the password. The higher these figures are, the greater the cost of generating +// the hash and the longer the runtime. It also follows that the greater the cost +// will be for any attacker trying to guess the password. If the code is running +// on a machine with multiple cores, then you can decrease the runtime without +// reducing the cost by increasing the Parallelism parameter. This controls the +// number of threads that the work is spread across. Important note: Changing the +// value of the Parallelism parameter changes the hash output. +// +// For guidance and an outline process for choosing appropriate parameters see +// https://tools.ietf.org/html/draft-irtf-cfrg-argon2-04#section-4 +type Params struct { + // The amount of memory used by the algorithm (in kibibytes). + Memory uint32 + + // The number of iterations over the memory. + Iterations uint32 + + // The number of threads (or lanes) used by the algorithm. + // Recommended value is between 1 and runtime.NumCPU(). + Parallelism uint8 + + // Length of the random salt. 16 bytes is recommended for password hashing. + SaltLength uint32 + + // Length of the generated key. 16 bytes or more is recommended. + KeyLength uint32 +} + +// CreateHash returns a Argon2id hash of a plain-text password using the +// provided algorithm parameters. The returned hash follows the format used by +// the Argon2 reference C implementation and contains the base64-encoded Argon2id d +// derived key prefixed by the salt and parameters. It looks like this: +// +// $argon2id$v=19$m=65536,t=3,p=2$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG +func CreateHash(password string, params *Params) (hash string, err error) { + salt, err := generateRandomBytes(params.SaltLength) + if err != nil { + return "", err + } + + key := argon2.IDKey([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLength) + + b64Salt := base64.RawStdEncoding.EncodeToString(salt) + b64Key := base64.RawStdEncoding.EncodeToString(key) + + hash = fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, params.Memory, params.Iterations, params.Parallelism, b64Salt, b64Key) + return hash, nil +} + +// ComparePasswordAndHash performs a constant-time comparison between a +// plain-text password and Argon2id hash, using the parameters and salt +// contained in the hash. It returns true if they match, otherwise it returns +// false. +func ComparePasswordAndHash(password, hash string) (match bool, err error) { + match, _, err = CheckHash(password, hash) + return match, err +} + +// CheckHash is like ComparePasswordAndHash, except it also returns the params that the hash was +// created with. This can be useful if you want to update your hash params over time (which you +// should). +func CheckHash(password, hash string) (match bool, params *Params, err error) { + params, salt, key, err := DecodeHash(hash) + if err != nil { + return false, nil, err + } + + otherKey := argon2.IDKey([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLength) + + keyLen := int32(len(key)) + otherKeyLen := int32(len(otherKey)) + + if subtle.ConstantTimeEq(keyLen, otherKeyLen) == 0 { + return false, params, nil + } + if subtle.ConstantTimeCompare(key, otherKey) == 1 { + return true, params, nil + } + return false, params, nil +} + +func generateRandomBytes(n uint32) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return nil, err + } + + return b, nil +} + +// DecodeHash expects a hash created from this package, and parses it to return the params used to +// create it, as well as the salt and key (password hash). +func DecodeHash(hash string) (params *Params, salt, key []byte, err error) { + vals := strings.Split(hash, "$") + if len(vals) != 6 { + return nil, nil, nil, ErrInvalidHash + } + + if vals[1] != "argon2id" { + return nil, nil, nil, ErrIncompatibleVariant + } + + var version int + _, err = fmt.Sscanf(vals[2], "v=%d", &version) + if err != nil { + return nil, nil, nil, err + } + if version != argon2.Version { + return nil, nil, nil, ErrIncompatibleVersion + } + + params = &Params{} + _, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", ¶ms.Memory, ¶ms.Iterations, ¶ms.Parallelism) + if err != nil { + return nil, nil, nil, err + } + + salt, err = base64.RawStdEncoding.Strict().DecodeString(vals[4]) + if err != nil { + return nil, nil, nil, err + } + params.SaltLength = uint32(len(salt)) + + key, err = base64.RawStdEncoding.Strict().DecodeString(vals[5]) + if err != nil { + return nil, nil, nil, err + } + params.KeyLength = uint32(len(key)) + + return params, salt, key, nil +} diff --git a/backend/util/crypto.go b/backend/util/crypto.go index 28e44f8..2dfc4d9 100644 --- a/backend/util/crypto.go +++ b/backend/util/crypto.go @@ -21,6 +21,7 @@ func GenerateRandomPassword() string { for i := range b { b[i] = letterBytes[rand.Intn(len(letterBytes))] } + return string(b) } From d37c88de3bc4393537cad936b34b727a09f539c0 Mon Sep 17 00:00:00 2001 From: RockChinQ <1010553892@qq.com> Date: Wed, 14 Aug 2024 12:54:09 +0800 Subject: [PATCH 2/3] =?UTF-8?q?doc:=20=E6=B7=BB=E5=8A=A0=E5=A4=87=E6=A1=88?= =?UTF-8?q?=E5=8F=B7=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 8 ++++++++ docs/docs/usage/campux.md | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..8009d73 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,8 @@ +# Campux Docs + +基于 VitePress 构建,调试命令: + +```bash +npm install +npm run docs:dev +``` \ No newline at end of file diff --git a/docs/docs/usage/campux.md b/docs/docs/usage/campux.md index 127e8e6..e9e9c0a 100644 --- a/docs/docs/usage/campux.md +++ b/docs/docs/usage/campux.md @@ -6,7 +6,7 @@ ### beianhao -域名备案号,这个很重要,会显示在投稿按钮下方。 +域名备案号,这个很重要([为什么?](https://help.aliyun.com/zh/icp-filing/support/website-to-add-the-record-number-faq)),会显示在投稿按钮下方。 ### popup_announcement From 2062063811dcd9f9f0fd0dff05345e62bb3c5ba3 Mon Sep 17 00:00:00 2001 From: RockChinQ <1010553892@qq.com> Date: Wed, 14 Aug 2024 12:55:46 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E5=BC=BA=E5=88=B6=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E5=88=B0=20argon2id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config/config.go | 1 - backend/database/mongo.go | 5 ++-- backend/database/po.go | 3 +-- backend/service/account.go | 52 +++++++++++++------------------------- backend/util/crypto.go | 16 ------------ 5 files changed, 20 insertions(+), 57 deletions(-) diff --git a/backend/config/config.go b/backend/config/config.go index 11b9b4a..85e1a2b 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -47,7 +47,6 @@ func SetDefault() { viper.SetDefault("mq.redis.hash.post_publish_status", "campux_post_publish_status") viper.SetDefault("mq.redis.prefix.oauth2_code", "campux_oauth2_code") - viper.SetDefault("experimental.password.hash.argon", true) } // 创建配置文件对象 diff --git a/backend/database/mongo.go b/backend/database/mongo.go index e84f839..53baf44 100644 --- a/backend/database/mongo.go +++ b/backend/database/mongo.go @@ -168,7 +168,7 @@ func (m *MongoDBManager) GetAccountByUIN(uin int64) (*AccountPO, error) { return &acc, nil } -func (m *MongoDBManager) UpdatePassword(uin int64, pwd, salt string) error { +func (m *MongoDBManager) UpdatePassword(uin int64, pwd string) error { // 更新 _, err := m.Client.Database(viper.GetString("database.mongo.db")).Collection(ACCOUNT_COLLECTION).UpdateOne( @@ -178,8 +178,7 @@ func (m *MongoDBManager) UpdatePassword(uin int64, pwd, salt string) error { }, bson.M{ "$set": bson.M{ - "pwd": pwd, - "salt": salt, + "pwd": pwd, }, }, ) diff --git a/backend/database/po.go b/backend/database/po.go index 5502982..c49316a 100644 --- a/backend/database/po.go +++ b/backend/database/po.go @@ -25,8 +25,7 @@ type AccountPO struct { Uin int64 `json:"uin" bson:"uin"` // QQ号 Pwd string `json:"pwd" bson:"pwd"` // 数据库存md5之后的密码 CreatedAt time.Time `json:"created_at" bson:"created_at"` // CST时间 - UserGroup UserGroup `json:"user_group" bson:"user_group"` // 用户组 - Salt string `json:"salt" bson:"salt"` // 加盐 + UserGroup UserGroup `json:"user_group" bson:"user_group"` // 用户 } type AccountExpose struct { diff --git a/backend/service/account.go b/backend/service/account.go index c6a046f..7be68ba 100644 --- a/backend/service/account.go +++ b/backend/service/account.go @@ -2,7 +2,6 @@ package service import ( "errors" - "github.com/spf13/viper" "time" "github.com/RockChinQ/Campux/backend/database" @@ -35,22 +34,16 @@ func (as *AccountService) CreateAccount(uin int64) (string, error) { return "", ErrAccountAlreadyExist } else { initPwd := util.GenerateRandomPassword() - salt := util.GenerateRandomSalt() var pwdHash string - if viper.GetBool(`experimental.password.hash.argon`) { - pwdHash, err = util.CreateHash(initPwd, util.DefaultParams) - if err != nil { - return "", err - } - } else { - pwdHash = util.EncryptPassword(initPwd, salt) + pwdHash, err = util.CreateHash(initPwd, util.DefaultParams) + if err != nil { + return "", err } acc := &database.AccountPO{ Uin: uin, Pwd: pwdHash, UserGroup: database.USER_GROUP_USER, - Salt: salt, CreatedAt: util.GetCSTTime(), } @@ -72,13 +65,13 @@ func (as *AccountService) CheckAccount(uin int64, pwd string) (string, error) { } var valid bool - if viper.GetBool(`experimental.password.hash.argon`) { - valid, err = util.ComparePasswordAndHash(pwd, acc.Pwd) - if err != nil { - return "", err + valid, err = util.ComparePasswordAndHash(pwd, acc.Pwd) + if err != nil { + if err == util.ErrInvalidHash { + return "", errors.New("hash 算法已更改,请重置密码") } - } else { - valid = acc.Pwd == util.EncryptPassword(pwd, acc.Salt) + + return "", err } if !valid { @@ -104,21 +97,16 @@ func (as *AccountService) ResetPassword(uin int64) (string, error) { // 生成新密码 newPwd := util.GenerateRandomPassword() - salt := util.GenerateRandomSalt() var encryptedPwd string - if viper.GetBool(`experimental.password.hash.argon`) { - encryptedPwd, err = util.CreateHash(newPwd, util.DefaultParams) - if err != nil { - return "", err - } - } else { - encryptedPwd = util.EncryptPassword(newPwd, salt) + encryptedPwd, err = util.CreateHash(newPwd, util.DefaultParams) + if err != nil { + return "", err } // 更新密码 - err = as.DB.UpdatePassword(uin, encryptedPwd, salt) + err = as.DB.UpdatePassword(uin, encryptedPwd) return newPwd, err } @@ -135,20 +123,14 @@ func (as *AccountService) ChangePassword(uin int64, newPwd string) error { return ErrAccountNotFound } - salt := util.GenerateRandomSalt() - var encryptedPwd string - if viper.GetBool(`experimental.password.hash.argon`) { - encryptedPwd, err = util.CreateHash(newPwd, util.DefaultParams) - if err != nil { - return err - } - } else { - encryptedPwd = util.EncryptPassword(newPwd, salt) + encryptedPwd, err = util.CreateHash(newPwd, util.DefaultParams) + if err != nil { + return err } // 更新密码 - err = as.DB.UpdatePassword(uin, encryptedPwd, salt) + err = as.DB.UpdatePassword(uin, encryptedPwd) return err } diff --git a/backend/util/crypto.go b/backend/util/crypto.go index 2dfc4d9..80c8691 100644 --- a/backend/util/crypto.go +++ b/backend/util/crypto.go @@ -24,19 +24,3 @@ func GenerateRandomPassword() string { return string(b) } - -// 随机生成一个包含小写字母和数字的字符串,长度为16 -// 用于生成salt -func GenerateRandomSalt() string { - const letterBytes = "abcdefghijklmnopqrstuvwxyz0123456789" - b := make([]byte, 16) - for i := range b { - b[i] = letterBytes[rand.Intn(len(letterBytes))] - } - return string(b) -} - -// 计算密码的md5值 -func EncryptPassword(password, salt string) string { - return MD5(password + salt) -}