Skip to content

Commit

Permalink
age: reject leading zeroes and sign in scrypt work factor
Browse files Browse the repository at this point in the history
  • Loading branch information
FiloSottile committed Jun 19, 2022
1 parent 2088adf commit 2e09054
Show file tree
Hide file tree
Showing 42 changed files with 456 additions and 11 deletions.
9 changes: 9 additions & 0 deletions internal/testkit/testkit.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ func (f *TestFile) ArgsLine(args ...string) {
f.TextLine(strings.Join(append([]string{"->"}, args...), " "))
}

func (f *TestFile) UnreadArgsLine() []string {
line := strings.TrimPrefix(f.UnreadLine(), "-> ")
return strings.Split(line, " ")
}

var b64 = base64.RawStdEncoding.EncodeToString

func (f *TestFile) Body(body []byte) {
Expand Down Expand Up @@ -160,6 +165,10 @@ func (f *TestFile) ScryptRecordPassphrase(passphrase string) {

func (f *TestFile) ScryptNoRecordPassphrase(passphrase string, workFactor int) {
salt := f.Rand(16)
f.ScryptNoRecordPassphraseWithSalt(passphrase, workFactor, salt)
}

func (f *TestFile) ScryptNoRecordPassphraseWithSalt(passphrase string, workFactor int, salt []byte) {
f.ArgsLine("scrypt", b64(salt), strconv.Itoa(workFactor))
key, err := scrypt.Key([]byte(passphrase), append([]byte("age-encryption.org/v1/scrypt"), salt...),
1<<workFactor, 8, 1, 32)
Expand Down
10 changes: 8 additions & 2 deletions scrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"crypto/rand"
"errors"
"fmt"
"regexp"
"strconv"

"filippo.io/age/internal/format"
Expand Down Expand Up @@ -128,6 +129,8 @@ func (i *ScryptIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) {
return multiUnwrap(i.unwrap, stanzas)
}

var digitsRe = regexp.MustCompile(`^[1-9][0-9]*$`)

func (i *ScryptIdentity) unwrap(block *Stanza) ([]byte, error) {
if block.Type != "scrypt" {
return nil, ErrIncorrectIdentity
Expand All @@ -142,20 +145,23 @@ func (i *ScryptIdentity) unwrap(block *Stanza) ([]byte, error) {
if len(salt) != scryptSaltSize {
return nil, errors.New("invalid scrypt recipient block")
}
if w := block.Args[1]; !digitsRe.MatchString(w) {
return nil, fmt.Errorf("scrypt work factor encoding invalid: %q", w)
}
logN, err := strconv.Atoi(block.Args[1])
if err != nil {
return nil, fmt.Errorf("failed to parse scrypt work factor: %v", err)
}
if logN > i.maxWorkFactor {
return nil, fmt.Errorf("scrypt work factor too large: %v", logN)
}
if logN <= 0 {
if logN <= 0 { // unreachable
return nil, fmt.Errorf("invalid scrypt work factor: %v", logN)
}

salt = append([]byte(scryptLabel), salt...)
k, err := scrypt.Key(i.password, salt, 1<<logN, 8, 1, chacha20poly1305.KeySize)
if err != nil {
if err != nil { // unreachable
return nil, fmt.Errorf("failed to generate scrypt hash: %v", err)
}

Expand Down
Binary file added testdata/testkit/scrypt_bad_tag
Binary file not shown.
Binary file added testdata/testkit/scrypt_extra_argument
Binary file not shown.
Binary file added testdata/testkit/scrypt_not_canonical_body
Binary file not shown.
Binary file added testdata/testkit/scrypt_not_canonical_salt
Binary file not shown.
Binary file added testdata/testkit/scrypt_salt_long
Binary file not shown.
9 changes: 9 additions & 0 deletions testdata/testkit/scrypt_salt_missing
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
expect: header failure
file key: 59454c4c4f57205355424d4152494e45
passphrase: password

age-encryption.org/v1
-> scrypt 10
W0mMthyhNJOV3debCwkQcUlNx/i6Ss/A07aQCrG5Gcw
--- 1QsPcEbBSylfP4apakJqtDBJMrpd81rPuSLTCvdZx6E
�]?7�PqӦ F�� ����ۮ�z�(r���|
Expand Down
Binary file added testdata/testkit/scrypt_salt_short
Binary file not shown.
Binary file added testdata/testkit/scrypt_uppercase
Binary file not shown.
Binary file added testdata/testkit/scrypt_work_factor_hex
Binary file not shown.
Binary file added testdata/testkit/scrypt_work_factor_leading_garbage
Binary file not shown.
Binary file added testdata/testkit/scrypt_work_factor_leading_plus
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added testdata/testkit/scrypt_work_factor_missing
Binary file not shown.
Binary file added testdata/testkit/scrypt_work_factor_negative
Binary file not shown.
Binary file added testdata/testkit/scrypt_work_factor_overflow
Binary file not shown.
Binary file added testdata/testkit/scrypt_work_factor_trailing_garbage
Binary file not shown.
Binary file added testdata/testkit/scrypt_work_factor_wrong
Binary file not shown.
Binary file added testdata/testkit/scrypt_work_factor_zero
Binary file not shown.
27 changes: 27 additions & 0 deletions tests/scrypt_bad_tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore

package main

import (
"encoding/base64"

"filippo.io/age/internal/testkit"
)

func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.Scrypt("password", 10)
body, _ := base64.RawStdEncoding.DecodeString(f.UnreadLine())
body[len(body)-1] ^= 0xff
f.TextLine(base64.RawStdEncoding.EncodeToString(body))
f.HMAC()
f.Payload("age")
f.ExpectNoMatch()
f.Comment("the ChaCha20Poly1305 authentication tag on the body of the scrypt stanza is wrong")
f.Generate()
}
23 changes: 23 additions & 0 deletions tests/scrypt_extra_argument.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore

package main

import "filippo.io/age/internal/testkit"

func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.Scrypt("password", 10)
body, args := f.UnreadLine(), f.UnreadLine()
f.TextLine(args + " 10")
f.TextLine(body)
f.HMAC()
f.Payload("age")
f.ExpectHeaderFailure()
f.Comment("the base64 encoding of the share is not canonical")
f.Generate()
}
22 changes: 22 additions & 0 deletions tests/scrypt_not_canonical_body.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore

package main

import "filippo.io/age/internal/testkit"

func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.Scrypt("password", 10)
body := f.UnreadLine()
f.TextLine(testkit.NotCanonicalBase64(body))
f.HMAC()
f.Payload("age")
f.ExpectHeaderFailure()
f.Comment("the base64 encoding of the share is not canonical")
f.Generate()
}
22 changes: 22 additions & 0 deletions tests/scrypt_not_canonical_salt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore

package main

import "filippo.io/age/internal/testkit"

func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.Scrypt("password", 10)
body, args := f.UnreadLine(), f.UnreadArgsLine()
f.ArgsLine(args[0], testkit.NotCanonicalBase64(args[1]), args[2])
f.TextLine(body)
f.HMAC()
f.Payload("age")
f.ExpectHeaderFailure()
f.Generate()
}
20 changes: 20 additions & 0 deletions tests/scrypt_salt_long.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore

package main

import "filippo.io/age/internal/testkit"

func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.ScryptRecordPassphrase("password")
f.ScryptNoRecordPassphraseWithSalt("password", 10, f.Rand(20))
f.HMAC()
f.Payload("age")
f.ExpectHeaderFailure()
f.Generate()
}
23 changes: 23 additions & 0 deletions tests/scrypt_salt_missing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore

package main

import "filippo.io/age/internal/testkit"

func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.ScryptRecordPassphrase("password")
f.ScryptNoRecordPassphraseWithSalt("password", 10, nil)
body, args := f.UnreadLine(), f.UnreadArgsLine()
f.ArgsLine(args[0], args[2])
f.TextLine(body)
f.HMAC()
f.Payload("age")
f.ExpectHeaderFailure()
f.Generate()
}
20 changes: 20 additions & 0 deletions tests/scrypt_salt_short.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore

package main

import "filippo.io/age/internal/testkit"

func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.ScryptRecordPassphrase("password")
f.ScryptNoRecordPassphraseWithSalt("password", 10, f.Rand(12))
f.HMAC()
f.Payload("age")
f.ExpectHeaderFailure()
f.Generate()
}
22 changes: 22 additions & 0 deletions tests/scrypt_uppercase.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore

package main

import "filippo.io/age/internal/testkit"

func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.Scrypt("password", 10)
body, args := f.UnreadLine(), f.UnreadArgsLine()
f.ArgsLine("Scrypt", args[1], args[2])
f.TextLine(body)
f.HMAC()
f.Payload("age")
f.ExpectNoMatch()
f.Generate()
}
22 changes: 22 additions & 0 deletions tests/scrypt_work_factor_hex.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore

