Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: argon2id 和相关迁移内容 #28

Merged
merged 3 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions backend/database/mongo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
},
},
)
Expand Down
3 changes: 1 addition & 2 deletions backend/database/po.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
39 changes: 28 additions & 11 deletions backend/service/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@ func (as *AccountService) CreateAccount(uin int64) (string, error) {
return "", ErrAccountAlreadyExist
} else {
initPwd := util.GenerateRandomPassword()
salt := util.GenerateRandomSalt()

var pwdHash string
pwdHash, err = util.CreateHash(initPwd, util.DefaultParams)
if err != nil {
return "", err
}
acc := &database.AccountPO{
Uin: uin,
Pwd: util.EncryptPassword(initPwd, salt),
Pwd: pwdHash,
UserGroup: database.USER_GROUP_USER,
Salt: salt,
CreatedAt: util.GetCSTTime(),
}

Expand All @@ -61,7 +64,15 @@ func (as *AccountService) CheckAccount(uin int64, pwd string) (string, error) {
return "", ErrAccountNotFound
}

valid := acc.Pwd == util.EncryptPassword(pwd, acc.Salt)
var valid bool
valid, err = util.ComparePasswordAndHash(pwd, acc.Pwd)
if err != nil {
if err == util.ErrInvalidHash {
return "", errors.New("hash 算法已更改,请重置密码")
}

return "", err
}

if !valid {
return "", ErrPasswordIncorrect
Expand All @@ -86,12 +97,16 @@ func (as *AccountService) ResetPassword(uin int64) (string, error) {

// 生成新密码
newPwd := util.GenerateRandomPassword()
salt := util.GenerateRandomSalt()

encryptedPwd := util.EncryptPassword(newPwd, salt)
var encryptedPwd string

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
}
Expand All @@ -108,12 +123,14 @@ func (as *AccountService) ChangePassword(uin int64, newPwd string) error {
return ErrAccountNotFound
}

salt := util.GenerateRandomSalt()

encryptedPwd := util.EncryptPassword(newPwd, salt)
var encryptedPwd string
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
}
Expand Down
179 changes: 179 additions & 0 deletions backend/util/argon2id.go
Original file line number Diff line number Diff line change
@@ -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", &params.Memory, &params.Iterations, &params.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
}
15 changes: 0 additions & 15 deletions backend/util/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,6 @@ func GenerateRandomPassword() string {
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
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)
}
8 changes: 8 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Campux Docs

基于 VitePress 构建,调试命令:

```bash
npm install
npm run docs:dev
```
2 changes: 1 addition & 1 deletion docs/docs/usage/campux.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

### beianhao

域名备案号,这个很重要,会显示在投稿按钮下方。
域名备案号,这个很重要([为什么?](https://help.aliyun.com/zh/icp-filing/support/website-to-add-the-record-number-faq)),会显示在投稿按钮下方。

### popup_announcement

Expand Down