This commit is contained in:
parent
3ea935d3c4
commit
aec0d23ac0
@ -55,10 +55,10 @@ func (s *Service) SetSecurityQuestion(userID int, question, answer string) error
|
||||
}
|
||||
question = strings.TrimSpace(norm.NFKC.String(question))
|
||||
answer = normalizeAnswer(answer)
|
||||
if len([]byte(question)) < 2 || len([]byte(question)) > 255 {
|
||||
if len([]rune(question)) < 1 {
|
||||
return ErrInvalidQuestion
|
||||
}
|
||||
if len([]byte(answer)) < 8 || len([]byte(answer)) > 256 {
|
||||
if len([]rune(answer)) < 1 {
|
||||
return ErrInvalidAnswer
|
||||
}
|
||||
hash, err := password.Hash(answer)
|
||||
@ -71,12 +71,8 @@ func (s *Service) SetSecurityQuestion(userID int, question, answer string) error
|
||||
return fmt.Errorf("load security question: %w", err)
|
||||
}
|
||||
if has {
|
||||
version := item.Version + 1
|
||||
if version <= 0 {
|
||||
version = 1
|
||||
}
|
||||
item.Question, item.AnswerHash, item.Version = question, hash, version
|
||||
if _, err := s.engine.ID(item.ID).Cols("question", "answer_hash", "version", "updated_at").Update(item); err != nil {
|
||||
item.Question, item.AnswerHash = question, hash
|
||||
if _, err := s.engine.ID(item.ID).Cols("question", "answer_hash", "updated_at").Update(item); err != nil {
|
||||
return fmt.Errorf("update security question: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -32,6 +32,36 @@ func newFixture(t *testing.T) *Service {
|
||||
return service
|
||||
}
|
||||
|
||||
func TestSecurityQuestionSupportsSingleCharacterAndUnlimited(t *testing.T) {
|
||||
service := newFixture(t)
|
||||
if err := service.SetSecurityQuestion(1, "Q", "A"); err != nil {
|
||||
t.Fatalf("setting 1 char question and answer failed: %v", err)
|
||||
}
|
||||
question, ok, err := service.GetSecurityQuestion(1)
|
||||
if err != nil || !ok || question != "Q" {
|
||||
t.Fatalf("question = %q, ok=%v, err=%v", question, ok, err)
|
||||
}
|
||||
if verified, err := service.VerifySecurityQuestion(1, "A"); err != nil || !verified {
|
||||
t.Fatalf("verification failed: %v, %v", verified, err)
|
||||
}
|
||||
if err := service.SetSecurityQuestion(1, " ", "A"); !errors.Is(err, ErrInvalidQuestion) {
|
||||
t.Fatalf("want ErrInvalidQuestion, got: %v", err)
|
||||
}
|
||||
if err := service.SetSecurityQuestion(1, "Q", " "); !errors.Is(err, ErrInvalidAnswer) {
|
||||
t.Fatalf("want ErrInvalidAnswer, got: %v", err)
|
||||
}
|
||||
if err := service.SetSecurityQuestion(1, "UpdatedQ", "UpdatedA"); err != nil {
|
||||
t.Fatalf("update security question failed: %v", err)
|
||||
}
|
||||
updatedQuestion, ok, err := service.GetSecurityQuestion(1)
|
||||
if err != nil || !ok || updatedQuestion != "UpdatedQ" {
|
||||
t.Fatalf("updated question = %q, ok=%v, err=%v", updatedQuestion, ok, err)
|
||||
}
|
||||
if verified, err := service.VerifySecurityQuestion(1, "UpdatedA"); err != nil || !verified {
|
||||
t.Fatalf("verification of updated answer failed: %v, %v", verified, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityQuestionIsCaseInsensitiveAndNeverReturnsHash(t *testing.T) {
|
||||
service := newFixture(t)
|
||||
if err := service.SetSecurityQuestion(1, "第一只宠物叫什么?", "BlueBird123"); err != nil {
|
||||
|
||||
@ -22,8 +22,7 @@ const (
|
||||
saltLength = 16
|
||||
keyLength = 32
|
||||
maxHashSize = 255
|
||||
minSecretSize = 8
|
||||
maxSecretSize = 256
|
||||
minSecretSize = 1
|
||||
minPasswordSize = 8
|
||||
maxPasswordSize = 12
|
||||
)
|
||||
@ -41,7 +40,7 @@ func ValidateUserPassword(plain string) error {
|
||||
// Hash 生成 Argon2id 密码哈希。
|
||||
func Hash(plain string) (string, error) {
|
||||
plainSize := len([]byte(plain))
|
||||
if plainSize < minSecretSize || plainSize > maxSecretSize {
|
||||
if plainSize < minSecretSize {
|
||||
return "", ErrInvalidLength
|
||||
}
|
||||
salt := make([]byte, saltLength)
|
||||
@ -83,7 +82,7 @@ func HashUserPassword(plain string) (string, error) {
|
||||
|
||||
// Verify 校验 Argon2id 密码哈希,不接受旧算法格式。
|
||||
func Verify(encoded, plain string) bool {
|
||||
if len([]byte(plain)) > maxSecretSize {
|
||||
if len([]byte(plain)) < minSecretSize {
|
||||
return false
|
||||
}
|
||||
parts := strings.Split(encoded, "$")
|
||||
|
||||
@ -16,14 +16,21 @@ func TestHashAndVerify(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashRejectsInvalidLengths(t *testing.T) {
|
||||
for _, plain := range []string{"short", strings.Repeat("a", maxSecretSize+1)} {
|
||||
if _, err := Hash(plain); err == nil {
|
||||
t.Fatalf("invalid secret length was accepted: %d", len([]byte(plain)))
|
||||
func TestHashSupportsSingleCharacterAndLongInputs(t *testing.T) {
|
||||
for _, plain := range []string{"a", "中", strings.Repeat("long-secret-key-123", 50)} {
|
||||
encoded, err := Hash(plain)
|
||||
if err != nil {
|
||||
t.Fatalf("valid secret %q failed: %v", plain, err)
|
||||
}
|
||||
if !Verify(encoded, plain) {
|
||||
t.Fatalf("verification failed for %q", plain)
|
||||
}
|
||||
}
|
||||
if Verify(mustHash(t, "StrongPass123"), strings.Repeat("a", maxSecretSize+1)) {
|
||||
t.Fatal("oversized verification input was accepted")
|
||||
if _, err := Hash(""); !errors.Is(err, ErrInvalidLength) {
|
||||
t.Fatalf("empty secret should be rejected, got: %v", err)
|
||||
}
|
||||
if Verify(mustHash(t, "StrongPass123"), "") {
|
||||
t.Fatal("empty verification input was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -40,8 +40,8 @@
|
||||
<p v-else class="settings-desc">密保问题不能单独重置密码,必须与离线恢复码组合使用。</p>
|
||||
</div>
|
||||
<el-form :model="questionForm" label-position="top" class="settings-form">
|
||||
<el-form-item label="密保问题"><el-input v-model="questionForm.question" maxlength="128" placeholder="例如:我最喜欢的颜色是什么?" /></el-form-item>
|
||||
<el-form-item label="密保答案"><el-input v-model="questionForm.answer" type="password" show-password maxlength="256" placeholder="请输入答案,保存后不会回显" /></el-form-item>
|
||||
<el-form-item label="密保问题"><el-input v-model="questionForm.question" placeholder="例如:我最喜欢的颜色是什么?" /></el-form-item>
|
||||
<el-form-item label="密保答案"><el-input v-model="questionForm.answer" type="password" show-password placeholder="请输入答案,保存后不会回显" /></el-form-item>
|
||||
<div class="form-actions"><el-button type="primary" @click="saveSecurityQuestion">{{ securityQuestion.configured ? '更新密保问题' : '设置密保问题' }}</el-button></div>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user