Skip to content

Commit

Permalink
Merge pull request #28 from idoknow/refactor/argon2id-migration
Browse files Browse the repository at this point in the history
Feat: argon2id 和相关迁移内容
  • Loading branch information
RockChinQ authored Aug 14, 2024
2 parents 4ed2407 + 2062063 commit 0ed2557
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 32 deletions.
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

0 comments on commit 0ed2557

Please sign in to comment.