Skip to content

Commit 986292f

Browse files
authored
Merge pull request #21 from thevilledev/fix/workfactor-clone
fix: snapshot workfactor on credential creation
2 parents 33f397d + 5d96def commit 986292f

File tree

2 files changed

+98
-8
lines changed

2 files changed

+98
-8
lines changed

config.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ type WorkFactor interface {
2727
Unmarshal([]int) error
2828
}
2929

30+
// cloneWorkFactor returns an independent copy of the provided WorkFactor
31+
func cloneWorkFactor(wf WorkFactor) (WorkFactor, error) {
32+
switch v := wf.(type) {
33+
case *Pbkdf2WorkFactor:
34+
c := *v
35+
return &c, nil
36+
case *BcryptWorkFactor:
37+
c := *v
38+
return &c, nil
39+
case *ScryptWorkFactor:
40+
c := *v
41+
return &c, nil
42+
default:
43+
return nil, fmt.Errorf("unsupported WorkFactor type: %T", wf)
44+
}
45+
}
46+
3047
// WorkFactorsEqual determines if 2 WorkFactors are equivalent
3148
func WorkFactorsEqual(a, b WorkFactor) bool {
3249
if reflect.TypeOf(a) != reflect.TypeOf(b) {
@@ -135,5 +152,9 @@ func (c Config) NewCredential(userID UserID, password string) (*Credential, erro
135152
if err != nil {
136153
return nil, err
137154
}
138-
return &Credential{UserID: userID, Kdf: c.Kdf, WorkFactor: c.WorkFactor, Salt: salt, Hash: hash}, nil
155+
wfCopy, err := cloneWorkFactor(c.WorkFactor)
156+
if err != nil {
157+
return nil, err
158+
}
159+
return &Credential{UserID: userID, Kdf: c.Kdf, WorkFactor: wfCopy, Salt: salt, Hash: hash}, nil
139160
}

credential_test.go

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import (
44
_ "crypto/sha256"
55
_ "crypto/sha512"
66
"testing"
7-
)
87

9-
import (
8+
"golang.org/x/crypto/bcrypt"
9+
1010
"github.com/dhui/passhash"
1111
)
1212

@@ -180,7 +180,7 @@ func TestMatchesPasswordMatchNoUpdate(t *testing.T) {
180180
if credential.Kdf != origKdf {
181181
t.Errorf("Original credential Kdf changed. %v != %v", credential.Kdf, origKdf)
182182
}
183-
if credential.WorkFactor != origWorkFactor {
183+
if !passhash.WorkFactorsEqual(credential.WorkFactor, origWorkFactor) {
184184
t.Errorf("Original credential WorkFactor changed. %v != %v", credential.WorkFactor, origWorkFactor)
185185
}
186186

@@ -195,7 +195,7 @@ func TestMatchesPasswordMatchNoUpdate(t *testing.T) {
195195
t.Errorf("Credential Kdf unexpectedly updated from safe recommended Kdf. %v != %v", credential.Kdf,
196196
passhash.DefaultConfig.Kdf)
197197
}
198-
if credential.WorkFactor != passhash.DefaultWorkFactor[passhash.DefaultConfig.Kdf] {
198+
if !passhash.WorkFactorsEqual(credential.WorkFactor, passhash.DefaultWorkFactor[passhash.DefaultConfig.Kdf]) {
199199
t.Errorf("Credential WorkFactor unexpected updated from safe recommended Kdf WorkFactor. %v != %v",
200200
credential.WorkFactor, passhash.DefaultWorkFactor[passhash.DefaultConfig.Kdf])
201201
}
@@ -223,7 +223,7 @@ func TestMatchesPasswordUpdateKdfAndWorkFactor(t *testing.T) {
223223
if credential.Kdf != origKdf {
224224
t.Errorf("Original credential Kdf changed. %v != %v", credential.Kdf, origKdf)
225225
}
226-
if credential.WorkFactor != origWorkFactor {
226+
if !passhash.WorkFactorsEqual(credential.WorkFactor, origWorkFactor) {
227227
t.Errorf("Original credential WorkFactor changed. %v != %v", credential.WorkFactor, origWorkFactor)
228228
}
229229

@@ -238,7 +238,7 @@ func TestMatchesPasswordUpdateKdfAndWorkFactor(t *testing.T) {
238238
t.Errorf("Updated credential Kdf did not update to safe recommended Kdf. %v != %v", credential.Kdf,
239239
passhash.DefaultConfig.Kdf)
240240
}
241-
if credential.WorkFactor != passhash.DefaultWorkFactor[passhash.DefaultConfig.Kdf] {
241+
if !passhash.WorkFactorsEqual(credential.WorkFactor, passhash.DefaultWorkFactor[passhash.DefaultConfig.Kdf]) {
242242
t.Errorf("Updated credential WorkFactor did not update to safe recommended Kdf WorkFactor. %v != %v",
243243
credential.WorkFactor, passhash.DefaultWorkFactor[passhash.DefaultConfig.Kdf])
244244
}
@@ -278,12 +278,81 @@ func TestMatchesPasswordUpdateWorkFactor(t *testing.T) {
278278
if credential.Kdf != kdf {
279279
t.Errorf("Updated credential Kdf changed. %v != %v", credential.Kdf, kdf)
280280
}
281-
if credential.WorkFactor != passhash.DefaultWorkFactor[passhash.DefaultConfig.Kdf] {
281+
if !passhash.WorkFactorsEqual(credential.WorkFactor, passhash.DefaultWorkFactor[passhash.DefaultConfig.Kdf]) {
282282
t.Errorf("Updated credential WorkFactor did not update to safe recommended Kdf WorkFactor. %v != %v",
283283
credential.WorkFactor, passhash.DefaultWorkFactor[passhash.DefaultConfig.Kdf])
284284
}
285285
}
286286

287+
func TestScryptWorkFactorMutationDoesNotBreakExistingCredential(t *testing.T) {
288+
cfg := passhash.Config{
289+
Kdf: passhash.Scrypt,
290+
WorkFactor: &passhash.ScryptWorkFactor{N: 1024, R: 16, P: 1},
291+
SaltSize: 16,
292+
KeyLength: 32,
293+
AuditLogger: &passhash.DummyAuditLogger{},
294+
Store: passhash.DummyCredentialStore{},
295+
}
296+
userID := passhash.UserID(1)
297+
298+
cred, err := cfg.NewCredential(userID, testPassword)
299+
if err != nil {
300+
t.Fatalf("Unable to create new Credential: %v", err)
301+
}
302+
303+
if matched, _ := cred.MatchesPasswordWithConfig(cfg, testPassword); !matched {
304+
t.Fatalf("Password did not match before work factor change")
305+
}
306+
307+
wf := cfg.WorkFactor.(*passhash.ScryptWorkFactor)
308+
wf.N *= 2
309+
310+
if matched, _ := cred.MatchesPasswordWithConfig(cfg, testPassword); !matched {
311+
t.Fatalf("Password did not match after work factor change; credential should be stable")
312+
}
313+
}
314+
315+
func TestBcryptWorkFactorMutationTriggersUpgrade(t *testing.T) {
316+
cfg := passhash.Config{
317+
Kdf: passhash.Bcrypt,
318+
WorkFactor: &passhash.BcryptWorkFactor{Cost: 10},
319+
SaltSize: 16,
320+
KeyLength: 32,
321+
AuditLogger: &passhash.DummyAuditLogger{},
322+
Store: passhash.DummyCredentialStore{},
323+
}
324+
userID := passhash.UserID(2)
325+
326+
cred, err := cfg.NewCredential(userID, testPassword)
327+
if err != nil {
328+
t.Fatalf("Unable to create new Credential: %v", err)
329+
}
330+
331+
beforeCost, err := bcrypt.Cost(cred.Hash)
332+
if err != nil {
333+
t.Fatalf("Unable to read bcrypt cost: %v", err)
334+
}
335+
336+
wf := cfg.WorkFactor.(*passhash.BcryptWorkFactor)
337+
wf.Cost = beforeCost + 2
338+
339+
matched, updated := cred.MatchesPasswordWithConfig(cfg, testPassword)
340+
if !matched {
341+
t.Fatalf("Password did not match after bcrypt work factor change")
342+
}
343+
if !updated {
344+
t.Fatalf("Expected credential to be upgraded after bcrypt work factor change")
345+
}
346+
347+
afterCost, err := bcrypt.Cost(cred.Hash)
348+
if err != nil {
349+
t.Fatalf("Unable to read bcrypt cost after upgrade: %v", err)
350+
}
351+
if afterCost <= beforeCost {
352+
t.Fatalf("Expected bcrypt cost to increase, got before=%d after=%d", beforeCost, afterCost)
353+
}
354+
}
355+
287356
func TestChangePassword(t *testing.T) {
288357
userID := passhash.UserID(0)
289358
credential, err := passhash.NewCredential(userID, testPassword)

0 commit comments

Comments
 (0)