package main

import "filippo.io/age/internal/testkit"

func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.Scrypt("password", 10)
body, args := f.UnreadLine(), f.UnreadArgsLine()
f.ArgsLine(args[0], args[1], "0xa")
f.TextLine(body)
f.HMAC()
f.Payload("age")
f.ExpectHeaderFailure()
f.Generate()
}
22 changes: 22 additions & 0 deletions tests/scrypt_work_factor_leading_garbage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore

package main

import "filippo.io/age/internal/testkit"

func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.Scrypt("password", 10)
body, args := f.UnreadLine(), f.UnreadArgsLine()
f.ArgsLine(args[0], args[1], "aaaa10")
f.TextLine(body)
f.HMAC()
f.Payload("age")
f.ExpectHeaderFailure()
f.Generate()
}
22 changes: 22 additions & 0 deletions tests/scrypt_work_factor_leading_plus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore

package main

import "filippo.io/age/internal/testkit"

func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.Scrypt("password", 10)
body, args := f.UnreadLine(), f.UnreadArgsLine()
f.ArgsLine(args[0], args[1], "+10")
f.TextLine(body)
f.HMAC()
f.Payload("age")
f.ExpectHeaderFailure()
f.Generate()
}
22 changes: 22 additions & 0 deletions tests/scrypt_work_factor_leading_zero_decimal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore

package main

import "filippo.io/age/internal/testkit"

func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.Scrypt("password", 10)
body, args := f.UnreadLine(), f.UnreadArgsLine()
f.ArgsLine(args[0], args[1], "010")
f.TextLine(body)
f.HMAC()
f.Payload("age")
f.ExpectHeaderFailure()
f.Generate()
}
22 changes: 22 additions & 0 deletions tests/scrypt_work_factor_leading_zero_octal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore

package main

import "filippo.io/age/internal/testkit"

func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.Scrypt("password", 10)
body, args := f.UnreadLine(), f.UnreadArgsLine()
f.ArgsLine(args[0], args[1], "012")
f.TextLine(body)
f.HMAC()
f.Payload("age")
f.ExpectHeaderFailure()
f.Generate()
}
Loading

0 comments on commit 2e09054

Please sign in to comment.