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

Ensure TOTP codes cannot be reused. #2908

Merged
merged 2 commits into from
Jun 23, 2017
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
6 changes: 6 additions & 0 deletions builtin/logical/totp/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package totp

import (
"strings"
"time"

"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
cache "github.com/patrickmn/go-cache"
)

func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
Expand All @@ -25,11 +27,15 @@ func Backend(conf *logical.BackendConfig) *backend {
Secrets: []*framework.Secret{},
}

b.usedCodes = cache.New(0, 30*time.Second)

return &b
}

type backend struct {
*framework.Backend

usedCodes *cache.Cache
}

const backendHelp = `
Expand Down
9 changes: 6 additions & 3 deletions builtin/logical/totp/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,10 @@ func TestBackend_keyCrudDefaultValues(t *testing.T) {
Steps: []logicaltest.TestStep{
testAccStepCreateKey(t, "test", keyData, false),
testAccStepReadKey(t, "test", expected),
testAccStepValidateCode(t, "test", code, true),
testAccStepValidateCode(t, "test", invalidCode, false),
testAccStepValidateCode(t, "test", code, true, false),
// Next step should fail because it should be in the used cache
testAccStepValidateCode(t, "test", code, false, true),
testAccStepValidateCode(t, "test", invalidCode, false, false),
testAccStepDeleteKey(t, "test"),
testAccStepReadKey(t, "test", nil),
},
Expand Down Expand Up @@ -1091,13 +1093,14 @@ func testAccStepReadKey(t *testing.T, name string, expected map[string]interface
}
}

func testAccStepValidateCode(t *testing.T, name string, code string, valid bool) logicaltest.TestStep {
func testAccStepValidateCode(t *testing.T, name string, code string, valid, expectError bool) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "code/" + name,
Data: map[string]interface{}{
"code": code,
},
ErrorOk: expectError,
Check: func(resp *logical.Response) error {
if resp == nil {
return fmt.Errorf("bad: %#v", resp)
Expand Down
18 changes: 18 additions & 0 deletions builtin/logical/totp/path_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"time"

"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
otplib "github.com/pquerna/otp"
Expand Down Expand Up @@ -84,6 +85,13 @@ func (b *backend) pathValidateCode(
return logical.ErrorResponse(fmt.Sprintf("unknown key: %s", name)), nil
}

usedName := fmt.Sprintf("%s_%s", name, code)

_, ok := b.usedCodes.Get(usedName)
if ok {
return logical.ErrorResponse("code already used; wait until the next time period"), nil
}

valid, err := totplib.ValidateCustom(code, key.Key, time.Now(), totplib.ValidateOpts{
Period: key.Period,
Skew: key.Skew,
Expand All @@ -94,6 +102,16 @@ func (b *backend) pathValidateCode(
return logical.ErrorResponse("an error occured while validating the code"), err
}

// Take the key skew, add two for behind and in front, and multiple that by
// the period to cover the full possibility of the validity of the key
err = b.usedCodes.Add(usedName, nil, time.Duration(
int64(time.Second)*
int64(key.Period)*
int64((2+key.Skew))))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we add the skew and not multiple it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about (time.Second * key.Period) + 2 * key.Skew? Multiplying the skew but adding the multiplied number.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skew isn't seconds, it's periods. So that will give you 2 extra nanoseconds.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then the equation should be time.Second * (key.Period + 2 * key.Skew). Otherwise, the value will bump up multifold.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key.Period is not a duration, it's an integer number of seconds. If the period is 30 seconds and the skew is 1 period, that equation will give you time.Second * 32. What we want is time.Second * 90.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, okay. Skew was something different in my mind. Looks good.

if err != nil {
return nil, errwrap.Wrapf("error adding code to used cache: {{err}}", err)
}

return &logical.Response{
Data: map[string]interface{}{
"valid": valid,
Expand Down