From f9f4cd03431e62490282b2a075f2cdb5f3d85ddd Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sat, 4 Mar 2017 21:13:50 -0800 Subject: [PATCH 01/43] Initialized basic outline of TOTP backend using Postgresql backend as template --- builtin/logical/totp/backend.go | 47 ++++++ builtin/logical/totp/path_role_create.go | 87 +++++++++++ builtin/logical/totp/path_roles.go | 175 +++++++++++++++++++++++ 3 files changed, 309 insertions(+) create mode 100644 builtin/logical/totp/backend.go create mode 100644 builtin/logical/totp/path_role_create.go create mode 100644 builtin/logical/totp/path_roles.go diff --git a/builtin/logical/totp/backend.go b/builtin/logical/totp/backend.go new file mode 100644 index 000000000000..9c0113286c29 --- /dev/null +++ b/builtin/logical/totp/backend.go @@ -0,0 +1,47 @@ +package totp + +import ( + "fmt" + "strings" + + log "github.com/mgutz/logxi/v1" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func Factory(conf *logical.BackendConfig) (logical.Backend, error) { + return Backend(conf).Setup(conf) +} + +func Backend(conf *logical.BackendConfig) *backend { + var b backend + b.Backend = &framework.Backend{ + Help: strings.TrimSpace(backendHelp), + + Paths: []*framework.Path{ + pathListRoles(&b), + pathRoles(&b), + pathRoleCreate(&b), + }, + + Secrets: []*framework.Secret{}, + } + + b.logger = conf.Logger + return &b +} + +type backend struct { + *framework.Backend + + logger log.Logger +} + +// This needs to be updated +const backendHelp = ` +The PostgreSQL backend dynamically generates database users. + +After mounting this backend, configure it using the endpoints within +the "config/" path. +` diff --git a/builtin/logical/totp/path_role_create.go b/builtin/logical/totp/path_role_create.go new file mode 100644 index 000000000000..ba14da3c6fd3 --- /dev/null +++ b/builtin/logical/totp/path_role_create.go @@ -0,0 +1,87 @@ +package postgresql + +import ( + "fmt" + "strings" + "time" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + "github.com/pquerna/totp" +) + +// Update with TOTP values +func pathRoleCreate(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "creds/" + framework.GenericNameRegex("name"), + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the role.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathRoleCreateRead, + }, + + HelpSynopsis: pathRoleCreateReadHelpSyn, + HelpDescription: pathRoleCreateReadHelpDesc, + } +} + +func (b *backend) pathRoleCreateRead( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.logger.Trace("totp/pathRoleCreateRead: enter") + defer b.logger.Trace("totp/pathRoleCreateRead: exit") + + name := data.Get("name").(string) + + // Get the role + b.logger.Trace("totp/pathRoleCreateRead: getting role") + role, err := b.Role(req.Storage, name) + if err != nil { + return nil, err + } + if role == nil { + return logical.ErrorResponse(fmt.Sprintf("unknown role: %s", name)), nil + } + + // Generate TOTP token + /* + //Generate key using totp library + totpKey, err := totp.GenerateCodeCustom(role.key, time.Now().UTC(), ValdidateOpts{ + Period: role.period, + Skew: 1, + Digits: otp.DigitsSix + Algorithm: otp.AlgorithmSHA1 + }); + + if err != nil { + return nil, err + } + */ + + // Return the secret + b.logger.Trace("totp/pathRoleCreateRead: generating secret") + + /* + return &logical.Response{ + Data: map[string]interface{}{ + "token": totpKey, + }, + }, nil + */ + return resp, nil +} + +// Update help strings +const pathRoleCreateReadHelpSyn = ` +Request database credentials for a certain role. +` + +const pathRoleCreateReadHelpDesc = ` +This path reads database credentials for a certain role. The +database credentials will be generated on demand and will be automatically +revoked when the lease is up. +` diff --git a/builtin/logical/totp/path_roles.go b/builtin/logical/totp/path_roles.go new file mode 100644 index 000000000000..a1550c2177aa --- /dev/null +++ b/builtin/logical/totp/path_roles.go @@ -0,0 +1,175 @@ +package postgresql + +import ( + "fmt" + "strings" + + "github.com/hashicorp/vault/helper/strutil" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathListRoles(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "roles/?$", + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ListOperation: b.pathRoleList, + }, + + HelpSynopsis: pathRoleHelpSyn, + HelpDescription: pathRoleHelpDesc, + } +} + +func pathRoles(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "roles/" + framework.GenericNameRegex("name"), + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "Name of the role.", + }, + + "sql": { + Type: framework.TypeString, + Description: "SQL string to create a user. See help for more info.", + }, + + "revocation_sql": { + Type: framework.TypeString, + Description: `SQL statements to be executed to revoke a user. Must be a semicolon-separated +string, a base64-encoded semicolon-separated string, a serialized JSON string +array, or a base64-encoded serialized JSON string array. The '{{name}}' value +will be substituted.`, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathRoleRead, + logical.UpdateOperation: b.pathRoleCreate, + logical.DeleteOperation: b.pathRoleDelete, + }, + + HelpSynopsis: pathRoleHelpSyn, + HelpDescription: pathRoleHelpDesc, + } +} + +func (b *backend) Role(s logical.Storage, n string) (*roleEntry, error) { + entry, err := s.Get("role/" + n) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var result roleEntry + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + + return &result, nil +} + +func (b *backend) pathRoleDelete( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + err := req.Storage.Delete("role/" + data.Get("name").(string)) + if err != nil { + return nil, err + } + + return nil, nil +} + +func (b *backend) pathRoleRead( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + role, err := b.Role(req.Storage, data.Get("name").(string)) + if err != nil { + return nil, err + } + if role == nil { + return nil, nil + } + + // Return values of role + return &logical.Response{ + Data: map[string]interface{}{ + "period": role.period + }, + }, nil +} + +func (b *backend) pathRoleList( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + entries, err := req.Storage.List("role/") + if err != nil { + return nil, err + } + + return logical.ListResponse(entries), nil +} + +func (b *backend) pathRoleCreate( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + name := data.Get("name").(string) + sql := data.Get("sql").(string) + + // Store it + entry, err := logical.StorageEntryJSON("role/"+name, &roleEntry{ + SQL: sql, + RevocationSQL: data.Get("revocation_sql").(string), + }) + if err != nil { + return nil, err + } + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + + return nil, nil +} + +type roleEntry struct { + SQL string `json:"sql" mapstructure:"sql" structs:"sql"` + RevocationSQL string `json:"revocation_sql" mapstructure:"revocation_sql" structs:"revocation_sql"` +} + +const pathRoleHelpSyn = ` +Manage the roles that can be created with this backend. +` + +const pathRoleHelpDesc = ` +This path lets you manage the roles that can be created with this backend. + +The "sql" parameter customizes the SQL string used to create the role. +This can be a sequence of SQL queries. Some substitution will be done to the +SQL string for certain keys. The names of the variables must be surrounded +by "{{" and "}}" to be replaced. + + * "name" - The random username generated for the DB user. + + * "password" - The random password generated for the DB user. + + * "expiration" - The timestamp when this user will expire. + +Example of a decent SQL query to use: + + CREATE ROLE "{{name}}" WITH + LOGIN + PASSWORD '{{password}}' + VALID UNTIL '{{expiration}}'; + GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}"; + +Note the above user would be able to access everything in schema public. +For more complex GRANT clauses, see the PostgreSQL manual. + +The "revocation_sql" parameter customizes the SQL string used to revoke a user. +Example of a decent revocation SQL query to use: + + REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM {{name}}; + REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM {{name}}; + REVOKE USAGE ON SCHEMA public FROM {{name}}; + DROP ROLE IF EXISTS {{name}}; +` From 77a169425d4e6c9bac3a82576da74777fddd07dd Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sun, 5 Mar 2017 12:32:37 -0800 Subject: [PATCH 02/43] Updated TOTP backend.go's structure and help string --- builtin/logical/totp/backend.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/builtin/logical/totp/backend.go b/builtin/logical/totp/backend.go index 9c0113286c29..940d6bc92438 100644 --- a/builtin/logical/totp/backend.go +++ b/builtin/logical/totp/backend.go @@ -1,7 +1,6 @@ package totp import ( - "fmt" "strings" log "github.com/mgutz/logxi/v1" @@ -34,14 +33,9 @@ func Backend(conf *logical.BackendConfig) *backend { type backend struct { *framework.Backend - logger log.Logger } -// This needs to be updated const backendHelp = ` -The PostgreSQL backend dynamically generates database users. - -After mounting this backend, configure it using the endpoints within -the "config/" path. +The TOTP backend dynamically generates time-based one-time use passwords. ` From b04ab7e7ef875ba96198b2e67547bc78947d4d3c Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sun, 5 Mar 2017 13:20:39 -0800 Subject: [PATCH 03/43] Updated TOTP path_roles.go's structure and help strings --- builtin/logical/totp/path_roles.go | 117 ++++++++++++++++++----------- 1 file changed, 74 insertions(+), 43 deletions(-) diff --git a/builtin/logical/totp/path_roles.go b/builtin/logical/totp/path_roles.go index a1550c2177aa..ed0dc2c47573 100644 --- a/builtin/logical/totp/path_roles.go +++ b/builtin/logical/totp/path_roles.go @@ -1,10 +1,6 @@ -package postgresql +package totp import ( - "fmt" - "strings" - - "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -31,17 +27,34 @@ func pathRoles(b *backend) *framework.Path { Description: "Name of the role.", }, - "sql": { + "key": { + Type: framework.TypeString, + Description: "The shared master key used to generate a TOTP token.", + }, + + "issuer": { + Type: framework.TypeString, + Description: `The name of the key's issuing organization.`, + }, + + "account_name": { + Type: framework.TypeString, + Description: `The name of the account associated with the key.`, + }, + + "period": { + Type: framework.TypeInt, + Description: `The length of time used to generate a counter for the TOTP token calculation.`, + }, + + "algorithm": { Type: framework.TypeString, - Description: "SQL string to create a user. See help for more info.", + Description: `The hashing algorithm used to generate the TOTP token.`, }, - "revocation_sql": { - Type: framework.TypeString, - Description: `SQL statements to be executed to revoke a user. Must be a semicolon-separated -string, a base64-encoded semicolon-separated string, a serialized JSON string -array, or a base64-encoded serialized JSON string array. The '{{name}}' value -will be substituted.`, + "digits": { + Type: framework.TypeInt, + Description: `The number of digits in the generated TOTP token.`, }, }, @@ -92,11 +105,15 @@ func (b *backend) pathRoleRead( if role == nil { return nil, nil } - + // Return values of role return &logical.Response{ Data: map[string]interface{}{ - "period": role.period + "issuer": role.Issuer, + "account_name": role.Account_Name, + "period": role.Period, + "algorithm": role.Algorithm, + "digits": role.Digits, }, }, nil } @@ -114,12 +131,38 @@ func (b *backend) pathRoleList( func (b *backend) pathRoleCreate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { name := data.Get("name").(string) - sql := data.Get("sql").(string) + key := data.Get("key").(string) + issuer := data.Get("issuer").(string) + account_name := data.Get("account_name").(string) + period := data.Get("period").(int) + algorithm := data.Get("algorithm").(string) + digits := data.Get("digits").(int) + + // Set optional parameters if neccessary + if period == 0 { + period = 30 + } + + switch algorithm { + case "SHA1", "SHA256", "SHA512", "MD5": + default: + algorithm = "SHA1" + } + + switch digits { + case 6, 8: + default: + digits = 6 + } // Store it entry, err := logical.StorageEntryJSON("role/"+name, &roleEntry{ - SQL: sql, - RevocationSQL: data.Get("revocation_sql").(string), + Key: key, + Issuer: issuer, + Account_Name: aaccount_name, + Period: period, + Algorithm: algorithm, + Digits: digits, }) if err != nil { return nil, err @@ -132,8 +175,12 @@ func (b *backend) pathRoleCreate( } type roleEntry struct { - SQL string `json:"sql" mapstructure:"sql" structs:"sql"` - RevocationSQL string `json:"revocation_sql" mapstructure:"revocation_sql" structs:"revocation_sql"` + Key string `json:"key" mapstructure:"key" structs:"key"` + Issuer string `json:"issuer" mapstructure:"issuer" structs:"issuer"` + Account_Name string `json:"account_name" mapstructure:"account_name" structs:"account_name"` + Period string `json:"period" mapstructure:"period" structs:"period"` + Algorithm string `json:"algorithm" mapstructure:"algorithm" structs:"algorithm"` + Digits string `json:"digits" mapstructure:"digits" structs:"digits"` } const pathRoleHelpSyn = ` @@ -143,33 +190,17 @@ Manage the roles that can be created with this backend. const pathRoleHelpDesc = ` This path lets you manage the roles that can be created with this backend. -The "sql" parameter customizes the SQL string used to create the role. -This can be a sequence of SQL queries. Some substitution will be done to the -SQL string for certain keys. The names of the variables must be surrounded -by "{{" and "}}" to be replaced. - - * "name" - The random username generated for the DB user. - - * "password" - The random password generated for the DB user. +Role Parameters: - * "expiration" - The timestamp when this user will expire. + * "key" - required - The shared master key used to generate a TOTP token. -Example of a decent SQL query to use: + * "issuer" - required - The name of the key's issuing organization. - CREATE ROLE "{{name}}" WITH - LOGIN - PASSWORD '{{password}}' - VALID UNTIL '{{expiration}}'; - GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}"; + * "account_name" - required - The name of the account associated with the key. -Note the above user would be able to access everything in schema public. -For more complex GRANT clauses, see the PostgreSQL manual. + * "period" - optional - The length of time used to generate a counter for the TOTP token calculation. Default value is 30 seconds. -The "revocation_sql" parameter customizes the SQL string used to revoke a user. -Example of a decent revocation SQL query to use: + * "algorithm" - optional - The hashing algorithm used to generate the TOTP token. Default value is "SHA1". Other options include "SHA256", "SHA512" and "MD5". - REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM {{name}}; - REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM {{name}}; - REVOKE USAGE ON SCHEMA public FROM {{name}}; - DROP ROLE IF EXISTS {{name}}; + * "digits" - optional - The number of digits in the generated TOTP token. Default value is 6. Options include 6 or 8. ` From f64f5b9a77c7a92617b54e76926eebd1ded33e5e Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sun, 5 Mar 2017 13:38:03 -0800 Subject: [PATCH 04/43] Updated TOTP path_role_create.go's structure and help strings --- builtin/logical/totp/path_role_create.go | 49 +++++++++--------------- 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/builtin/logical/totp/path_role_create.go b/builtin/logical/totp/path_role_create.go index ba14da3c6fd3..03f2cfc637a3 100644 --- a/builtin/logical/totp/path_role_create.go +++ b/builtin/logical/totp/path_role_create.go @@ -1,16 +1,14 @@ -package postgresql +package totp import ( "fmt" - "strings" "time" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" - "github.com/pquerna/totp" + "github.com/pquerna/otp/totp" ) -// Update with TOTP values func pathRoleCreate(b *backend) *framework.Path { return &framework.Path{ Pattern: "creds/" + framework.GenericNameRegex("name"), @@ -47,41 +45,32 @@ func (b *backend) pathRoleCreateRead( return logical.ErrorResponse(fmt.Sprintf("unknown role: %s", name)), nil } - // Generate TOTP token - /* - //Generate key using totp library - totpKey, err := totp.GenerateCodeCustom(role.key, time.Now().UTC(), ValdidateOpts{ - Period: role.period, - Skew: 1, - Digits: otp.DigitsSix - Algorithm: otp.AlgorithmSHA1 - }); + // Generate password using totp library + totpToken, err := totp.GenerateCodeCustom(role.Key, time.Now().UTC(), ValdidateOpts{ + Period: role.Period, + Digits: role.Digits, + Algorithm: role.Algorithm, + }) - if err != nil { - return nil, err - } - */ + if err != nil { + return nil, err + } // Return the secret b.logger.Trace("totp/pathRoleCreateRead: generating secret") - /* - return &logical.Response{ - Data: map[string]interface{}{ - "token": totpKey, - }, - }, nil - */ + resp := &logical.Response{ + Data: map[string]interface{}{ + "token": totpToken, + }, + }, nil + return resp, nil } -// Update help strings const pathRoleCreateReadHelpSyn = ` -Request database credentials for a certain role. +Request time-based one-time use password for a certain role. ` - const pathRoleCreateReadHelpDesc = ` -This path reads database credentials for a certain role. The -database credentials will be generated on demand and will be automatically -revoked when the lease is up. +This path generates a time-based one-time use password for a certain role. ` From 49f3c0c82dd8bf7bb1f318e6811ffd32931a126f Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sun, 5 Mar 2017 13:38:42 -0800 Subject: [PATCH 05/43] Fixed typo in path_roles.go --- builtin/logical/totp/path_roles.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/logical/totp/path_roles.go b/builtin/logical/totp/path_roles.go index ed0dc2c47573..d6c546fb52ba 100644 --- a/builtin/logical/totp/path_roles.go +++ b/builtin/logical/totp/path_roles.go @@ -159,7 +159,7 @@ func (b *backend) pathRoleCreate( entry, err := logical.StorageEntryJSON("role/"+name, &roleEntry{ Key: key, Issuer: issuer, - Account_Name: aaccount_name, + Account_Name: account_name, Period: period, Algorithm: algorithm, Digits: digits, From f19b57ffdfd3a9b9ea086747ef674a27130ec0b0 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sun, 5 Mar 2017 15:21:15 -0800 Subject: [PATCH 06/43] Fixed errors in path_role_create.go and path_roles.go --- builtin/logical/totp/path_role_create.go | 30 +++++++++++++++++++++--- builtin/logical/totp/path_roles.go | 4 ++-- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/builtin/logical/totp/path_role_create.go b/builtin/logical/totp/path_role_create.go index 03f2cfc637a3..687014e80c72 100644 --- a/builtin/logical/totp/path_role_create.go +++ b/builtin/logical/totp/path_role_create.go @@ -6,7 +6,8 @@ import ( "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" - "github.com/pquerna/otp/totp" + otplib "github.com/pquerna/otp/ + totplib "github.com/pquerna/otp/totp" ) func pathRoleCreate(b *backend) *framework.Path { @@ -45,8 +46,31 @@ func (b *backend) pathRoleCreateRead( return logical.ErrorResponse(fmt.Sprintf("unknown role: %s", name)), nil } + // Translate digits and algorithm to a format the totp library understands + digits otplib.Digits + switch role.Digits{ + case 6: + digits = otplib.DigitsSix + case 8: + digits = otplib.DigitsEight + } + + algorithm otplib.Algorithm + switch role.Algorithm{ + case "SHA1": + algorithm = otplib.AlgorithmSHA1 + case "SHA256": + algorithm = otplib.AlgorithmSHA256 + case "SHA512": + algorithm = otplib.AlgorithmSHA512 + case "MD5": + algorithm = otplib.AlgorithmMD5 + default: + algorithm = otplib.AlgorithmSHA1 + } + // Generate password using totp library - totpToken, err := totp.GenerateCodeCustom(role.Key, time.Now().UTC(), ValdidateOpts{ + totpToken, err := totplib.GenerateCodeCustom(role.Key, time.Now().UTC(), totplib.ValidateOpts{ Period: role.Period, Digits: role.Digits, Algorithm: role.Algorithm, @@ -59,7 +83,7 @@ func (b *backend) pathRoleCreateRead( // Return the secret b.logger.Trace("totp/pathRoleCreateRead: generating secret") - resp := &logical.Response{ + resp, err := &logical.Response{ Data: map[string]interface{}{ "token": totpToken, }, diff --git a/builtin/logical/totp/path_roles.go b/builtin/logical/totp/path_roles.go index d6c546fb52ba..0005832a678d 100644 --- a/builtin/logical/totp/path_roles.go +++ b/builtin/logical/totp/path_roles.go @@ -178,9 +178,9 @@ type roleEntry struct { Key string `json:"key" mapstructure:"key" structs:"key"` Issuer string `json:"issuer" mapstructure:"issuer" structs:"issuer"` Account_Name string `json:"account_name" mapstructure:"account_name" structs:"account_name"` - Period string `json:"period" mapstructure:"period" structs:"period"` + Period uint `json:"period" mapstructure:"period" structs:"period"` Algorithm string `json:"algorithm" mapstructure:"algorithm" structs:"algorithm"` - Digits string `json:"digits" mapstructure:"digits" structs:"digits"` + Digits uint `json:"digits" mapstructure:"digits" structs:"digits"` } const pathRoleHelpSyn = ` From ff8fe83e76eae1e874fb8b54594988a71b68789c Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sun, 5 Mar 2017 15:23:52 -0800 Subject: [PATCH 07/43] Added TOTP secret backend information to cli commands --- cli/commands.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/commands.go b/cli/commands.go index 190111177953..14875d805a37 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -28,6 +28,7 @@ import ( "github.com/hashicorp/vault/builtin/logical/postgresql" "github.com/hashicorp/vault/builtin/logical/rabbitmq" "github.com/hashicorp/vault/builtin/logical/ssh" + "github.com/hashicorp/vault/builtin/logical/totp" "github.com/hashicorp/vault/builtin/logical/transit" "github.com/hashicorp/vault/audit" @@ -91,6 +92,7 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory { "mysql": mysql.Factory, "ssh": ssh.Factory, "rabbitmq": rabbitmq.Factory, + "totp": totp.Factory, }, ShutdownCh: command.MakeShutdownCh(), SighupCh: command.MakeSighupCh(), From 4a222fd29b4078dc178ff188c09bc5c721ddef4d Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sun, 5 Mar 2017 15:32:24 -0800 Subject: [PATCH 08/43] Fixed build errors in path_roles.go and path_role_create.go --- builtin/logical/totp/path_role_create.go | 46 ++++++++++++------------ builtin/logical/totp/path_roles.go | 4 +-- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/builtin/logical/totp/path_role_create.go b/builtin/logical/totp/path_role_create.go index 687014e80c72..15bbf4517961 100644 --- a/builtin/logical/totp/path_role_create.go +++ b/builtin/logical/totp/path_role_create.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" - otplib "github.com/pquerna/otp/ + otplib "github.com/pquerna/otp" totplib "github.com/pquerna/otp/totp" ) @@ -47,33 +47,33 @@ func (b *backend) pathRoleCreateRead( } // Translate digits and algorithm to a format the totp library understands - digits otplib.Digits - switch role.Digits{ - case 6: - digits = otplib.DigitsSix - case 8: - digits = otplib.DigitsEight + var digits otplib.Digits + switch role.Digits { + case 6: + digits = otplib.DigitsSix + case 8: + digits = otplib.DigitsEight } - - algorithm otplib.Algorithm - switch role.Algorithm{ - case "SHA1": - algorithm = otplib.AlgorithmSHA1 - case "SHA256": - algorithm = otplib.AlgorithmSHA256 - case "SHA512": - algorithm = otplib.AlgorithmSHA512 - case "MD5": - algorithm = otplib.AlgorithmMD5 - default: - algorithm = otplib.AlgorithmSHA1 + + var algorithm otplib.Algorithm + switch role.Algorithm { + case "SHA1": + algorithm = otplib.AlgorithmSHA1 + case "SHA256": + algorithm = otplib.AlgorithmSHA256 + case "SHA512": + algorithm = otplib.AlgorithmSHA512 + case "MD5": + algorithm = otplib.AlgorithmMD5 + default: + algorithm = otplib.AlgorithmSHA1 } - + // Generate password using totp library totpToken, err := totplib.GenerateCodeCustom(role.Key, time.Now().UTC(), totplib.ValidateOpts{ Period: role.Period, - Digits: role.Digits, - Algorithm: role.Algorithm, + Digits: digits, + Algorithm: algorithm, }) if err != nil { diff --git a/builtin/logical/totp/path_roles.go b/builtin/logical/totp/path_roles.go index 0005832a678d..99aacdf000e6 100644 --- a/builtin/logical/totp/path_roles.go +++ b/builtin/logical/totp/path_roles.go @@ -134,9 +134,9 @@ func (b *backend) pathRoleCreate( key := data.Get("key").(string) issuer := data.Get("issuer").(string) account_name := data.Get("account_name").(string) - period := data.Get("period").(int) + period := data.Get("period").(uint) algorithm := data.Get("algorithm").(string) - digits := data.Get("digits").(int) + digits := data.Get("digits").(uint) // Set optional parameters if neccessary if period == 0 { From 44b681b2921d730379e91d9157292f2fd809f110 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sun, 5 Mar 2017 15:49:11 -0800 Subject: [PATCH 09/43] Changed field values of period and digits from uint to int, added uint conversion of period when generating passwords --- builtin/logical/totp/path_role_create.go | 4 +++- builtin/logical/totp/path_roles.go | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/builtin/logical/totp/path_role_create.go b/builtin/logical/totp/path_role_create.go index 15bbf4517961..750985318800 100644 --- a/builtin/logical/totp/path_role_create.go +++ b/builtin/logical/totp/path_role_create.go @@ -69,9 +69,11 @@ func (b *backend) pathRoleCreateRead( algorithm = otplib.AlgorithmSHA1 } + period := uint(role.Period) + // Generate password using totp library totpToken, err := totplib.GenerateCodeCustom(role.Key, time.Now().UTC(), totplib.ValidateOpts{ - Period: role.Period, + Period: period, Digits: digits, Algorithm: algorithm, }) diff --git a/builtin/logical/totp/path_roles.go b/builtin/logical/totp/path_roles.go index 99aacdf000e6..7bd817d7cd1c 100644 --- a/builtin/logical/totp/path_roles.go +++ b/builtin/logical/totp/path_roles.go @@ -134,9 +134,9 @@ func (b *backend) pathRoleCreate( key := data.Get("key").(string) issuer := data.Get("issuer").(string) account_name := data.Get("account_name").(string) - period := data.Get("period").(uint) + period := data.Get("period").(int) algorithm := data.Get("algorithm").(string) - digits := data.Get("digits").(uint) + digits := data.Get("digits").(int) // Set optional parameters if neccessary if period == 0 { @@ -178,9 +178,9 @@ type roleEntry struct { Key string `json:"key" mapstructure:"key" structs:"key"` Issuer string `json:"issuer" mapstructure:"issuer" structs:"issuer"` Account_Name string `json:"account_name" mapstructure:"account_name" structs:"account_name"` - Period uint `json:"period" mapstructure:"period" structs:"period"` + Period int `json:"period" mapstructure:"period" structs:"period"` Algorithm string `json:"algorithm" mapstructure:"algorithm" structs:"algorithm"` - Digits uint `json:"digits" mapstructure:"digits" structs:"digits"` + Digits int `json:"digits" mapstructure:"digits" structs:"digits"` } const pathRoleHelpSyn = ` From 092e797744ffa265132d15db96f4c4ad40c80643 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sun, 5 Mar 2017 17:11:58 -0800 Subject: [PATCH 10/43] Initialized TOTP test file based on structure of postgresql test file --- builtin/logical/totp/backend_test.go | 200 +++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 builtin/logical/totp/backend_test.go diff --git a/builtin/logical/totp/backend_test.go b/builtin/logical/totp/backend_test.go new file mode 100644 index 000000000000..a97598d099a0 --- /dev/null +++ b/builtin/logical/totp/backend_test.go @@ -0,0 +1,200 @@ +package totp + +import ( + "fmt" + "log" + "testing" + "time" + + "github.com/hashicorp/vault/logical" + logicaltest "github.com/hashicorp/vault/logical/testing" + "github.com/mitchellh/mapstructure" + otplib "github.com/pquerna/otp" + totplib "github.com/pquerna/otp/totp" +) + +var ( + masterKey string +) + +func TestBackend_basic(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepConfig(t, connData, false), + testAccStepCreateRole(t, "web", testRole, false), + testAccStepReadCreds(t, b, config.StorageView, "web", connURL), + }, + }) +} + +func TestBackend_roleCrud(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepConfig(t, connData, false), + testAccStepCreateRole(t, "web", testRole, false), + testAccStepReadRole(t, "web", testRole), + testAccStepDeleteRole(t, "web"), + testAccStepReadRole(t, "web", ""), + }, + }) +} + +func testAccStepCreateRole(t *testing.T, name string, sql string, expectFail bool) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: path.Join("roles", name), + Data: map[string]interface{}{ + "sql": sql, + }, + ErrorOk: expectFail, + } +} + +func testAccStepCreateRoleWithRevocationSQL(t *testing.T, name, sql, revocationSQL string, expectFail bool) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: path.Join("roles", name), + Data: map[string]interface{}{ + "sql": sql, + "revocation_sql": revocationSQL, + }, + ErrorOk: expectFail, + } +} + +func testAccStepDeleteRole(t *testing.T, name string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.DeleteOperation, + Path: path.Join("roles", name), + } +} + +func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, name string, connURL string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: path.Join("creds", name), + Check: func(resp *logical.Response) error { + var d struct { + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + return err + } + log.Printf("[TRACE] Generated credentials: %v", d) + conn, err := pq.ParseURL(connURL) + + if err != nil { + t.Fatal(err) + } + + conn += " timezone=utc" + + db, err := sql.Open("postgres", conn) + if err != nil { + t.Fatal(err) + } + + returnedRows := func() int { + stmt, err := db.Prepare("SELECT DISTINCT schemaname FROM pg_tables WHERE has_table_privilege($1, 'information_schema.role_column_grants', 'select');") + if err != nil { + return -1 + } + defer stmt.Close() + + rows, err := stmt.Query(d.Username) + if err != nil { + return -1 + } + defer rows.Close() + + i := 0 + for rows.Next() { + i++ + } + return i + } + + // minNumPermissions is the minimum number of permissions that will always be present. + const minNumPermissions = 2 + + userRows := returnedRows() + if userRows < minNumPermissions { + t.Fatalf("did not get expected number of rows, got %d", userRows) + } + + resp, err = b.HandleRequest(&logical.Request{ + Operation: logical.RevokeOperation, + Storage: s, + Secret: &logical.Secret{ + InternalData: map[string]interface{}{ + "secret_type": "creds", + "username": d.Username, + "role": name, + }, + }, + }) + if err != nil { + return err + } + if resp != nil { + if resp.IsError() { + return fmt.Errorf("Error on resp: %#v", *resp) + } + } + + userRows = returnedRows() + // User shouldn't exist so returnedRows() should encounter an error and exit with -1 + if userRows != -1 { + t.Fatalf("did not get expected number of rows, got %d", userRows) + } + + return nil + }, + } +} + +func testAccStepReadRole(t *testing.T, name string, sql string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "roles/" + name, + Check: func(resp *logical.Response) error { + if resp == nil { + if sql == "" { + return nil + } + + return fmt.Errorf("bad: %#v", resp) + } + + var d struct { + SQL string `mapstructure:"sql"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + return err + } + + if d.SQL != sql { + return fmt.Errorf("bad: %#v", resp) + } + + return nil + }, + } +} From 7a97efa187621e8437c7d94f7a898a613fb7d047 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Mon, 6 Mar 2017 18:00:10 -0800 Subject: [PATCH 11/43] Added enforcement of input values --- builtin/logical/totp/path_roles.go | 35 ++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/builtin/logical/totp/path_roles.go b/builtin/logical/totp/path_roles.go index 7bd817d7cd1c..dc1e3f3b4185 100644 --- a/builtin/logical/totp/path_roles.go +++ b/builtin/logical/totp/path_roles.go @@ -1,6 +1,9 @@ package totp import ( + "encoding/base32" + "fmt" + "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -138,6 +141,30 @@ func (b *backend) pathRoleCreate( algorithm := data.Get("algorithm").(string) digits := data.Get("digits").(int) + // Enforce input value requirements + if key == "" { + return logical.ErrorResponse("The key value is required."), nil + } + + _, err := base32.StdEncoding.DecodeString(key) + + if err != nil { + return logical.ErrorResponse(fmt.Sprintf( + "Invalid key value: %s", err)), nil + } + + if period < 0 { + return logical.ErrorResponse("The period value must be greater than zero."), nil + } + + if issuer == "" { + return logical.ErrorResponse("The issuer value is required."), nil + } + + if account_name == "" { + return logical.ErrorResponse("The account_name value is required."), nil + } + // Set optional parameters if neccessary if period == 0 { period = 30 @@ -145,14 +172,18 @@ func (b *backend) pathRoleCreate( switch algorithm { case "SHA1", "SHA256", "SHA512", "MD5": - default: + case "": algorithm = "SHA1" + default: + return logical.ErrorResponse("The algorithm value is not valid."), nil } switch digits { case 6, 8: - default: + case 0: digits = 6 + default: + return logical.ErrorResponse("The digit value can only be 6 or 8."), nil } // Store it From d2b07be8b55f162202e368340a23de3468ab8a60 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Mon, 6 Mar 2017 18:05:28 -0800 Subject: [PATCH 12/43] Added otp library to vendor folder --- vendor/github.com/pquerna/otp/LICENSE | 202 +++++++++++++++++++++ vendor/github.com/pquerna/otp/NOTICE | 5 + vendor/github.com/pquerna/otp/README.md | 60 ++++++ vendor/github.com/pquerna/otp/doc.go | 70 +++++++ vendor/github.com/pquerna/otp/hotp/hotp.go | 187 +++++++++++++++++++ vendor/github.com/pquerna/otp/otp.go | 200 ++++++++++++++++++++ vendor/github.com/pquerna/otp/totp/totp.go | 191 +++++++++++++++++++ 7 files changed, 915 insertions(+) create mode 100644 vendor/github.com/pquerna/otp/LICENSE create mode 100644 vendor/github.com/pquerna/otp/NOTICE create mode 100644 vendor/github.com/pquerna/otp/README.md create mode 100644 vendor/github.com/pquerna/otp/doc.go create mode 100644 vendor/github.com/pquerna/otp/hotp/hotp.go create mode 100644 vendor/github.com/pquerna/otp/otp.go create mode 100644 vendor/github.com/pquerna/otp/totp/totp.go diff --git a/vendor/github.com/pquerna/otp/LICENSE b/vendor/github.com/pquerna/otp/LICENSE new file mode 100644 index 000000000000..d64569567334 --- /dev/null +++ b/vendor/github.com/pquerna/otp/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/pquerna/otp/NOTICE b/vendor/github.com/pquerna/otp/NOTICE new file mode 100644 index 000000000000..50e2e75016eb --- /dev/null +++ b/vendor/github.com/pquerna/otp/NOTICE @@ -0,0 +1,5 @@ +otp +Copyright (c) 2014, Paul Querna + +This product includes software developed by +Paul Querna (http://paul.querna.org/). diff --git a/vendor/github.com/pquerna/otp/README.md b/vendor/github.com/pquerna/otp/README.md new file mode 100644 index 000000000000..148e8980d6d1 --- /dev/null +++ b/vendor/github.com/pquerna/otp/README.md @@ -0,0 +1,60 @@ +# otp: One Time Password utilities Go / Golang + +[![GoDoc](https://godoc.org/github.com/pquerna/otp?status.svg)](https://godoc.org/github.com/pquerna/otp) [![Build Status](https://travis-ci.org/pquerna/otp.svg?branch=master)](https://travis-ci.org/pquerna/otp) + +# Why One Time Passwords? + +One Time Passwords (OTPs) are an mechanism to improve security over passwords alone. When a Time-based OTP (TOTP) is stored on a user's phone, and combined with something the user knows (Password), you have an easy on-ramp to [Multi-factor authentication](http://en.wikipedia.org/wiki/Multi-factor_authentication) without adding a dependency on a SMS provider. This Password and TOTP combination is used by many popular websites including Google, Github, Facebook, Salesforce and many others. + +The `otp` library enables you to easily add TOTPs to your own application, increasing your user's security against mass-password breaches and malware. + +Because TOTP is standardized and widely deployed, there are many [mobile clients and software implementations](http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm#Client_implementations). + +## `otp` Supports: + +* Generating QR Code images for easy user enrollment. +* Time-based One-time Password Algorithm (TOTP) (RFC 6238): Time based OTP, the most commonly used method. +* HMAC-based One-time Password Algorithm (HOTP) (RFC 4226): Counter based OTP, which TOTP is based upon. +* Generation and Validation of codes for either algorithm. + +## Implementing TOTP in your application: + +### User Enrollment + +For an example of a working enrollment work flow, [Github has documented theirs](https://help.github.com/articles/configuring-two-factor-authentication-via-a-totp-mobile-app/ +), but the basics are: + +1. Generate new TOTP Key for a User. `key,_ := totp.Generate(...)`. +1. Display the Key's Secret and QR-Code for the User. `key.Secret()` and `key.Image(...)`. +1. Test that the user can successfully use their TOTP. `totp.Validate(...)`. +1. Store TOTP Secret for the User in your backend. `key.Secret()` +1. Provide the user with "recovery codes". (See Recovery Codes bellow) + +### Code Generation + +* In either TOTP or HOTP cases, use the `GenerateCode` function and a counter or + `time.Time` struct to generate a valid code compatible with most implementations. +* For uncommon or custom settings, or to catch unlikely errors, use `GenerateCodeCustom` + in either module. + +### Validation + +1. Prompt and validate User's password as normal. +1. If the user has TOTP enabled, prompt for TOTP passcode. +1. Retrieve the User's TOTP Secret from your backend. +1. Validate the user's passcode. `totp.Validate(...)` + + +### Recovery Codes + +When a user loses access to their TOTP device, they would no longer have access to their account. Because TOTPs are often configured on mobile devices that can be lost, stolen or damaged, this is a common problem. For this reason many providers give their users "backup codes" or "recovery codes". These are a set of one time use codes that can be used instead of the TOTP. These can simply be randomly generated strings that you store in your backend. [Github's documentation provides an overview of the user experience]( +https://help.github.com/articles/downloading-your-two-factor-authentication-recovery-codes/). + + +## Improvements, bugs, adding feature, etc: + +Please [open issues in Github](https://github.com/pquerna/otp/issues) for ideas, bugs, and general thoughts. Pull requests are of course preferred :) + +## License + +`otp` is licensed under the [Apache License, Version 2.0](./LICENSE) diff --git a/vendor/github.com/pquerna/otp/doc.go b/vendor/github.com/pquerna/otp/doc.go new file mode 100644 index 000000000000..b8b4c8cc193f --- /dev/null +++ b/vendor/github.com/pquerna/otp/doc.go @@ -0,0 +1,70 @@ +/** + * Copyright 2014 Paul Querna + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Package otp implements both HOTP and TOTP based +// one time passcodes in a Google Authenticator compatible manner. +// +// When adding a TOTP for a user, you must store the "secret" value +// persistently. It is recommend to store the secret in an encrypted field in your +// datastore. Due to how TOTP works, it is not possible to store a hash +// for the secret value like you would a password. +// +// To enroll a user, you must first generate an OTP for them. Google +// Authenticator supports using a QR code as an enrollment method: +// +// import ( +// "github.com/pquerna/otp/totp" +// +// "bytes" +// "image/png" +// ) +// +// key, err := totp.Generate(totp.GenerateOpts{ +// Issuer: "Example.com", +// AccountName: "alice@example.com", +// }) +// +// // Convert TOTP key into a QR code encoded as a PNG image. +// var buf bytes.Buffer +// img, err := key.Image(200, 200) +// png.Encode(&buf, img) +// +// // display the QR code to the user. +// display(buf.Bytes()) +// +// // Now Validate that the user's successfully added the passcode. +// passcode := promptForPasscode() +// valid := totp.Validate(passcode, key.Secret()) +// +// if valid { +// // User successfully used their TOTP, save it to your backend! +// storeSecret("alice@example.com", key.Secret()) +// } +// +// Validating a TOTP passcode is very easy, just prompt the user for a passcode +// and retrieve the associated user's previously stored secret. +// import "github.com/pquerna/otp/totp" +// +// passcode := promptForPasscode() +// secret := getSecret("alice@example.com") +// +// valid := totp.Validate(passcode, secret) +// +// if valid { +// // Success! continue login process. +// } +package otp diff --git a/vendor/github.com/pquerna/otp/hotp/hotp.go b/vendor/github.com/pquerna/otp/hotp/hotp.go new file mode 100644 index 000000000000..ced7d8e28d2d --- /dev/null +++ b/vendor/github.com/pquerna/otp/hotp/hotp.go @@ -0,0 +1,187 @@ +/** + * Copyright 2014 Paul Querna + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package hotp + +import ( + "github.com/pquerna/otp" + + "crypto/hmac" + "crypto/rand" + "crypto/subtle" + "encoding/base32" + "encoding/binary" + "fmt" + "math" + "net/url" + "strings" +) + +const debug = false + +// Validate a HOTP passcode given a counter and secret. +// This is a shortcut for ValidateCustom, with parameters that +// are compataible with Google-Authenticator. +func Validate(passcode string, counter uint64, secret string) bool { + rv, _ := ValidateCustom( + passcode, + counter, + secret, + ValidateOpts{ + Digits: otp.DigitsSix, + Algorithm: otp.AlgorithmSHA1, + }, + ) + return rv +} + +// ValidateOpts provides options for ValidateCustom(). +type ValidateOpts struct { + // Digits as part of the input. Defaults to 6. + Digits otp.Digits + // Algorithm to use for HMAC. Defaults to SHA1. + Algorithm otp.Algorithm +} + +// GenerateCode creates a HOTP passcode given a counter and secret. +// This is a shortcut for GenerateCodeCustom, with parameters that +// are compataible with Google-Authenticator. +func GenerateCode(secret string, counter uint64) (string, error) { + return GenerateCodeCustom(secret, counter, ValidateOpts{ + Digits: otp.DigitsSix, + Algorithm: otp.AlgorithmSHA1, + }) +} + +// GenerateCodeCustom uses a counter and secret value and options struct to +// create a passcode. +func GenerateCodeCustom(secret string, counter uint64, opts ValidateOpts) (passcode string, err error) { + // As noted in issue #10 this adds support for TOTP secrets that are + // missing their padding. + if n := len(secret) % 8; n != 0 { + secret = secret + strings.Repeat("=", 8-n) + } + + secretBytes, err := base32.StdEncoding.DecodeString(secret) + if err != nil { + return "", otp.ErrValidateSecretInvalidBase32 + } + + buf := make([]byte, 8) + mac := hmac.New(opts.Algorithm.Hash, secretBytes) + binary.BigEndian.PutUint64(buf, counter) + if debug { + fmt.Printf("counter=%v\n", counter) + fmt.Printf("buf=%v\n", buf) + } + + mac.Write(buf) + sum := mac.Sum(nil) + + // "Dynamic truncation" in RFC 4226 + // http://tools.ietf.org/html/rfc4226#section-5.4 + offset := sum[len(sum)-1] & 0xf + value := int64(((int(sum[offset]) & 0x7f) << 24) | + ((int(sum[offset+1] & 0xff)) << 16) | + ((int(sum[offset+2] & 0xff)) << 8) | + (int(sum[offset+3]) & 0xff)) + + l := opts.Digits.Length() + mod := int32(value % int64(math.Pow10(l))) + + if debug { + fmt.Printf("offset=%v\n", offset) + fmt.Printf("value=%v\n", value) + fmt.Printf("mod'ed=%v\n", mod) + } + + return opts.Digits.Format(mod), nil +} + +// ValidateCustom validates an HOTP with customizable options. Most users should +// use Validate(). +func ValidateCustom(passcode string, counter uint64, secret string, opts ValidateOpts) (bool, error) { + passcode = strings.TrimSpace(passcode) + + if len(passcode) != opts.Digits.Length() { + return false, otp.ErrValidateInputInvalidLength + } + + otpstr, err := GenerateCodeCustom(secret, counter, opts) + if err != nil { + return false, err + } + + if subtle.ConstantTimeCompare([]byte(otpstr), []byte(passcode)) == 1 { + return true, nil + } + + return false, nil +} + +// GenerateOpts provides options for .Generate() +type GenerateOpts struct { + // Name of the issuing Organization/Company. + Issuer string + // Name of the User's Account (eg, email address) + AccountName string + // Size in size of the generated Secret. Defaults to 10 bytes. + SecretSize uint + // Digits to request. Defaults to 6. + Digits otp.Digits + // Algorithm to use for HMAC. Defaults to SHA1. + Algorithm otp.Algorithm +} + +// Generate creates a new HOTP Key. +func Generate(opts GenerateOpts) (*otp.Key, error) { + // url encode the Issuer/AccountName + if opts.Issuer == "" { + return nil, otp.ErrGenerateMissingIssuer + } + + if opts.AccountName == "" { + return nil, otp.ErrGenerateMissingAccountName + } + + if opts.SecretSize == 0 { + opts.SecretSize = 10 + } + + // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example + + v := url.Values{} + secret := make([]byte, opts.SecretSize) + _, err := rand.Read(secret) + if err != nil { + return nil, err + } + + v.Set("secret", base32.StdEncoding.EncodeToString(secret)) + v.Set("issuer", opts.Issuer) + v.Set("algorithm", opts.Algorithm.String()) + v.Set("digits", opts.Digits.String()) + + u := url.URL{ + Scheme: "otpauth", + Host: "hotp", + Path: "/" + opts.Issuer + ":" + opts.AccountName, + RawQuery: v.Encode(), + } + + return otp.NewKeyFromURL(u.String()) +} diff --git a/vendor/github.com/pquerna/otp/otp.go b/vendor/github.com/pquerna/otp/otp.go new file mode 100644 index 000000000000..0fa970927347 --- /dev/null +++ b/vendor/github.com/pquerna/otp/otp.go @@ -0,0 +1,200 @@ +/** + * Copyright 2014 Paul Querna + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package otp + +import ( + "github.com/boombuler/barcode" + "github.com/boombuler/barcode/qr" + + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "errors" + "fmt" + "hash" + "image" + "net/url" + "strings" +) + +// Error when attempting to convert the secret from base32 to raw bytes. +var ErrValidateSecretInvalidBase32 = errors.New("Decoding of secret as base32 failed.") + +// The user provided passcode length was not expected. +var ErrValidateInputInvalidLength = errors.New("Input length unexpected") + +// When generating a Key, the Issuer must be set. +var ErrGenerateMissingIssuer = errors.New("Issuer must be set") + +// When generating a Key, the Account Name must be set. +var ErrGenerateMissingAccountName = errors.New("AccountName must be set") + +// Key represents an TOTP or HTOP key. +type Key struct { + orig string + url *url.URL +} + +// NewKeyFromURL creates a new Key from an TOTP or HOTP url. +// +// The URL format is documented here: +// https://github.com/google/google-authenticator/wiki/Key-Uri-Format +// +func NewKeyFromURL(orig string) (*Key, error) { + u, err := url.Parse(orig) + + if err != nil { + return nil, err + } + + return &Key{ + orig: orig, + url: u, + }, nil +} + +func (k *Key) String() string { + return k.orig +} + +// Image returns an QR-Code image of the specified width and height, +// suitable for use by many clients like Google-Authenricator +// to enroll a user's TOTP/HOTP key. +func (k *Key) Image(width int, height int) (image.Image, error) { + b, err := qr.Encode(k.orig, qr.M, qr.Auto) + + if err != nil { + return nil, err + } + + b, err = barcode.Scale(b, width, height) + + if err != nil { + return nil, err + } + + return b, nil +} + +// Type returns "hotp" or "totp". +func (k *Key) Type() string { + return k.url.Host +} + +// Issuer returns the name of the issuing organization. +func (k *Key) Issuer() string { + q := k.url.Query() + + issuer := q.Get("issuer") + + if issuer != "" { + return issuer + } + + p := strings.TrimPrefix(k.url.Path, "/") + i := strings.Index(p, ":") + + if i == -1 { + return "" + } + + return p[:i] +} + +// AccountName returns the name of the user's account. +func (k *Key) AccountName() string { + p := strings.TrimPrefix(k.url.Path, "/") + i := strings.Index(p, ":") + + if i == -1 { + return p + } + + return p[i+1:] +} + +// Secret returns the opaque secret for this Key. +func (k *Key) Secret() string { + q := k.url.Query() + + return q.Get("secret") +} + +// Algorithm represents the hashing function to use in the HMAC +// operation needed for OTPs. +type Algorithm int + +const ( + AlgorithmSHA1 Algorithm = iota + AlgorithmSHA256 + AlgorithmSHA512 + AlgorithmMD5 +) + +func (a Algorithm) String() string { + switch a { + case AlgorithmSHA1: + return "SHA1" + case AlgorithmSHA256: + return "SHA256" + case AlgorithmSHA512: + return "SHA512" + case AlgorithmMD5: + return "MD5" + } + panic("unreached") +} + +func (a Algorithm) Hash() hash.Hash { + switch a { + case AlgorithmSHA1: + return sha1.New() + case AlgorithmSHA256: + return sha256.New() + case AlgorithmSHA512: + return sha512.New() + case AlgorithmMD5: + return md5.New() + } + panic("unreached") +} + +// Digits represents the number of digits present in the +// user's OTP passcode. Six and Eight are the most common values. +type Digits int + +const ( + DigitsSix Digits = 6 + DigitsEight Digits = 8 +) + +// Format converts an integer into the zero-filled size for this Digits. +func (d Digits) Format(in int32) string { + f := fmt.Sprintf("%%0%dd", d) + return fmt.Sprintf(f, in) +} + +// Length returns the number of characters for this Digits. +func (d Digits) Length() int { + return int(d) +} + +func (d Digits) String() string { + return fmt.Sprintf("%d", d) +} diff --git a/vendor/github.com/pquerna/otp/totp/totp.go b/vendor/github.com/pquerna/otp/totp/totp.go new file mode 100644 index 000000000000..af5ab8296717 --- /dev/null +++ b/vendor/github.com/pquerna/otp/totp/totp.go @@ -0,0 +1,191 @@ +/** + * Copyright 2014 Paul Querna + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package totp + +import ( + "github.com/pquerna/otp" + "github.com/pquerna/otp/hotp" + + "crypto/rand" + "encoding/base32" + "math" + "net/url" + "strconv" + "time" +) + +// Validate a TOTP using the current time. +// A shortcut for ValidateCustom, Validate uses a configuration +// that is compatible with Google-Authenticator and most clients. +func Validate(passcode string, secret string) bool { + rv, _ := ValidateCustom( + passcode, + secret, + time.Now().UTC(), + ValidateOpts{ + Period: 30, + Skew: 1, + Digits: otp.DigitsSix, + Algorithm: otp.AlgorithmSHA1, + }, + ) + return rv +} + +// GenerateCode creates a TOTP token using the current time. +// A shortcut for GenerateCodeCustom, GenerateCode uses a configuration +// that is compatible with Google-Authenticator and most clients. +func GenerateCode(secret string, t time.Time) (string, error) { + return GenerateCodeCustom(secret, t, ValidateOpts{ + Period: 30, + Skew: 1, + Digits: otp.DigitsSix, + Algorithm: otp.AlgorithmSHA1, + }) +} + +// ValidateOpts provides options for ValidateCustom(). +type ValidateOpts struct { + // Number of seconds a TOTP hash is valid for. Defaults to 30 seconds. + Period uint + // Periods before or after the current time to allow. Value of 1 allows up to Period + // of either side of the specified time. Defaults to 0 allowed skews. Values greater + // than 1 are likely sketchy. + Skew uint + // Digits as part of the input. Defaults to 6. + Digits otp.Digits + // Algorithm to use for HMAC. Defaults to SHA1. + Algorithm otp.Algorithm +} + +// GenerateCodeCustom takes a timepoint and produces a passcode using a +// secret and the provided opts. (Under the hood, this is making an adapted +// call to hotp.GenerateCodeCustom) +func GenerateCodeCustom(secret string, t time.Time, opts ValidateOpts) (passcode string, err error) { + if opts.Period == 0 { + opts.Period = 30 + } + counter := uint64(math.Floor(float64(t.Unix()) / float64(opts.Period))) + passcode, err = hotp.GenerateCodeCustom(secret, counter, hotp.ValidateOpts{ + Digits: opts.Digits, + Algorithm: opts.Algorithm, + }) + if err != nil { + return "", err + } + return passcode, nil +} + +// ValidateCustom validates a TOTP given a user specified time and custom options. +// Most users should use Validate() to provide an interpolatable TOTP experience. +func ValidateCustom(passcode string, secret string, t time.Time, opts ValidateOpts) (bool, error) { + if opts.Period == 0 { + opts.Period = 30 + } + + counters := []uint64{} + counter := int64(math.Floor(float64(t.Unix()) / float64(opts.Period))) + + counters = append(counters, uint64(counter)) + for i := 1; i <= int(opts.Skew); i++ { + counters = append(counters, uint64(counter+int64(i))) + counters = append(counters, uint64(counter-int64(i))) + } + + for _, counter := range counters { + rv, err := hotp.ValidateCustom(passcode, counter, secret, hotp.ValidateOpts{ + Digits: opts.Digits, + Algorithm: opts.Algorithm, + }) + + if err != nil { + return false, err + } + + if rv == true { + return true, nil + } + } + + return false, nil +} + +// GenerateOpts provides options for Generate(). The default values +// are compatible with Google-Authenticator. +type GenerateOpts struct { + // Name of the issuing Organization/Company. + Issuer string + // Name of the User's Account (eg, email address) + AccountName string + // Number of seconds a TOTP hash is valid for. Defaults to 30 seconds. + Period uint + // Size in size of the generated Secret. Defaults to 10 bytes. + SecretSize uint + // Digits to request. Defaults to 6. + Digits otp.Digits + // Algorithm to use for HMAC. Defaults to SHA1. + Algorithm otp.Algorithm +} + +// Generate a new TOTP Key. +func Generate(opts GenerateOpts) (*otp.Key, error) { + // url encode the Issuer/AccountName + if opts.Issuer == "" { + return nil, otp.ErrGenerateMissingIssuer + } + + if opts.AccountName == "" { + return nil, otp.ErrGenerateMissingAccountName + } + + if opts.Period == 0 { + opts.Period = 30 + } + + if opts.SecretSize == 0 { + opts.SecretSize = 10 + } + + if opts.Digits == 0 { + opts.Digits = otp.DigitsSix + } + + // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example + + v := url.Values{} + secret := make([]byte, opts.SecretSize) + _, err := rand.Read(secret) + if err != nil { + return nil, err + } + + v.Set("secret", base32.StdEncoding.EncodeToString(secret)) + v.Set("issuer", opts.Issuer) + v.Set("period", strconv.FormatUint(uint64(opts.Period), 10)) + v.Set("algorithm", opts.Algorithm.String()) + v.Set("digits", opts.Digits.String()) + + u := url.URL{ + Scheme: "otpauth", + Host: "totp", + Path: "/" + opts.Issuer + ":" + opts.AccountName, + RawQuery: v.Encode(), + } + + return otp.NewKeyFromURL(u.String()) +} From fb98943a13d3d81b29cf31bd58b8ed1f7acb3dd4 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Tue, 7 Mar 2017 12:59:03 -0800 Subject: [PATCH 13/43] Added test steps and cleaned up errors --- builtin/logical/totp/backend_test.go | 182 ++++++++++------------- builtin/logical/totp/path_role_create.go | 1 + builtin/logical/totp/path_roles.go | 13 -- 3 files changed, 81 insertions(+), 115 deletions(-) diff --git a/builtin/logical/totp/backend_test.go b/builtin/logical/totp/backend_test.go index a97598d099a0..b736a587686d 100644 --- a/builtin/logical/totp/backend_test.go +++ b/builtin/logical/totp/backend_test.go @@ -3,19 +3,36 @@ package totp import ( "fmt" "log" + "path" "testing" - "time" "github.com/hashicorp/vault/logical" logicaltest "github.com/hashicorp/vault/logical/testing" "github.com/mitchellh/mapstructure" - otplib "github.com/pquerna/otp" totplib "github.com/pquerna/otp/totp" ) -var ( - masterKey string -) +/* + Test each algorithm type + Test digits + Test periods + Test defaults + Test invalid period (negative) + Test invalid key + Test invalid account_name + Test invalid issuer +*/ + +func createKey() (string, error) { + keyUrl, err := totplib.Generate(totplib.GenerateOpts{ + Issuer: "Vault", + AccountName: "Test", + }) + + key := keyUrl.Secret() + + return key, err +} func TestBackend_basic(t *testing.T) { config := logical.TestBackendConfig() @@ -25,12 +42,20 @@ func TestBackend_basic(t *testing.T) { t.Fatal(err) } + // Generate a new shared key + key := createKey() + + roleData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "key": key, + } + logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ - testAccStepConfig(t, connData, false), - testAccStepCreateRole(t, "web", testRole, false), - testAccStepReadCreds(t, b, config.StorageView, "web", connURL), + testAccStepCreateRole(t, "test", roleData, false), + testAccStepReadCreds(t, b, config.StorageView, "test", key), }, }) } @@ -43,38 +68,39 @@ func TestBackend_roleCrud(t *testing.T) { t.Fatal(err) } + key := createKey() + + roleData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "key": key, + } + + expected := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "digits": 6, + "period": 30, + "algorithm": "SHA1", + } + logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ - testAccStepConfig(t, connData, false), - testAccStepCreateRole(t, "web", testRole, false), - testAccStepReadRole(t, "web", testRole), - testAccStepDeleteRole(t, "web"), - testAccStepReadRole(t, "web", ""), + testAccStepCreateRole(t, "test", roleData, false), + testAccStepReadRole(t, "test", expected), + testAccStepDeleteRole(t, "test"), + testAccStepReadRole(t, "test", ""), }, }) } -func testAccStepCreateRole(t *testing.T, name string, sql string, expectFail bool) logicaltest.TestStep { +func testAccStepCreateRole(t *testing.T, name string, data map[string]interface{}, expectFail bool) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: path.Join("roles", name), - Data: map[string]interface{}{ - "sql": sql, - }, - ErrorOk: expectFail, - } -} - -func testAccStepCreateRoleWithRevocationSQL(t *testing.T, name, sql, revocationSQL string, expectFail bool) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: path.Join("roles", name), - Data: map[string]interface{}{ - "sql": sql, - "revocation_sql": revocationSQL, - }, - ErrorOk: expectFail, + Data: data, + ErrorOk: expectFail, } } @@ -85,84 +111,23 @@ func testAccStepDeleteRole(t *testing.T, name string) logicaltest.TestStep { } } -func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, name string, connURL string) logicaltest.TestStep { +func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, name string, key string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.ReadOperation, Path: path.Join("creds", name), Check: func(resp *logical.Response) error { var d struct { - Username string `mapstructure:"username"` - Password string `mapstructure:"password"` + Token string `mapstructure:"token"` } if err := mapstructure.Decode(resp.Data, &d); err != nil { return err } log.Printf("[TRACE] Generated credentials: %v", d) - conn, err := pq.ParseURL(connURL) - if err != nil { - t.Fatal(err) - } + valid := totplib.Validate(d.Token, key) - conn += " timezone=utc" - - db, err := sql.Open("postgres", conn) - if err != nil { - t.Fatal(err) - } - - returnedRows := func() int { - stmt, err := db.Prepare("SELECT DISTINCT schemaname FROM pg_tables WHERE has_table_privilege($1, 'information_schema.role_column_grants', 'select');") - if err != nil { - return -1 - } - defer stmt.Close() - - rows, err := stmt.Query(d.Username) - if err != nil { - return -1 - } - defer rows.Close() - - i := 0 - for rows.Next() { - i++ - } - return i - } - - // minNumPermissions is the minimum number of permissions that will always be present. - const minNumPermissions = 2 - - userRows := returnedRows() - if userRows < minNumPermissions { - t.Fatalf("did not get expected number of rows, got %d", userRows) - } - - resp, err = b.HandleRequest(&logical.Request{ - Operation: logical.RevokeOperation, - Storage: s, - Secret: &logical.Secret{ - InternalData: map[string]interface{}{ - "secret_type": "creds", - "username": d.Username, - "role": name, - }, - }, - }) - if err != nil { - return err - } - if resp != nil { - if resp.IsError() { - return fmt.Errorf("Error on resp: %#v", *resp) - } - } - - userRows = returnedRows() - // User shouldn't exist so returnedRows() should encounter an error and exit with -1 - if userRows != -1 { - t.Fatalf("did not get expected number of rows, got %d", userRows) + if !valid { + t.Fatalf("Generated token isn't valid.") } return nil @@ -170,28 +135,41 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, na } } -func testAccStepReadRole(t *testing.T, name string, sql string) logicaltest.TestStep { +func testAccStepReadRole(t *testing.T, name string, expected map[string]interface{}) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.ReadOperation, Path: "roles/" + name, Check: func(resp *logical.Response) error { if resp == nil { - if sql == "" { + if expected == nil { return nil } - return fmt.Errorf("bad: %#v", resp) } var d struct { - SQL string `mapstructure:"sql"` + Issuer string `mapstructure:"issuer"` + Account_Name string `mapstructure:"account_name"` + Period int `mapstructure:"period"` + Algorithm string `mapstructure:"algorithm"` + Digits int `mapstructure:"digits"` } + if err := mapstructure.Decode(resp.Data, &d); err != nil { return err } - if d.SQL != sql { - return fmt.Errorf("bad: %#v", resp) + switch { + case d.Issuer != expected.Get("issuer"): + return fmt.Errorf("Issuer should equal: %s", expected.Get("issuer")) + case d.Account_Name != expected.Get("account_name"): + return fmt.Errorf("Account_Name should equal: %s", expected.Get("account_name")) + case d.Period != expected.Get("period"): + return fmt.Errorf("Period should equal: %i", expected.Get("period")) + case d.Algorithm != expected.Get("algorithm"): + return fmt.Errorf("Algorithm should equal: %s", expected.Get("algorithm")) + case d.Digits != expected.Get("digits"): + return fmt.Errorf("Digits should equal: %i", expected.Get("digits")) } return nil diff --git a/builtin/logical/totp/path_role_create.go b/builtin/logical/totp/path_role_create.go index 750985318800..336ffa6f3598 100644 --- a/builtin/logical/totp/path_role_create.go +++ b/builtin/logical/totp/path_role_create.go @@ -99,4 +99,5 @@ Request time-based one-time use password for a certain role. ` const pathRoleCreateReadHelpDesc = ` This path generates a time-based one-time use password for a certain role. + ` diff --git a/builtin/logical/totp/path_roles.go b/builtin/logical/totp/path_roles.go index dc1e3f3b4185..c6d7349562bb 100644 --- a/builtin/logical/totp/path_roles.go +++ b/builtin/logical/totp/path_roles.go @@ -221,17 +221,4 @@ Manage the roles that can be created with this backend. const pathRoleHelpDesc = ` This path lets you manage the roles that can be created with this backend. -Role Parameters: - - * "key" - required - The shared master key used to generate a TOTP token. - - * "issuer" - required - The name of the key's issuing organization. - - * "account_name" - required - The name of the account associated with the key. - - * "period" - optional - The length of time used to generate a counter for the TOTP token calculation. Default value is 30 seconds. - - * "algorithm" - optional - The hashing algorithm used to generate the TOTP token. Default value is "SHA1". Other options include "SHA256", "SHA512" and "MD5". - - * "digits" - optional - The number of digits in the generated TOTP token. Default value is 6. Options include 6 or 8. ` From 1a7fe62aba37d8539dee6de6c58dd4853e8af3fa Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Fri, 10 Mar 2017 11:48:39 -0800 Subject: [PATCH 14/43] Modified read credential test step, not working yet --- builtin/logical/totp/backend_test.go | 80 +++++++++++++++++++++------- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/builtin/logical/totp/backend_test.go b/builtin/logical/totp/backend_test.go index b736a587686d..c0b3e7bb826e 100644 --- a/builtin/logical/totp/backend_test.go +++ b/builtin/logical/totp/backend_test.go @@ -5,10 +5,12 @@ import ( "log" "path" "testing" + "time" "github.com/hashicorp/vault/logical" logicaltest "github.com/hashicorp/vault/logical/testing" "github.com/mitchellh/mapstructure" + //otplib "github.com/pquerna/otp/" totplib "github.com/pquerna/otp/totp" ) @@ -43,7 +45,7 @@ func TestBackend_basic(t *testing.T) { } // Generate a new shared key - key := createKey() + key, _ := createKey() roleData := map[string]interface{}{ "issuer": "Vault", @@ -55,12 +57,12 @@ func TestBackend_basic(t *testing.T) { Backend: b, Steps: []logicaltest.TestStep{ testAccStepCreateRole(t, "test", roleData, false), - testAccStepReadCreds(t, b, config.StorageView, "test", key), + testAccStepReadCreds(t, b, config.StorageView, "test"), }, }) } -func TestBackend_roleCrud(t *testing.T) { +func TestBackend_roleCrudDefaultValues(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(config) @@ -68,7 +70,7 @@ func TestBackend_roleCrud(t *testing.T) { t.Fatal(err) } - key := createKey() + key, _ := createKey() roleData := map[string]interface{}{ "issuer": "Vault", @@ -90,16 +92,16 @@ func TestBackend_roleCrud(t *testing.T) { testAccStepCreateRole(t, "test", roleData, false), testAccStepReadRole(t, "test", expected), testAccStepDeleteRole(t, "test"), - testAccStepReadRole(t, "test", ""), + testAccStepReadRole(t, "test", nil), }, }) } -func testAccStepCreateRole(t *testing.T, name string, data map[string]interface{}, expectFail bool) logicaltest.TestStep { +func testAccStepCreateRole(t *testing.T, name string, roleData map[string]interface{}, expectFail bool) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: path.Join("roles", name), - Data: data, + Data: roleData, ErrorOk: expectFail, } } @@ -111,7 +113,7 @@ func testAccStepDeleteRole(t *testing.T, name string) logicaltest.TestStep { } } -func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, name string, key string) logicaltest.TestStep { +func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, name string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.ReadOperation, Path: path.Join("creds", name), @@ -124,7 +126,47 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, na } log.Printf("[TRACE] Generated credentials: %v", d) - valid := totplib.Validate(d.Token, key) + role, err := (backend) b.Role(s, name) + + if err != nil { + t.Fatalf("Error retrieving role.") + } + + if role == nil { + t.Fatalf("Retrieved role is nil.") + } + + // Translate digits and algorithm to a format the totp library understands + var digits otplib.Digits + switch role.Digits { + case 6: + digits = otplib.DigitsSix + case 8: + digits = otplib.DigitsEight + } + + var algorithm otplib.Algorithm + switch role.Algorithm { + case "SHA1": + algorithm = otplib.AlgorithmSHA1 + case "SHA256": + algorithm = otplib.AlgorithmSHA256 + case "SHA512": + algorithm = otplib.AlgorithmSHA512 + case "MD5": + algorithm = otplib.AlgorithmMD5 + default: + algorithm = otplib.AlgorithmSHA1 + } + + period := uint(role.Period) + + valid := totplib.ValidateCustom(d.Token, role.Key, time.Now().UTC(), totplib.ValidateOpts{ + Period: period, + Skew: 1, + Digits: digits, + Algorithm: algorithm, + }) if !valid { t.Fatalf("Generated token isn't valid.") @@ -160,16 +202,16 @@ func testAccStepReadRole(t *testing.T, name string, expected map[string]interfac } switch { - case d.Issuer != expected.Get("issuer"): - return fmt.Errorf("Issuer should equal: %s", expected.Get("issuer")) - case d.Account_Name != expected.Get("account_name"): - return fmt.Errorf("Account_Name should equal: %s", expected.Get("account_name")) - case d.Period != expected.Get("period"): - return fmt.Errorf("Period should equal: %i", expected.Get("period")) - case d.Algorithm != expected.Get("algorithm"): - return fmt.Errorf("Algorithm should equal: %s", expected.Get("algorithm")) - case d.Digits != expected.Get("digits"): - return fmt.Errorf("Digits should equal: %i", expected.Get("digits")) + case d.Issuer != expected["issuer"]: + return fmt.Errorf("Issuer should equal: %s", expected["issuer"]) + case d.Account_Name != expected["account_name"]: + return fmt.Errorf("Account_Name should equal: %s", expected["account_name"]) + case d.Period != expected["period"]: + return fmt.Errorf("Period should equal: %i", expected["period"]) + case d.Algorithm != expected["algorithm"]: + return fmt.Errorf("Algorithm should equal: %s", expected["algorithm"]) + case d.Digits != expected["digits"]: + return fmt.Errorf("Digits should equal: %i", expected["digits"]) } return nil From 32690562035ba4f8c9b83dc648fd8b60c9acae90 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Fri, 10 Mar 2017 12:04:32 -0800 Subject: [PATCH 15/43] Use of vendored package not allowed - Test error --- builtin/logical/totp/backend_test.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/builtin/logical/totp/backend_test.go b/builtin/logical/totp/backend_test.go index c0b3e7bb826e..d8d3cf19d6a5 100644 --- a/builtin/logical/totp/backend_test.go +++ b/builtin/logical/totp/backend_test.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/vault/logical" logicaltest "github.com/hashicorp/vault/logical/testing" "github.com/mitchellh/mapstructure" - //otplib "github.com/pquerna/otp/" + otplib "github.com/pquerna/otp/" totplib "github.com/pquerna/otp/totp" ) @@ -126,16 +126,21 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, na } log.Printf("[TRACE] Generated credentials: %v", d) - role, err := (backend) b.Role(s, name) + // Read saved role entry + entry, err := s.Get("role/" + name) if err != nil { t.Fatalf("Error retrieving role.") } - - if role == nil { + if entry == nil { t.Fatalf("Retrieved role is nil.") } + var role roleEntry + if err := entry.DecodeJSON(&role); err != nil { + t.Fatalf("JSON decoding error while reading role entry.") + } + // Translate digits and algorithm to a format the totp library understands var digits otplib.Digits switch role.Digits { From fcd030994bc1741dbf490f3995944e091b11da61 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sat, 11 Mar 2017 17:32:24 -0800 Subject: [PATCH 16/43] Removed vendor files for TOTP library --- vendor/github.com/pquerna/otp/LICENSE | 202 --------------------- vendor/github.com/pquerna/otp/NOTICE | 5 - vendor/github.com/pquerna/otp/README.md | 60 ------ vendor/github.com/pquerna/otp/doc.go | 70 ------- vendor/github.com/pquerna/otp/hotp/hotp.go | 187 ------------------- vendor/github.com/pquerna/otp/otp.go | 200 -------------------- vendor/github.com/pquerna/otp/totp/totp.go | 191 ------------------- 7 files changed, 915 deletions(-) delete mode 100644 vendor/github.com/pquerna/otp/LICENSE delete mode 100644 vendor/github.com/pquerna/otp/NOTICE delete mode 100644 vendor/github.com/pquerna/otp/README.md delete mode 100644 vendor/github.com/pquerna/otp/doc.go delete mode 100644 vendor/github.com/pquerna/otp/hotp/hotp.go delete mode 100644 vendor/github.com/pquerna/otp/otp.go delete mode 100644 vendor/github.com/pquerna/otp/totp/totp.go diff --git a/vendor/github.com/pquerna/otp/LICENSE b/vendor/github.com/pquerna/otp/LICENSE deleted file mode 100644 index d64569567334..000000000000 --- a/vendor/github.com/pquerna/otp/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/vendor/github.com/pquerna/otp/NOTICE b/vendor/github.com/pquerna/otp/NOTICE deleted file mode 100644 index 50e2e75016eb..000000000000 --- a/vendor/github.com/pquerna/otp/NOTICE +++ /dev/null @@ -1,5 +0,0 @@ -otp -Copyright (c) 2014, Paul Querna - -This product includes software developed by -Paul Querna (http://paul.querna.org/). diff --git a/vendor/github.com/pquerna/otp/README.md b/vendor/github.com/pquerna/otp/README.md deleted file mode 100644 index 148e8980d6d1..000000000000 --- a/vendor/github.com/pquerna/otp/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# otp: One Time Password utilities Go / Golang - -[![GoDoc](https://godoc.org/github.com/pquerna/otp?status.svg)](https://godoc.org/github.com/pquerna/otp) [![Build Status](https://travis-ci.org/pquerna/otp.svg?branch=master)](https://travis-ci.org/pquerna/otp) - -# Why One Time Passwords? - -One Time Passwords (OTPs) are an mechanism to improve security over passwords alone. When a Time-based OTP (TOTP) is stored on a user's phone, and combined with something the user knows (Password), you have an easy on-ramp to [Multi-factor authentication](http://en.wikipedia.org/wiki/Multi-factor_authentication) without adding a dependency on a SMS provider. This Password and TOTP combination is used by many popular websites including Google, Github, Facebook, Salesforce and many others. - -The `otp` library enables you to easily add TOTPs to your own application, increasing your user's security against mass-password breaches and malware. - -Because TOTP is standardized and widely deployed, there are many [mobile clients and software implementations](http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm#Client_implementations). - -## `otp` Supports: - -* Generating QR Code images for easy user enrollment. -* Time-based One-time Password Algorithm (TOTP) (RFC 6238): Time based OTP, the most commonly used method. -* HMAC-based One-time Password Algorithm (HOTP) (RFC 4226): Counter based OTP, which TOTP is based upon. -* Generation and Validation of codes for either algorithm. - -## Implementing TOTP in your application: - -### User Enrollment - -For an example of a working enrollment work flow, [Github has documented theirs](https://help.github.com/articles/configuring-two-factor-authentication-via-a-totp-mobile-app/ -), but the basics are: - -1. Generate new TOTP Key for a User. `key,_ := totp.Generate(...)`. -1. Display the Key's Secret and QR-Code for the User. `key.Secret()` and `key.Image(...)`. -1. Test that the user can successfully use their TOTP. `totp.Validate(...)`. -1. Store TOTP Secret for the User in your backend. `key.Secret()` -1. Provide the user with "recovery codes". (See Recovery Codes bellow) - -### Code Generation - -* In either TOTP or HOTP cases, use the `GenerateCode` function and a counter or - `time.Time` struct to generate a valid code compatible with most implementations. -* For uncommon or custom settings, or to catch unlikely errors, use `GenerateCodeCustom` - in either module. - -### Validation - -1. Prompt and validate User's password as normal. -1. If the user has TOTP enabled, prompt for TOTP passcode. -1. Retrieve the User's TOTP Secret from your backend. -1. Validate the user's passcode. `totp.Validate(...)` - - -### Recovery Codes - -When a user loses access to their TOTP device, they would no longer have access to their account. Because TOTPs are often configured on mobile devices that can be lost, stolen or damaged, this is a common problem. For this reason many providers give their users "backup codes" or "recovery codes". These are a set of one time use codes that can be used instead of the TOTP. These can simply be randomly generated strings that you store in your backend. [Github's documentation provides an overview of the user experience]( -https://help.github.com/articles/downloading-your-two-factor-authentication-recovery-codes/). - - -## Improvements, bugs, adding feature, etc: - -Please [open issues in Github](https://github.com/pquerna/otp/issues) for ideas, bugs, and general thoughts. Pull requests are of course preferred :) - -## License - -`otp` is licensed under the [Apache License, Version 2.0](./LICENSE) diff --git a/vendor/github.com/pquerna/otp/doc.go b/vendor/github.com/pquerna/otp/doc.go deleted file mode 100644 index b8b4c8cc193f..000000000000 --- a/vendor/github.com/pquerna/otp/doc.go +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright 2014 Paul Querna - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -// Package otp implements both HOTP and TOTP based -// one time passcodes in a Google Authenticator compatible manner. -// -// When adding a TOTP for a user, you must store the "secret" value -// persistently. It is recommend to store the secret in an encrypted field in your -// datastore. Due to how TOTP works, it is not possible to store a hash -// for the secret value like you would a password. -// -// To enroll a user, you must first generate an OTP for them. Google -// Authenticator supports using a QR code as an enrollment method: -// -// import ( -// "github.com/pquerna/otp/totp" -// -// "bytes" -// "image/png" -// ) -// -// key, err := totp.Generate(totp.GenerateOpts{ -// Issuer: "Example.com", -// AccountName: "alice@example.com", -// }) -// -// // Convert TOTP key into a QR code encoded as a PNG image. -// var buf bytes.Buffer -// img, err := key.Image(200, 200) -// png.Encode(&buf, img) -// -// // display the QR code to the user. -// display(buf.Bytes()) -// -// // Now Validate that the user's successfully added the passcode. -// passcode := promptForPasscode() -// valid := totp.Validate(passcode, key.Secret()) -// -// if valid { -// // User successfully used their TOTP, save it to your backend! -// storeSecret("alice@example.com", key.Secret()) -// } -// -// Validating a TOTP passcode is very easy, just prompt the user for a passcode -// and retrieve the associated user's previously stored secret. -// import "github.com/pquerna/otp/totp" -// -// passcode := promptForPasscode() -// secret := getSecret("alice@example.com") -// -// valid := totp.Validate(passcode, secret) -// -// if valid { -// // Success! continue login process. -// } -package otp diff --git a/vendor/github.com/pquerna/otp/hotp/hotp.go b/vendor/github.com/pquerna/otp/hotp/hotp.go deleted file mode 100644 index ced7d8e28d2d..000000000000 --- a/vendor/github.com/pquerna/otp/hotp/hotp.go +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Copyright 2014 Paul Querna - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package hotp - -import ( - "github.com/pquerna/otp" - - "crypto/hmac" - "crypto/rand" - "crypto/subtle" - "encoding/base32" - "encoding/binary" - "fmt" - "math" - "net/url" - "strings" -) - -const debug = false - -// Validate a HOTP passcode given a counter and secret. -// This is a shortcut for ValidateCustom, with parameters that -// are compataible with Google-Authenticator. -func Validate(passcode string, counter uint64, secret string) bool { - rv, _ := ValidateCustom( - passcode, - counter, - secret, - ValidateOpts{ - Digits: otp.DigitsSix, - Algorithm: otp.AlgorithmSHA1, - }, - ) - return rv -} - -// ValidateOpts provides options for ValidateCustom(). -type ValidateOpts struct { - // Digits as part of the input. Defaults to 6. - Digits otp.Digits - // Algorithm to use for HMAC. Defaults to SHA1. - Algorithm otp.Algorithm -} - -// GenerateCode creates a HOTP passcode given a counter and secret. -// This is a shortcut for GenerateCodeCustom, with parameters that -// are compataible with Google-Authenticator. -func GenerateCode(secret string, counter uint64) (string, error) { - return GenerateCodeCustom(secret, counter, ValidateOpts{ - Digits: otp.DigitsSix, - Algorithm: otp.AlgorithmSHA1, - }) -} - -// GenerateCodeCustom uses a counter and secret value and options struct to -// create a passcode. -func GenerateCodeCustom(secret string, counter uint64, opts ValidateOpts) (passcode string, err error) { - // As noted in issue #10 this adds support for TOTP secrets that are - // missing their padding. - if n := len(secret) % 8; n != 0 { - secret = secret + strings.Repeat("=", 8-n) - } - - secretBytes, err := base32.StdEncoding.DecodeString(secret) - if err != nil { - return "", otp.ErrValidateSecretInvalidBase32 - } - - buf := make([]byte, 8) - mac := hmac.New(opts.Algorithm.Hash, secretBytes) - binary.BigEndian.PutUint64(buf, counter) - if debug { - fmt.Printf("counter=%v\n", counter) - fmt.Printf("buf=%v\n", buf) - } - - mac.Write(buf) - sum := mac.Sum(nil) - - // "Dynamic truncation" in RFC 4226 - // http://tools.ietf.org/html/rfc4226#section-5.4 - offset := sum[len(sum)-1] & 0xf - value := int64(((int(sum[offset]) & 0x7f) << 24) | - ((int(sum[offset+1] & 0xff)) << 16) | - ((int(sum[offset+2] & 0xff)) << 8) | - (int(sum[offset+3]) & 0xff)) - - l := opts.Digits.Length() - mod := int32(value % int64(math.Pow10(l))) - - if debug { - fmt.Printf("offset=%v\n", offset) - fmt.Printf("value=%v\n", value) - fmt.Printf("mod'ed=%v\n", mod) - } - - return opts.Digits.Format(mod), nil -} - -// ValidateCustom validates an HOTP with customizable options. Most users should -// use Validate(). -func ValidateCustom(passcode string, counter uint64, secret string, opts ValidateOpts) (bool, error) { - passcode = strings.TrimSpace(passcode) - - if len(passcode) != opts.Digits.Length() { - return false, otp.ErrValidateInputInvalidLength - } - - otpstr, err := GenerateCodeCustom(secret, counter, opts) - if err != nil { - return false, err - } - - if subtle.ConstantTimeCompare([]byte(otpstr), []byte(passcode)) == 1 { - return true, nil - } - - return false, nil -} - -// GenerateOpts provides options for .Generate() -type GenerateOpts struct { - // Name of the issuing Organization/Company. - Issuer string - // Name of the User's Account (eg, email address) - AccountName string - // Size in size of the generated Secret. Defaults to 10 bytes. - SecretSize uint - // Digits to request. Defaults to 6. - Digits otp.Digits - // Algorithm to use for HMAC. Defaults to SHA1. - Algorithm otp.Algorithm -} - -// Generate creates a new HOTP Key. -func Generate(opts GenerateOpts) (*otp.Key, error) { - // url encode the Issuer/AccountName - if opts.Issuer == "" { - return nil, otp.ErrGenerateMissingIssuer - } - - if opts.AccountName == "" { - return nil, otp.ErrGenerateMissingAccountName - } - - if opts.SecretSize == 0 { - opts.SecretSize = 10 - } - - // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example - - v := url.Values{} - secret := make([]byte, opts.SecretSize) - _, err := rand.Read(secret) - if err != nil { - return nil, err - } - - v.Set("secret", base32.StdEncoding.EncodeToString(secret)) - v.Set("issuer", opts.Issuer) - v.Set("algorithm", opts.Algorithm.String()) - v.Set("digits", opts.Digits.String()) - - u := url.URL{ - Scheme: "otpauth", - Host: "hotp", - Path: "/" + opts.Issuer + ":" + opts.AccountName, - RawQuery: v.Encode(), - } - - return otp.NewKeyFromURL(u.String()) -} diff --git a/vendor/github.com/pquerna/otp/otp.go b/vendor/github.com/pquerna/otp/otp.go deleted file mode 100644 index 0fa970927347..000000000000 --- a/vendor/github.com/pquerna/otp/otp.go +++ /dev/null @@ -1,200 +0,0 @@ -/** - * Copyright 2014 Paul Querna - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package otp - -import ( - "github.com/boombuler/barcode" - "github.com/boombuler/barcode/qr" - - "crypto/md5" - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" - "errors" - "fmt" - "hash" - "image" - "net/url" - "strings" -) - -// Error when attempting to convert the secret from base32 to raw bytes. -var ErrValidateSecretInvalidBase32 = errors.New("Decoding of secret as base32 failed.") - -// The user provided passcode length was not expected. -var ErrValidateInputInvalidLength = errors.New("Input length unexpected") - -// When generating a Key, the Issuer must be set. -var ErrGenerateMissingIssuer = errors.New("Issuer must be set") - -// When generating a Key, the Account Name must be set. -var ErrGenerateMissingAccountName = errors.New("AccountName must be set") - -// Key represents an TOTP or HTOP key. -type Key struct { - orig string - url *url.URL -} - -// NewKeyFromURL creates a new Key from an TOTP or HOTP url. -// -// The URL format is documented here: -// https://github.com/google/google-authenticator/wiki/Key-Uri-Format -// -func NewKeyFromURL(orig string) (*Key, error) { - u, err := url.Parse(orig) - - if err != nil { - return nil, err - } - - return &Key{ - orig: orig, - url: u, - }, nil -} - -func (k *Key) String() string { - return k.orig -} - -// Image returns an QR-Code image of the specified width and height, -// suitable for use by many clients like Google-Authenricator -// to enroll a user's TOTP/HOTP key. -func (k *Key) Image(width int, height int) (image.Image, error) { - b, err := qr.Encode(k.orig, qr.M, qr.Auto) - - if err != nil { - return nil, err - } - - b, err = barcode.Scale(b, width, height) - - if err != nil { - return nil, err - } - - return b, nil -} - -// Type returns "hotp" or "totp". -func (k *Key) Type() string { - return k.url.Host -} - -// Issuer returns the name of the issuing organization. -func (k *Key) Issuer() string { - q := k.url.Query() - - issuer := q.Get("issuer") - - if issuer != "" { - return issuer - } - - p := strings.TrimPrefix(k.url.Path, "/") - i := strings.Index(p, ":") - - if i == -1 { - return "" - } - - return p[:i] -} - -// AccountName returns the name of the user's account. -func (k *Key) AccountName() string { - p := strings.TrimPrefix(k.url.Path, "/") - i := strings.Index(p, ":") - - if i == -1 { - return p - } - - return p[i+1:] -} - -// Secret returns the opaque secret for this Key. -func (k *Key) Secret() string { - q := k.url.Query() - - return q.Get("secret") -} - -// Algorithm represents the hashing function to use in the HMAC -// operation needed for OTPs. -type Algorithm int - -const ( - AlgorithmSHA1 Algorithm = iota - AlgorithmSHA256 - AlgorithmSHA512 - AlgorithmMD5 -) - -func (a Algorithm) String() string { - switch a { - case AlgorithmSHA1: - return "SHA1" - case AlgorithmSHA256: - return "SHA256" - case AlgorithmSHA512: - return "SHA512" - case AlgorithmMD5: - return "MD5" - } - panic("unreached") -} - -func (a Algorithm) Hash() hash.Hash { - switch a { - case AlgorithmSHA1: - return sha1.New() - case AlgorithmSHA256: - return sha256.New() - case AlgorithmSHA512: - return sha512.New() - case AlgorithmMD5: - return md5.New() - } - panic("unreached") -} - -// Digits represents the number of digits present in the -// user's OTP passcode. Six and Eight are the most common values. -type Digits int - -const ( - DigitsSix Digits = 6 - DigitsEight Digits = 8 -) - -// Format converts an integer into the zero-filled size for this Digits. -func (d Digits) Format(in int32) string { - f := fmt.Sprintf("%%0%dd", d) - return fmt.Sprintf(f, in) -} - -// Length returns the number of characters for this Digits. -func (d Digits) Length() int { - return int(d) -} - -func (d Digits) String() string { - return fmt.Sprintf("%d", d) -} diff --git a/vendor/github.com/pquerna/otp/totp/totp.go b/vendor/github.com/pquerna/otp/totp/totp.go deleted file mode 100644 index af5ab8296717..000000000000 --- a/vendor/github.com/pquerna/otp/totp/totp.go +++ /dev/null @@ -1,191 +0,0 @@ -/** - * Copyright 2014 Paul Querna - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package totp - -import ( - "github.com/pquerna/otp" - "github.com/pquerna/otp/hotp" - - "crypto/rand" - "encoding/base32" - "math" - "net/url" - "strconv" - "time" -) - -// Validate a TOTP using the current time. -// A shortcut for ValidateCustom, Validate uses a configuration -// that is compatible with Google-Authenticator and most clients. -func Validate(passcode string, secret string) bool { - rv, _ := ValidateCustom( - passcode, - secret, - time.Now().UTC(), - ValidateOpts{ - Period: 30, - Skew: 1, - Digits: otp.DigitsSix, - Algorithm: otp.AlgorithmSHA1, - }, - ) - return rv -} - -// GenerateCode creates a TOTP token using the current time. -// A shortcut for GenerateCodeCustom, GenerateCode uses a configuration -// that is compatible with Google-Authenticator and most clients. -func GenerateCode(secret string, t time.Time) (string, error) { - return GenerateCodeCustom(secret, t, ValidateOpts{ - Period: 30, - Skew: 1, - Digits: otp.DigitsSix, - Algorithm: otp.AlgorithmSHA1, - }) -} - -// ValidateOpts provides options for ValidateCustom(). -type ValidateOpts struct { - // Number of seconds a TOTP hash is valid for. Defaults to 30 seconds. - Period uint - // Periods before or after the current time to allow. Value of 1 allows up to Period - // of either side of the specified time. Defaults to 0 allowed skews. Values greater - // than 1 are likely sketchy. - Skew uint - // Digits as part of the input. Defaults to 6. - Digits otp.Digits - // Algorithm to use for HMAC. Defaults to SHA1. - Algorithm otp.Algorithm -} - -// GenerateCodeCustom takes a timepoint and produces a passcode using a -// secret and the provided opts. (Under the hood, this is making an adapted -// call to hotp.GenerateCodeCustom) -func GenerateCodeCustom(secret string, t time.Time, opts ValidateOpts) (passcode string, err error) { - if opts.Period == 0 { - opts.Period = 30 - } - counter := uint64(math.Floor(float64(t.Unix()) / float64(opts.Period))) - passcode, err = hotp.GenerateCodeCustom(secret, counter, hotp.ValidateOpts{ - Digits: opts.Digits, - Algorithm: opts.Algorithm, - }) - if err != nil { - return "", err - } - return passcode, nil -} - -// ValidateCustom validates a TOTP given a user specified time and custom options. -// Most users should use Validate() to provide an interpolatable TOTP experience. -func ValidateCustom(passcode string, secret string, t time.Time, opts ValidateOpts) (bool, error) { - if opts.Period == 0 { - opts.Period = 30 - } - - counters := []uint64{} - counter := int64(math.Floor(float64(t.Unix()) / float64(opts.Period))) - - counters = append(counters, uint64(counter)) - for i := 1; i <= int(opts.Skew); i++ { - counters = append(counters, uint64(counter+int64(i))) - counters = append(counters, uint64(counter-int64(i))) - } - - for _, counter := range counters { - rv, err := hotp.ValidateCustom(passcode, counter, secret, hotp.ValidateOpts{ - Digits: opts.Digits, - Algorithm: opts.Algorithm, - }) - - if err != nil { - return false, err - } - - if rv == true { - return true, nil - } - } - - return false, nil -} - -// GenerateOpts provides options for Generate(). The default values -// are compatible with Google-Authenticator. -type GenerateOpts struct { - // Name of the issuing Organization/Company. - Issuer string - // Name of the User's Account (eg, email address) - AccountName string - // Number of seconds a TOTP hash is valid for. Defaults to 30 seconds. - Period uint - // Size in size of the generated Secret. Defaults to 10 bytes. - SecretSize uint - // Digits to request. Defaults to 6. - Digits otp.Digits - // Algorithm to use for HMAC. Defaults to SHA1. - Algorithm otp.Algorithm -} - -// Generate a new TOTP Key. -func Generate(opts GenerateOpts) (*otp.Key, error) { - // url encode the Issuer/AccountName - if opts.Issuer == "" { - return nil, otp.ErrGenerateMissingIssuer - } - - if opts.AccountName == "" { - return nil, otp.ErrGenerateMissingAccountName - } - - if opts.Period == 0 { - opts.Period = 30 - } - - if opts.SecretSize == 0 { - opts.SecretSize = 10 - } - - if opts.Digits == 0 { - opts.Digits = otp.DigitsSix - } - - // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example - - v := url.Values{} - secret := make([]byte, opts.SecretSize) - _, err := rand.Read(secret) - if err != nil { - return nil, err - } - - v.Set("secret", base32.StdEncoding.EncodeToString(secret)) - v.Set("issuer", opts.Issuer) - v.Set("period", strconv.FormatUint(uint64(opts.Period), 10)) - v.Set("algorithm", opts.Algorithm.String()) - v.Set("digits", opts.Digits.String()) - - u := url.URL{ - Scheme: "otpauth", - Host: "totp", - Path: "/" + opts.Issuer + ":" + opts.AccountName, - RawQuery: v.Encode(), - } - - return otp.NewKeyFromURL(u.String()) -} From 6136a0f4a05202ef58e9395df895edc513c9c887 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sat, 11 Mar 2017 17:37:39 -0800 Subject: [PATCH 17/43] Revert "Removed vendor files for TOTP library" This reverts commit fcd030994bc1741dbf490f3995944e091b11da61. --- vendor/github.com/pquerna/otp/LICENSE | 202 +++++++++++++++++++++ vendor/github.com/pquerna/otp/NOTICE | 5 + vendor/github.com/pquerna/otp/README.md | 60 ++++++ vendor/github.com/pquerna/otp/doc.go | 70 +++++++ vendor/github.com/pquerna/otp/hotp/hotp.go | 187 +++++++++++++++++++ vendor/github.com/pquerna/otp/otp.go | 200 ++++++++++++++++++++ vendor/github.com/pquerna/otp/totp/totp.go | 191 +++++++++++++++++++ 7 files changed, 915 insertions(+) create mode 100644 vendor/github.com/pquerna/otp/LICENSE create mode 100644 vendor/github.com/pquerna/otp/NOTICE create mode 100644 vendor/github.com/pquerna/otp/README.md create mode 100644 vendor/github.com/pquerna/otp/doc.go create mode 100644 vendor/github.com/pquerna/otp/hotp/hotp.go create mode 100644 vendor/github.com/pquerna/otp/otp.go create mode 100644 vendor/github.com/pquerna/otp/totp/totp.go diff --git a/vendor/github.com/pquerna/otp/LICENSE b/vendor/github.com/pquerna/otp/LICENSE new file mode 100644 index 000000000000..d64569567334 --- /dev/null +++ b/vendor/github.com/pquerna/otp/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/pquerna/otp/NOTICE b/vendor/github.com/pquerna/otp/NOTICE new file mode 100644 index 000000000000..50e2e75016eb --- /dev/null +++ b/vendor/github.com/pquerna/otp/NOTICE @@ -0,0 +1,5 @@ +otp +Copyright (c) 2014, Paul Querna + +This product includes software developed by +Paul Querna (http://paul.querna.org/). diff --git a/vendor/github.com/pquerna/otp/README.md b/vendor/github.com/pquerna/otp/README.md new file mode 100644 index 000000000000..148e8980d6d1 --- /dev/null +++ b/vendor/github.com/pquerna/otp/README.md @@ -0,0 +1,60 @@ +# otp: One Time Password utilities Go / Golang + +[![GoDoc](https://godoc.org/github.com/pquerna/otp?status.svg)](https://godoc.org/github.com/pquerna/otp) [![Build Status](https://travis-ci.org/pquerna/otp.svg?branch=master)](https://travis-ci.org/pquerna/otp) + +# Why One Time Passwords? + +One Time Passwords (OTPs) are an mechanism to improve security over passwords alone. When a Time-based OTP (TOTP) is stored on a user's phone, and combined with something the user knows (Password), you have an easy on-ramp to [Multi-factor authentication](http://en.wikipedia.org/wiki/Multi-factor_authentication) without adding a dependency on a SMS provider. This Password and TOTP combination is used by many popular websites including Google, Github, Facebook, Salesforce and many others. + +The `otp` library enables you to easily add TOTPs to your own application, increasing your user's security against mass-password breaches and malware. + +Because TOTP is standardized and widely deployed, there are many [mobile clients and software implementations](http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm#Client_implementations). + +## `otp` Supports: + +* Generating QR Code images for easy user enrollment. +* Time-based One-time Password Algorithm (TOTP) (RFC 6238): Time based OTP, the most commonly used method. +* HMAC-based One-time Password Algorithm (HOTP) (RFC 4226): Counter based OTP, which TOTP is based upon. +* Generation and Validation of codes for either algorithm. + +## Implementing TOTP in your application: + +### User Enrollment + +For an example of a working enrollment work flow, [Github has documented theirs](https://help.github.com/articles/configuring-two-factor-authentication-via-a-totp-mobile-app/ +), but the basics are: + +1. Generate new TOTP Key for a User. `key,_ := totp.Generate(...)`. +1. Display the Key's Secret and QR-Code for the User. `key.Secret()` and `key.Image(...)`. +1. Test that the user can successfully use their TOTP. `totp.Validate(...)`. +1. Store TOTP Secret for the User in your backend. `key.Secret()` +1. Provide the user with "recovery codes". (See Recovery Codes bellow) + +### Code Generation + +* In either TOTP or HOTP cases, use the `GenerateCode` function and a counter or + `time.Time` struct to generate a valid code compatible with most implementations. +* For uncommon or custom settings, or to catch unlikely errors, use `GenerateCodeCustom` + in either module. + +### Validation + +1. Prompt and validate User's password as normal. +1. If the user has TOTP enabled, prompt for TOTP passcode. +1. Retrieve the User's TOTP Secret from your backend. +1. Validate the user's passcode. `totp.Validate(...)` + + +### Recovery Codes + +When a user loses access to their TOTP device, they would no longer have access to their account. Because TOTPs are often configured on mobile devices that can be lost, stolen or damaged, this is a common problem. For this reason many providers give their users "backup codes" or "recovery codes". These are a set of one time use codes that can be used instead of the TOTP. These can simply be randomly generated strings that you store in your backend. [Github's documentation provides an overview of the user experience]( +https://help.github.com/articles/downloading-your-two-factor-authentication-recovery-codes/). + + +## Improvements, bugs, adding feature, etc: + +Please [open issues in Github](https://github.com/pquerna/otp/issues) for ideas, bugs, and general thoughts. Pull requests are of course preferred :) + +## License + +`otp` is licensed under the [Apache License, Version 2.0](./LICENSE) diff --git a/vendor/github.com/pquerna/otp/doc.go b/vendor/github.com/pquerna/otp/doc.go new file mode 100644 index 000000000000..b8b4c8cc193f --- /dev/null +++ b/vendor/github.com/pquerna/otp/doc.go @@ -0,0 +1,70 @@ +/** + * Copyright 2014 Paul Querna + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Package otp implements both HOTP and TOTP based +// one time passcodes in a Google Authenticator compatible manner. +// +// When adding a TOTP for a user, you must store the "secret" value +// persistently. It is recommend to store the secret in an encrypted field in your +// datastore. Due to how TOTP works, it is not possible to store a hash +// for the secret value like you would a password. +// +// To enroll a user, you must first generate an OTP for them. Google +// Authenticator supports using a QR code as an enrollment method: +// +// import ( +// "github.com/pquerna/otp/totp" +// +// "bytes" +// "image/png" +// ) +// +// key, err := totp.Generate(totp.GenerateOpts{ +// Issuer: "Example.com", +// AccountName: "alice@example.com", +// }) +// +// // Convert TOTP key into a QR code encoded as a PNG image. +// var buf bytes.Buffer +// img, err := key.Image(200, 200) +// png.Encode(&buf, img) +// +// // display the QR code to the user. +// display(buf.Bytes()) +// +// // Now Validate that the user's successfully added the passcode. +// passcode := promptForPasscode() +// valid := totp.Validate(passcode, key.Secret()) +// +// if valid { +// // User successfully used their TOTP, save it to your backend! +// storeSecret("alice@example.com", key.Secret()) +// } +// +// Validating a TOTP passcode is very easy, just prompt the user for a passcode +// and retrieve the associated user's previously stored secret. +// import "github.com/pquerna/otp/totp" +// +// passcode := promptForPasscode() +// secret := getSecret("alice@example.com") +// +// valid := totp.Validate(passcode, secret) +// +// if valid { +// // Success! continue login process. +// } +package otp diff --git a/vendor/github.com/pquerna/otp/hotp/hotp.go b/vendor/github.com/pquerna/otp/hotp/hotp.go new file mode 100644 index 000000000000..ced7d8e28d2d --- /dev/null +++ b/vendor/github.com/pquerna/otp/hotp/hotp.go @@ -0,0 +1,187 @@ +/** + * Copyright 2014 Paul Querna + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package hotp + +import ( + "github.com/pquerna/otp" + + "crypto/hmac" + "crypto/rand" + "crypto/subtle" + "encoding/base32" + "encoding/binary" + "fmt" + "math" + "net/url" + "strings" +) + +const debug = false + +// Validate a HOTP passcode given a counter and secret. +// This is a shortcut for ValidateCustom, with parameters that +// are compataible with Google-Authenticator. +func Validate(passcode string, counter uint64, secret string) bool { + rv, _ := ValidateCustom( + passcode, + counter, + secret, + ValidateOpts{ + Digits: otp.DigitsSix, + Algorithm: otp.AlgorithmSHA1, + }, + ) + return rv +} + +// ValidateOpts provides options for ValidateCustom(). +type ValidateOpts struct { + // Digits as part of the input. Defaults to 6. + Digits otp.Digits + // Algorithm to use for HMAC. Defaults to SHA1. + Algorithm otp.Algorithm +} + +// GenerateCode creates a HOTP passcode given a counter and secret. +// This is a shortcut for GenerateCodeCustom, with parameters that +// are compataible with Google-Authenticator. +func GenerateCode(secret string, counter uint64) (string, error) { + return GenerateCodeCustom(secret, counter, ValidateOpts{ + Digits: otp.DigitsSix, + Algorithm: otp.AlgorithmSHA1, + }) +} + +// GenerateCodeCustom uses a counter and secret value and options struct to +// create a passcode. +func GenerateCodeCustom(secret string, counter uint64, opts ValidateOpts) (passcode string, err error) { + // As noted in issue #10 this adds support for TOTP secrets that are + // missing their padding. + if n := len(secret) % 8; n != 0 { + secret = secret + strings.Repeat("=", 8-n) + } + + secretBytes, err := base32.StdEncoding.DecodeString(secret) + if err != nil { + return "", otp.ErrValidateSecretInvalidBase32 + } + + buf := make([]byte, 8) + mac := hmac.New(opts.Algorithm.Hash, secretBytes) + binary.BigEndian.PutUint64(buf, counter) + if debug { + fmt.Printf("counter=%v\n", counter) + fmt.Printf("buf=%v\n", buf) + } + + mac.Write(buf) + sum := mac.Sum(nil) + + // "Dynamic truncation" in RFC 4226 + // http://tools.ietf.org/html/rfc4226#section-5.4 + offset := sum[len(sum)-1] & 0xf + value := int64(((int(sum[offset]) & 0x7f) << 24) | + ((int(sum[offset+1] & 0xff)) << 16) | + ((int(sum[offset+2] & 0xff)) << 8) | + (int(sum[offset+3]) & 0xff)) + + l := opts.Digits.Length() + mod := int32(value % int64(math.Pow10(l))) + + if debug { + fmt.Printf("offset=%v\n", offset) + fmt.Printf("value=%v\n", value) + fmt.Printf("mod'ed=%v\n", mod) + } + + return opts.Digits.Format(mod), nil +} + +// ValidateCustom validates an HOTP with customizable options. Most users should +// use Validate(). +func ValidateCustom(passcode string, counter uint64, secret string, opts ValidateOpts) (bool, error) { + passcode = strings.TrimSpace(passcode) + + if len(passcode) != opts.Digits.Length() { + return false, otp.ErrValidateInputInvalidLength + } + + otpstr, err := GenerateCodeCustom(secret, counter, opts) + if err != nil { + return false, err + } + + if subtle.ConstantTimeCompare([]byte(otpstr), []byte(passcode)) == 1 { + return true, nil + } + + return false, nil +} + +// GenerateOpts provides options for .Generate() +type GenerateOpts struct { + // Name of the issuing Organization/Company. + Issuer string + // Name of the User's Account (eg, email address) + AccountName string + // Size in size of the generated Secret. Defaults to 10 bytes. + SecretSize uint + // Digits to request. Defaults to 6. + Digits otp.Digits + // Algorithm to use for HMAC. Defaults to SHA1. + Algorithm otp.Algorithm +} + +// Generate creates a new HOTP Key. +func Generate(opts GenerateOpts) (*otp.Key, error) { + // url encode the Issuer/AccountName + if opts.Issuer == "" { + return nil, otp.ErrGenerateMissingIssuer + } + + if opts.AccountName == "" { + return nil, otp.ErrGenerateMissingAccountName + } + + if opts.SecretSize == 0 { + opts.SecretSize = 10 + } + + // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example + + v := url.Values{} + secret := make([]byte, opts.SecretSize) + _, err := rand.Read(secret) + if err != nil { + return nil, err + } + + v.Set("secret", base32.StdEncoding.EncodeToString(secret)) + v.Set("issuer", opts.Issuer) + v.Set("algorithm", opts.Algorithm.String()) + v.Set("digits", opts.Digits.String()) + + u := url.URL{ + Scheme: "otpauth", + Host: "hotp", + Path: "/" + opts.Issuer + ":" + opts.AccountName, + RawQuery: v.Encode(), + } + + return otp.NewKeyFromURL(u.String()) +} diff --git a/vendor/github.com/pquerna/otp/otp.go b/vendor/github.com/pquerna/otp/otp.go new file mode 100644 index 000000000000..0fa970927347 --- /dev/null +++ b/vendor/github.com/pquerna/otp/otp.go @@ -0,0 +1,200 @@ +/** + * Copyright 2014 Paul Querna + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package otp + +import ( + "github.com/boombuler/barcode" + "github.com/boombuler/barcode/qr" + + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "errors" + "fmt" + "hash" + "image" + "net/url" + "strings" +) + +// Error when attempting to convert the secret from base32 to raw bytes. +var ErrValidateSecretInvalidBase32 = errors.New("Decoding of secret as base32 failed.") + +// The user provided passcode length was not expected. +var ErrValidateInputInvalidLength = errors.New("Input length unexpected") + +// When generating a Key, the Issuer must be set. +var ErrGenerateMissingIssuer = errors.New("Issuer must be set") + +// When generating a Key, the Account Name must be set. +var ErrGenerateMissingAccountName = errors.New("AccountName must be set") + +// Key represents an TOTP or HTOP key. +type Key struct { + orig string + url *url.URL +} + +// NewKeyFromURL creates a new Key from an TOTP or HOTP url. +// +// The URL format is documented here: +// https://github.com/google/google-authenticator/wiki/Key-Uri-Format +// +func NewKeyFromURL(orig string) (*Key, error) { + u, err := url.Parse(orig) + + if err != nil { + return nil, err + } + + return &Key{ + orig: orig, + url: u, + }, nil +} + +func (k *Key) String() string { + return k.orig +} + +// Image returns an QR-Code image of the specified width and height, +// suitable for use by many clients like Google-Authenricator +// to enroll a user's TOTP/HOTP key. +func (k *Key) Image(width int, height int) (image.Image, error) { + b, err := qr.Encode(k.orig, qr.M, qr.Auto) + + if err != nil { + return nil, err + } + + b, err = barcode.Scale(b, width, height) + + if err != nil { + return nil, err + } + + return b, nil +} + +// Type returns "hotp" or "totp". +func (k *Key) Type() string { + return k.url.Host +} + +// Issuer returns the name of the issuing organization. +func (k *Key) Issuer() string { + q := k.url.Query() + + issuer := q.Get("issuer") + + if issuer != "" { + return issuer + } + + p := strings.TrimPrefix(k.url.Path, "/") + i := strings.Index(p, ":") + + if i == -1 { + return "" + } + + return p[:i] +} + +// AccountName returns the name of the user's account. +func (k *Key) AccountName() string { + p := strings.TrimPrefix(k.url.Path, "/") + i := strings.Index(p, ":") + + if i == -1 { + return p + } + + return p[i+1:] +} + +// Secret returns the opaque secret for this Key. +func (k *Key) Secret() string { + q := k.url.Query() + + return q.Get("secret") +} + +// Algorithm represents the hashing function to use in the HMAC +// operation needed for OTPs. +type Algorithm int + +const ( + AlgorithmSHA1 Algorithm = iota + AlgorithmSHA256 + AlgorithmSHA512 + AlgorithmMD5 +) + +func (a Algorithm) String() string { + switch a { + case AlgorithmSHA1: + return "SHA1" + case AlgorithmSHA256: + return "SHA256" + case AlgorithmSHA512: + return "SHA512" + case AlgorithmMD5: + return "MD5" + } + panic("unreached") +} + +func (a Algorithm) Hash() hash.Hash { + switch a { + case AlgorithmSHA1: + return sha1.New() + case AlgorithmSHA256: + return sha256.New() + case AlgorithmSHA512: + return sha512.New() + case AlgorithmMD5: + return md5.New() + } + panic("unreached") +} + +// Digits represents the number of digits present in the +// user's OTP passcode. Six and Eight are the most common values. +type Digits int + +const ( + DigitsSix Digits = 6 + DigitsEight Digits = 8 +) + +// Format converts an integer into the zero-filled size for this Digits. +func (d Digits) Format(in int32) string { + f := fmt.Sprintf("%%0%dd", d) + return fmt.Sprintf(f, in) +} + +// Length returns the number of characters for this Digits. +func (d Digits) Length() int { + return int(d) +} + +func (d Digits) String() string { + return fmt.Sprintf("%d", d) +} diff --git a/vendor/github.com/pquerna/otp/totp/totp.go b/vendor/github.com/pquerna/otp/totp/totp.go new file mode 100644 index 000000000000..af5ab8296717 --- /dev/null +++ b/vendor/github.com/pquerna/otp/totp/totp.go @@ -0,0 +1,191 @@ +/** + * Copyright 2014 Paul Querna + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package totp + +import ( + "github.com/pquerna/otp" + "github.com/pquerna/otp/hotp" + + "crypto/rand" + "encoding/base32" + "math" + "net/url" + "strconv" + "time" +) + +// Validate a TOTP using the current time. +// A shortcut for ValidateCustom, Validate uses a configuration +// that is compatible with Google-Authenticator and most clients. +func Validate(passcode string, secret string) bool { + rv, _ := ValidateCustom( + passcode, + secret, + time.Now().UTC(), + ValidateOpts{ + Period: 30, + Skew: 1, + Digits: otp.DigitsSix, + Algorithm: otp.AlgorithmSHA1, + }, + ) + return rv +} + +// GenerateCode creates a TOTP token using the current time. +// A shortcut for GenerateCodeCustom, GenerateCode uses a configuration +// that is compatible with Google-Authenticator and most clients. +func GenerateCode(secret string, t time.Time) (string, error) { + return GenerateCodeCustom(secret, t, ValidateOpts{ + Period: 30, + Skew: 1, + Digits: otp.DigitsSix, + Algorithm: otp.AlgorithmSHA1, + }) +} + +// ValidateOpts provides options for ValidateCustom(). +type ValidateOpts struct { + // Number of seconds a TOTP hash is valid for. Defaults to 30 seconds. + Period uint + // Periods before or after the current time to allow. Value of 1 allows up to Period + // of either side of the specified time. Defaults to 0 allowed skews. Values greater + // than 1 are likely sketchy. + Skew uint + // Digits as part of the input. Defaults to 6. + Digits otp.Digits + // Algorithm to use for HMAC. Defaults to SHA1. + Algorithm otp.Algorithm +} + +// GenerateCodeCustom takes a timepoint and produces a passcode using a +// secret and the provided opts. (Under the hood, this is making an adapted +// call to hotp.GenerateCodeCustom) +func GenerateCodeCustom(secret string, t time.Time, opts ValidateOpts) (passcode string, err error) { + if opts.Period == 0 { + opts.Period = 30 + } + counter := uint64(math.Floor(float64(t.Unix()) / float64(opts.Period))) + passcode, err = hotp.GenerateCodeCustom(secret, counter, hotp.ValidateOpts{ + Digits: opts.Digits, + Algorithm: opts.Algorithm, + }) + if err != nil { + return "", err + } + return passcode, nil +} + +// ValidateCustom validates a TOTP given a user specified time and custom options. +// Most users should use Validate() to provide an interpolatable TOTP experience. +func ValidateCustom(passcode string, secret string, t time.Time, opts ValidateOpts) (bool, error) { + if opts.Period == 0 { + opts.Period = 30 + } + + counters := []uint64{} + counter := int64(math.Floor(float64(t.Unix()) / float64(opts.Period))) + + counters = append(counters, uint64(counter)) + for i := 1; i <= int(opts.Skew); i++ { + counters = append(counters, uint64(counter+int64(i))) + counters = append(counters, uint64(counter-int64(i))) + } + + for _, counter := range counters { + rv, err := hotp.ValidateCustom(passcode, counter, secret, hotp.ValidateOpts{ + Digits: opts.Digits, + Algorithm: opts.Algorithm, + }) + + if err != nil { + return false, err + } + + if rv == true { + return true, nil + } + } + + return false, nil +} + +// GenerateOpts provides options for Generate(). The default values +// are compatible with Google-Authenticator. +type GenerateOpts struct { + // Name of the issuing Organization/Company. + Issuer string + // Name of the User's Account (eg, email address) + AccountName string + // Number of seconds a TOTP hash is valid for. Defaults to 30 seconds. + Period uint + // Size in size of the generated Secret. Defaults to 10 bytes. + SecretSize uint + // Digits to request. Defaults to 6. + Digits otp.Digits + // Algorithm to use for HMAC. Defaults to SHA1. + Algorithm otp.Algorithm +} + +// Generate a new TOTP Key. +func Generate(opts GenerateOpts) (*otp.Key, error) { + // url encode the Issuer/AccountName + if opts.Issuer == "" { + return nil, otp.ErrGenerateMissingIssuer + } + + if opts.AccountName == "" { + return nil, otp.ErrGenerateMissingAccountName + } + + if opts.Period == 0 { + opts.Period = 30 + } + + if opts.SecretSize == 0 { + opts.SecretSize = 10 + } + + if opts.Digits == 0 { + opts.Digits = otp.DigitsSix + } + + // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example + + v := url.Values{} + secret := make([]byte, opts.SecretSize) + _, err := rand.Read(secret) + if err != nil { + return nil, err + } + + v.Set("secret", base32.StdEncoding.EncodeToString(secret)) + v.Set("issuer", opts.Issuer) + v.Set("period", strconv.FormatUint(uint64(opts.Period), 10)) + v.Set("algorithm", opts.Algorithm.String()) + v.Set("digits", opts.Digits.String()) + + u := url.URL{ + Scheme: "otpauth", + Host: "totp", + Path: "/" + opts.Issuer + ":" + opts.AccountName, + RawQuery: v.Encode(), + } + + return otp.NewKeyFromURL(u.String()) +} From 2a122fc2cc5ca946271a995e29f1f71daa23929f Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sat, 11 Mar 2017 17:54:02 -0800 Subject: [PATCH 18/43] Hopefully fixed vendor folder issue with TOTP Library --- vendor/github.com/boombuler/barcode/LICENSE | 21 + vendor/github.com/boombuler/barcode/README.md | 18 + .../github.com/boombuler/barcode/barcode.go | 27 ++ .../boombuler/barcode/qr/alphanumeric.go | 66 +++ .../boombuler/barcode/qr/automatic.go | 23 + .../github.com/boombuler/barcode/qr/blocks.go | 59 +++ .../boombuler/barcode/qr/encoder.go | 416 ++++++++++++++++++ .../boombuler/barcode/qr/errorcorrection.go | 29 ++ .../boombuler/barcode/qr/numeric.go | 56 +++ .../github.com/boombuler/barcode/qr/qrcode.go | 166 +++++++ .../boombuler/barcode/qr/unicode.go | 27 ++ .../boombuler/barcode/qr/versioninfo.go | 310 +++++++++++++ .../boombuler/barcode/scaledbarcode.go | 134 ++++++ .../boombuler/barcode/utils/base1dcode.go | 57 +++ .../boombuler/barcode/utils/bitlist.go | 119 +++++ .../boombuler/barcode/utils/galoisfield.go | 65 +++ .../boombuler/barcode/utils/gfpoly.go | 103 +++++ .../boombuler/barcode/utils/reedsolomon.go | 44 ++ .../boombuler/barcode/utils/runeint.go | 19 + vendor/github.com/pquerna/otp/example/main.go | 63 +++ vendor/vendor.json | 25 ++ 21 files changed, 1847 insertions(+) create mode 100644 vendor/github.com/boombuler/barcode/LICENSE create mode 100644 vendor/github.com/boombuler/barcode/README.md create mode 100644 vendor/github.com/boombuler/barcode/barcode.go create mode 100644 vendor/github.com/boombuler/barcode/qr/alphanumeric.go create mode 100644 vendor/github.com/boombuler/barcode/qr/automatic.go create mode 100644 vendor/github.com/boombuler/barcode/qr/blocks.go create mode 100644 vendor/github.com/boombuler/barcode/qr/encoder.go create mode 100644 vendor/github.com/boombuler/barcode/qr/errorcorrection.go create mode 100644 vendor/github.com/boombuler/barcode/qr/numeric.go create mode 100644 vendor/github.com/boombuler/barcode/qr/qrcode.go create mode 100644 vendor/github.com/boombuler/barcode/qr/unicode.go create mode 100644 vendor/github.com/boombuler/barcode/qr/versioninfo.go create mode 100644 vendor/github.com/boombuler/barcode/scaledbarcode.go create mode 100644 vendor/github.com/boombuler/barcode/utils/base1dcode.go create mode 100644 vendor/github.com/boombuler/barcode/utils/bitlist.go create mode 100644 vendor/github.com/boombuler/barcode/utils/galoisfield.go create mode 100644 vendor/github.com/boombuler/barcode/utils/gfpoly.go create mode 100644 vendor/github.com/boombuler/barcode/utils/reedsolomon.go create mode 100644 vendor/github.com/boombuler/barcode/utils/runeint.go create mode 100644 vendor/github.com/pquerna/otp/example/main.go diff --git a/vendor/github.com/boombuler/barcode/LICENSE b/vendor/github.com/boombuler/barcode/LICENSE new file mode 100644 index 000000000000..862b0ddcd7fe --- /dev/null +++ b/vendor/github.com/boombuler/barcode/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Florian Sundermann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/boombuler/barcode/README.md b/vendor/github.com/boombuler/barcode/README.md new file mode 100644 index 000000000000..85c34d6390d8 --- /dev/null +++ b/vendor/github.com/boombuler/barcode/README.md @@ -0,0 +1,18 @@ +##Introduction## +This is a package for GO which can be used to create different types of barcodes. + +##Supported Barcode Types## +* Aztec Code +* Codabar +* Code 128 +* Code 39 +* EAN 8 +* EAN 13 +* Datamatrix +* QR Codes +* 2 of 5 + +##Documentation## +See [GoDoc](https://godoc.org/github.com/boombuler/barcode) + +To create a barcode use the Encode function from one of the subpackages. diff --git a/vendor/github.com/boombuler/barcode/barcode.go b/vendor/github.com/boombuler/barcode/barcode.go new file mode 100644 index 000000000000..3479c7bc2057 --- /dev/null +++ b/vendor/github.com/boombuler/barcode/barcode.go @@ -0,0 +1,27 @@ +package barcode + +import "image" + +// Contains some meta information about a barcode +type Metadata struct { + // the name of the barcode kind + CodeKind string + // contains 1 for 1D barcodes or 2 for 2D barcodes + Dimensions byte +} + +// a rendered and encoded barcode +type Barcode interface { + image.Image + // returns some meta information about the barcode + Metadata() Metadata + // the data that was encoded in this barcode + Content() string +} + +// Additional interface that some barcodes might implement to provide +// the value of its checksum. +type BarcodeIntCS interface { + Barcode + CheckSum() int +} diff --git a/vendor/github.com/boombuler/barcode/qr/alphanumeric.go b/vendor/github.com/boombuler/barcode/qr/alphanumeric.go new file mode 100644 index 000000000000..4ded7c8e030b --- /dev/null +++ b/vendor/github.com/boombuler/barcode/qr/alphanumeric.go @@ -0,0 +1,66 @@ +package qr + +import ( + "errors" + "fmt" + "strings" + + "github.com/boombuler/barcode/utils" +) + +const charSet string = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:" + +func stringToAlphaIdx(content string) <-chan int { + result := make(chan int) + go func() { + for _, r := range content { + idx := strings.IndexRune(charSet, r) + result <- idx + if idx < 0 { + break + } + } + close(result) + }() + + return result +} + +func encodeAlphaNumeric(content string, ecl ErrorCorrectionLevel) (*utils.BitList, *versionInfo, error) { + + contentLenIsOdd := len(content)%2 == 1 + contentBitCount := (len(content) / 2) * 11 + if contentLenIsOdd { + contentBitCount += 6 + } + vi := findSmallestVersionInfo(ecl, alphaNumericMode, contentBitCount) + if vi == nil { + return nil, nil, errors.New("To much data to encode") + } + + res := new(utils.BitList) + res.AddBits(int(alphaNumericMode), 4) + res.AddBits(len(content), vi.charCountBits(alphaNumericMode)) + + encoder := stringToAlphaIdx(content) + + for idx := 0; idx < len(content)/2; idx++ { + c1 := <-encoder + c2 := <-encoder + if c1 < 0 || c2 < 0 { + return nil, nil, fmt.Errorf("\"%s\" can not be encoded as %s", content, AlphaNumeric) + } + res.AddBits(c1*45+c2, 11) + } + if contentLenIsOdd { + c := <-encoder + if c < 0 { + return nil, nil, fmt.Errorf("\"%s\" can not be encoded as %s", content, AlphaNumeric) + } + res.AddBits(c, 6) + } + + addPaddingAndTerminator(res, vi) + + return res, vi, nil +} diff --git a/vendor/github.com/boombuler/barcode/qr/automatic.go b/vendor/github.com/boombuler/barcode/qr/automatic.go new file mode 100644 index 000000000000..e7c56013f16e --- /dev/null +++ b/vendor/github.com/boombuler/barcode/qr/automatic.go @@ -0,0 +1,23 @@ +package qr + +import ( + "fmt" + + "github.com/boombuler/barcode/utils" +) + +func encodeAuto(content string, ecl ErrorCorrectionLevel) (*utils.BitList, *versionInfo, error) { + bits, vi, _ := Numeric.getEncoder()(content, ecl) + if bits != nil && vi != nil { + return bits, vi, nil + } + bits, vi, _ = AlphaNumeric.getEncoder()(content, ecl) + if bits != nil && vi != nil { + return bits, vi, nil + } + bits, vi, _ = Unicode.getEncoder()(content, ecl) + if bits != nil && vi != nil { + return bits, vi, nil + } + return nil, nil, fmt.Errorf("No encoding found to encode \"%s\"", content) +} diff --git a/vendor/github.com/boombuler/barcode/qr/blocks.go b/vendor/github.com/boombuler/barcode/qr/blocks.go new file mode 100644 index 000000000000..d3173787f601 --- /dev/null +++ b/vendor/github.com/boombuler/barcode/qr/blocks.go @@ -0,0 +1,59 @@ +package qr + +type block struct { + data []byte + ecc []byte +} +type blockList []*block + +func splitToBlocks(data <-chan byte, vi *versionInfo) blockList { + result := make(blockList, vi.NumberOfBlocksInGroup1+vi.NumberOfBlocksInGroup2) + + for b := 0; b < int(vi.NumberOfBlocksInGroup1); b++ { + blk := new(block) + blk.data = make([]byte, vi.DataCodeWordsPerBlockInGroup1) + for cw := 0; cw < int(vi.DataCodeWordsPerBlockInGroup1); cw++ { + blk.data[cw] = <-data + } + blk.ecc = ec.calcECC(blk.data, vi.ErrorCorrectionCodewordsPerBlock) + result[b] = blk + } + + for b := 0; b < int(vi.NumberOfBlocksInGroup2); b++ { + blk := new(block) + blk.data = make([]byte, vi.DataCodeWordsPerBlockInGroup2) + for cw := 0; cw < int(vi.DataCodeWordsPerBlockInGroup2); cw++ { + blk.data[cw] = <-data + } + blk.ecc = ec.calcECC(blk.data, vi.ErrorCorrectionCodewordsPerBlock) + result[int(vi.NumberOfBlocksInGroup1)+b] = blk + } + + return result +} + +func (bl blockList) interleave(vi *versionInfo) []byte { + var maxCodewordCount int + if vi.DataCodeWordsPerBlockInGroup1 > vi.DataCodeWordsPerBlockInGroup2 { + maxCodewordCount = int(vi.DataCodeWordsPerBlockInGroup1) + } else { + maxCodewordCount = int(vi.DataCodeWordsPerBlockInGroup2) + } + resultLen := (vi.DataCodeWordsPerBlockInGroup1+vi.ErrorCorrectionCodewordsPerBlock)*vi.NumberOfBlocksInGroup1 + + (vi.DataCodeWordsPerBlockInGroup2+vi.ErrorCorrectionCodewordsPerBlock)*vi.NumberOfBlocksInGroup2 + + result := make([]byte, 0, resultLen) + for i := 0; i < maxCodewordCount; i++ { + for b := 0; b < len(bl); b++ { + if len(bl[b].data) > i { + result = append(result, bl[b].data[i]) + } + } + } + for i := 0; i < int(vi.ErrorCorrectionCodewordsPerBlock); i++ { + for b := 0; b < len(bl); b++ { + result = append(result, bl[b].ecc[i]) + } + } + return result +} diff --git a/vendor/github.com/boombuler/barcode/qr/encoder.go b/vendor/github.com/boombuler/barcode/qr/encoder.go new file mode 100644 index 000000000000..2c6ab2111ada --- /dev/null +++ b/vendor/github.com/boombuler/barcode/qr/encoder.go @@ -0,0 +1,416 @@ +// Package qr can be used to create QR barcodes. +package qr + +import ( + "image" + + "github.com/boombuler/barcode" + "github.com/boombuler/barcode/utils" +) + +type encodeFn func(content string, eccLevel ErrorCorrectionLevel) (*utils.BitList, *versionInfo, error) + +// Encoding mode for QR Codes. +type Encoding byte + +const ( + // Auto will choose ths best matching encoding + Auto Encoding = iota + // Numeric encoding only encodes numbers [0-9] + Numeric + // AlphaNumeric encoding only encodes uppercase letters, numbers and [Space], $, %, *, +, -, ., /, : + AlphaNumeric + // Unicode encoding encodes the string as utf-8 + Unicode + // only for testing purpose + unknownEncoding +) + +func (e Encoding) getEncoder() encodeFn { + switch e { + case Auto: + return encodeAuto + case Numeric: + return encodeNumeric + case AlphaNumeric: + return encodeAlphaNumeric + case Unicode: + return encodeUnicode + } + return nil +} + +func (e Encoding) String() string { + switch e { + case Auto: + return "Auto" + case Numeric: + return "Numeric" + case AlphaNumeric: + return "AlphaNumeric" + case Unicode: + return "Unicode" + } + return "" +} + +// Encode returns a QR barcode with the given content, error correction level and uses the given encoding +func Encode(content string, level ErrorCorrectionLevel, mode Encoding) (barcode.Barcode, error) { + bits, vi, err := mode.getEncoder()(content, level) + if err != nil { + return nil, err + } + + blocks := splitToBlocks(bits.IterateBytes(), vi) + data := blocks.interleave(vi) + result := render(data, vi) + result.content = content + return result, nil +} + +func render(data []byte, vi *versionInfo) *qrcode { + dim := vi.modulWidth() + results := make([]*qrcode, 8) + for i := 0; i < 8; i++ { + results[i] = newBarcode(dim) + } + + occupied := newBarcode(dim) + + setAll := func(x int, y int, val bool) { + occupied.Set(x, y, true) + for i := 0; i < 8; i++ { + results[i].Set(x, y, val) + } + } + + drawFinderPatterns(vi, setAll) + drawAlignmentPatterns(occupied, vi, setAll) + + //Timing Pattern: + var i int + for i = 0; i < dim; i++ { + if !occupied.Get(i, 6) { + setAll(i, 6, i%2 == 0) + } + if !occupied.Get(6, i) { + setAll(6, i, i%2 == 0) + } + } + // Dark Module + setAll(8, dim-8, true) + + drawVersionInfo(vi, setAll) + drawFormatInfo(vi, -1, occupied.Set) + for i := 0; i < 8; i++ { + drawFormatInfo(vi, i, results[i].Set) + } + + // Write the data + var curBitNo int + + for pos := range iterateModules(occupied) { + var curBit bool + if curBitNo < len(data)*8 { + curBit = ((data[curBitNo/8] >> uint(7-(curBitNo%8))) & 1) == 1 + } else { + curBit = false + } + + for i := 0; i < 8; i++ { + setMasked(pos.X, pos.Y, curBit, i, results[i].Set) + } + curBitNo++ + } + + lowestPenalty := ^uint(0) + lowestPenaltyIdx := -1 + for i := 0; i < 8; i++ { + p := results[i].calcPenalty() + if p < lowestPenalty { + lowestPenalty = p + lowestPenaltyIdx = i + } + } + return results[lowestPenaltyIdx] +} + +func setMasked(x, y int, val bool, mask int, set func(int, int, bool)) { + switch mask { + case 0: + val = val != (((y + x) % 2) == 0) + break + case 1: + val = val != ((y % 2) == 0) + break + case 2: + val = val != ((x % 3) == 0) + break + case 3: + val = val != (((y + x) % 3) == 0) + break + case 4: + val = val != (((y/2 + x/3) % 2) == 0) + break + case 5: + val = val != (((y*x)%2)+((y*x)%3) == 0) + break + case 6: + val = val != ((((y*x)%2)+((y*x)%3))%2 == 0) + break + case 7: + val = val != ((((y+x)%2)+((y*x)%3))%2 == 0) + } + set(x, y, val) +} + +func iterateModules(occupied *qrcode) <-chan image.Point { + result := make(chan image.Point) + allPoints := make(chan image.Point) + go func() { + curX := occupied.dimension - 1 + curY := occupied.dimension - 1 + isUpward := true + + for true { + if isUpward { + allPoints <- image.Pt(curX, curY) + allPoints <- image.Pt(curX-1, curY) + curY-- + if curY < 0 { + curY = 0 + curX -= 2 + if curX == 6 { + curX-- + } + if curX < 0 { + break + } + isUpward = false + } + } else { + allPoints <- image.Pt(curX, curY) + allPoints <- image.Pt(curX-1, curY) + curY++ + if curY >= occupied.dimension { + curY = occupied.dimension - 1 + curX -= 2 + if curX == 6 { + curX-- + } + isUpward = true + if curX < 0 { + break + } + } + } + } + + close(allPoints) + }() + go func() { + for pt := range allPoints { + if !occupied.Get(pt.X, pt.Y) { + result <- pt + } + } + close(result) + }() + return result +} + +func drawFinderPatterns(vi *versionInfo, set func(int, int, bool)) { + dim := vi.modulWidth() + drawPattern := func(xoff int, yoff int) { + for x := -1; x < 8; x++ { + for y := -1; y < 8; y++ { + val := (x == 0 || x == 6 || y == 0 || y == 6 || (x > 1 && x < 5 && y > 1 && y < 5)) && (x <= 6 && y <= 6 && x >= 0 && y >= 0) + + if x+xoff >= 0 && x+xoff < dim && y+yoff >= 0 && y+yoff < dim { + set(x+xoff, y+yoff, val) + } + } + } + } + drawPattern(0, 0) + drawPattern(0, dim-7) + drawPattern(dim-7, 0) +} + +func drawAlignmentPatterns(occupied *qrcode, vi *versionInfo, set func(int, int, bool)) { + drawPattern := func(xoff int, yoff int) { + for x := -2; x <= 2; x++ { + for y := -2; y <= 2; y++ { + val := x == -2 || x == 2 || y == -2 || y == 2 || (x == 0 && y == 0) + set(x+xoff, y+yoff, val) + } + } + } + positions := vi.alignmentPatternPlacements() + + for _, x := range positions { + for _, y := range positions { + if occupied.Get(x, y) { + continue + } + drawPattern(x, y) + } + } +} + +var formatInfos = map[ErrorCorrectionLevel]map[int][]bool{ + L: { + 0: []bool{true, true, true, false, true, true, true, true, true, false, false, false, true, false, false}, + 1: []bool{true, true, true, false, false, true, false, true, true, true, true, false, false, true, true}, + 2: []bool{true, true, true, true, true, false, true, true, false, true, false, true, false, true, false}, + 3: []bool{true, true, true, true, false, false, false, true, false, false, true, true, true, false, true}, + 4: []bool{true, true, false, false, true, true, false, false, false, true, false, true, true, true, true}, + 5: []bool{true, true, false, false, false, true, true, false, false, false, true, true, false, false, false}, + 6: []bool{true, true, false, true, true, false, false, false, true, false, false, false, false, false, true}, + 7: []bool{true, true, false, true, false, false, true, false, true, true, true, false, true, true, false}, + }, + M: { + 0: []bool{true, false, true, false, true, false, false, false, false, false, true, false, false, true, false}, + 1: []bool{true, false, true, false, false, false, true, false, false, true, false, false, true, false, true}, + 2: []bool{true, false, true, true, true, true, false, false, true, true, true, true, true, false, false}, + 3: []bool{true, false, true, true, false, true, true, false, true, false, false, true, false, true, true}, + 4: []bool{true, false, false, false, true, false, true, true, true, true, true, true, false, false, true}, + 5: []bool{true, false, false, false, false, false, false, true, true, false, false, true, true, true, false}, + 6: []bool{true, false, false, true, true, true, true, true, false, false, true, false, true, true, true}, + 7: []bool{true, false, false, true, false, true, false, true, false, true, false, false, false, false, false}, + }, + Q: { + 0: []bool{false, true, true, false, true, false, true, false, true, false, true, true, true, true, true}, + 1: []bool{false, true, true, false, false, false, false, false, true, true, false, true, false, false, false}, + 2: []bool{false, true, true, true, true, true, true, false, false, true, true, false, false, false, true}, + 3: []bool{false, true, true, true, false, true, false, false, false, false, false, false, true, true, false}, + 4: []bool{false, true, false, false, true, false, false, true, false, true, true, false, true, false, false}, + 5: []bool{false, true, false, false, false, false, true, true, false, false, false, false, false, true, true}, + 6: []bool{false, true, false, true, true, true, false, true, true, false, true, true, false, true, false}, + 7: []bool{false, true, false, true, false, true, true, true, true, true, false, true, true, false, true}, + }, + H: { + 0: []bool{false, false, true, false, true, true, false, true, false, false, false, true, false, false, true}, + 1: []bool{false, false, true, false, false, true, true, true, false, true, true, true, true, true, false}, + 2: []bool{false, false, true, true, true, false, false, true, true, true, false, false, true, true, true}, + 3: []bool{false, false, true, true, false, false, true, true, true, false, true, false, false, false, false}, + 4: []bool{false, false, false, false, true, true, true, false, true, true, false, false, false, true, false}, + 5: []bool{false, false, false, false, false, true, false, false, true, false, true, false, true, false, true}, + 6: []bool{false, false, false, true, true, false, true, false, false, false, false, true, true, false, false}, + 7: []bool{false, false, false, true, false, false, false, false, false, true, true, true, false, true, true}, + }, +} + +func drawFormatInfo(vi *versionInfo, usedMask int, set func(int, int, bool)) { + var formatInfo []bool + + if usedMask == -1 { + formatInfo = []bool{true, true, true, true, true, true, true, true, true, true, true, true, true, true, true} // Set all to true cause -1 --> occupied mask. + } else { + formatInfo = formatInfos[vi.Level][usedMask] + } + + if len(formatInfo) == 15 { + dim := vi.modulWidth() + set(0, 8, formatInfo[0]) + set(1, 8, formatInfo[1]) + set(2, 8, formatInfo[2]) + set(3, 8, formatInfo[3]) + set(4, 8, formatInfo[4]) + set(5, 8, formatInfo[5]) + set(7, 8, formatInfo[6]) + set(8, 8, formatInfo[7]) + set(8, 7, formatInfo[8]) + set(8, 5, formatInfo[9]) + set(8, 4, formatInfo[10]) + set(8, 3, formatInfo[11]) + set(8, 2, formatInfo[12]) + set(8, 1, formatInfo[13]) + set(8, 0, formatInfo[14]) + + set(8, dim-1, formatInfo[0]) + set(8, dim-2, formatInfo[1]) + set(8, dim-3, formatInfo[2]) + set(8, dim-4, formatInfo[3]) + set(8, dim-5, formatInfo[4]) + set(8, dim-6, formatInfo[5]) + set(8, dim-7, formatInfo[6]) + set(dim-8, 8, formatInfo[7]) + set(dim-7, 8, formatInfo[8]) + set(dim-6, 8, formatInfo[9]) + set(dim-5, 8, formatInfo[10]) + set(dim-4, 8, formatInfo[11]) + set(dim-3, 8, formatInfo[12]) + set(dim-2, 8, formatInfo[13]) + set(dim-1, 8, formatInfo[14]) + } +} + +var versionInfoBitsByVersion = map[byte][]bool{ + 7: []bool{false, false, false, true, true, true, true, true, false, false, true, false, false, true, false, true, false, false}, + 8: []bool{false, false, true, false, false, false, false, true, false, true, true, false, true, true, true, true, false, false}, + 9: []bool{false, false, true, false, false, true, true, false, true, false, true, false, false, true, true, false, false, true}, + 10: []bool{false, false, true, false, true, false, false, true, false, false, true, true, false, true, false, false, true, true}, + 11: []bool{false, false, true, false, true, true, true, false, true, true, true, true, true, true, false, true, true, false}, + 12: []bool{false, false, true, true, false, false, false, true, true, true, false, true, true, false, false, false, true, false}, + 13: []bool{false, false, true, true, false, true, true, false, false, false, false, true, false, false, false, true, true, true}, + 14: []bool{false, false, true, true, true, false, false, true, true, false, false, false, false, false, true, true, false, true}, + 15: []bool{false, false, true, true, true, true, true, false, false, true, false, false, true, false, true, false, false, false}, + 16: []bool{false, true, false, false, false, false, true, false, true, true, false, true, true, true, true, false, false, false}, + 17: []bool{false, true, false, false, false, true, false, true, false, false, false, true, false, true, true, true, false, true}, + 18: []bool{false, true, false, false, true, false, true, false, true, false, false, false, false, true, false, true, true, true}, + 19: []bool{false, true, false, false, true, true, false, true, false, true, false, false, true, true, false, false, true, false}, + 20: []bool{false, true, false, true, false, false, true, false, false, true, true, false, true, false, false, true, true, false}, + 21: []bool{false, true, false, true, false, true, false, true, true, false, true, false, false, false, false, false, true, true}, + 22: []bool{false, true, false, true, true, false, true, false, false, false, true, true, false, false, true, false, false, true}, + 23: []bool{false, true, false, true, true, true, false, true, true, true, true, true, true, false, true, true, false, false}, + 24: []bool{false, true, true, false, false, false, true, true, true, false, true, true, false, false, false, true, false, false}, + 25: []bool{false, true, true, false, false, true, false, false, false, true, true, true, true, false, false, false, false, true}, + 26: []bool{false, true, true, false, true, false, true, true, true, true, true, false, true, false, true, false, true, true}, + 27: []bool{false, true, true, false, true, true, false, false, false, false, true, false, false, false, true, true, true, false}, + 28: []bool{false, true, true, true, false, false, true, true, false, false, false, false, false, true, true, false, true, false}, + 29: []bool{false, true, true, true, false, true, false, false, true, true, false, false, true, true, true, true, true, true}, + 30: []bool{false, true, true, true, true, false, true, true, false, true, false, true, true, true, false, true, false, true}, + 31: []bool{false, true, true, true, true, true, false, false, true, false, false, true, false, true, false, false, false, false}, + 32: []bool{true, false, false, false, false, false, true, false, false, true, true, true, false, true, false, true, false, true}, + 33: []bool{true, false, false, false, false, true, false, true, true, false, true, true, true, true, false, false, false, false}, + 34: []bool{true, false, false, false, true, false, true, false, false, false, true, false, true, true, true, false, true, false}, + 35: []bool{true, false, false, false, true, true, false, true, true, true, true, false, false, true, true, true, true, true}, + 36: []bool{true, false, false, true, false, false, true, false, true, true, false, false, false, false, true, false, true, true}, + 37: []bool{true, false, false, true, false, true, false, true, false, false, false, false, true, false, true, true, true, false}, + 38: []bool{true, false, false, true, true, false, true, false, true, false, false, true, true, false, false, true, false, false}, + 39: []bool{true, false, false, true, true, true, false, true, false, true, false, true, false, false, false, false, false, true}, + 40: []bool{true, false, true, false, false, false, true, true, false, false, false, true, true, false, true, false, false, true}, +} + +func drawVersionInfo(vi *versionInfo, set func(int, int, bool)) { + versionInfoBits, ok := versionInfoBitsByVersion[vi.Version] + + if ok && len(versionInfoBits) > 0 { + for i := 0; i < len(versionInfoBits); i++ { + x := (vi.modulWidth() - 11) + i%3 + y := i / 3 + set(x, y, versionInfoBits[len(versionInfoBits)-i-1]) + set(y, x, versionInfoBits[len(versionInfoBits)-i-1]) + } + } + +} + +func addPaddingAndTerminator(bl *utils.BitList, vi *versionInfo) { + for i := 0; i < 4 && bl.Len() < vi.totalDataBytes()*8; i++ { + bl.AddBit(false) + } + + for bl.Len()%8 != 0 { + bl.AddBit(false) + } + + for i := 0; bl.Len() < vi.totalDataBytes()*8; i++ { + if i%2 == 0 { + bl.AddByte(236) + } else { + bl.AddByte(17) + } + } +} diff --git a/vendor/github.com/boombuler/barcode/qr/errorcorrection.go b/vendor/github.com/boombuler/barcode/qr/errorcorrection.go new file mode 100644 index 000000000000..08ebf0ce62e2 --- /dev/null +++ b/vendor/github.com/boombuler/barcode/qr/errorcorrection.go @@ -0,0 +1,29 @@ +package qr + +import ( + "github.com/boombuler/barcode/utils" +) + +type errorCorrection struct { + rs *utils.ReedSolomonEncoder +} + +var ec = newErrorCorrection() + +func newErrorCorrection() *errorCorrection { + fld := utils.NewGaloisField(285, 256, 0) + return &errorCorrection{utils.NewReedSolomonEncoder(fld)} +} + +func (ec *errorCorrection) calcECC(data []byte, eccCount byte) []byte { + dataInts := make([]int, len(data)) + for i := 0; i < len(data); i++ { + dataInts[i] = int(data[i]) + } + res := ec.rs.Encode(dataInts, int(eccCount)) + result := make([]byte, len(res)) + for i := 0; i < len(res); i++ { + result[i] = byte(res[i]) + } + return result +} diff --git a/vendor/github.com/boombuler/barcode/qr/numeric.go b/vendor/github.com/boombuler/barcode/qr/numeric.go new file mode 100644 index 000000000000..49b44cc45d3a --- /dev/null +++ b/vendor/github.com/boombuler/barcode/qr/numeric.go @@ -0,0 +1,56 @@ +package qr + +import ( + "errors" + "fmt" + "strconv" + + "github.com/boombuler/barcode/utils" +) + +func encodeNumeric(content string, ecl ErrorCorrectionLevel) (*utils.BitList, *versionInfo, error) { + contentBitCount := (len(content) / 3) * 10 + switch len(content) % 3 { + case 1: + contentBitCount += 4 + case 2: + contentBitCount += 7 + } + vi := findSmallestVersionInfo(ecl, numericMode, contentBitCount) + if vi == nil { + return nil, nil, errors.New("To much data to encode") + } + res := new(utils.BitList) + res.AddBits(int(numericMode), 4) + res.AddBits(len(content), vi.charCountBits(numericMode)) + + for pos := 0; pos < len(content); pos += 3 { + var curStr string + if pos+3 <= len(content) { + curStr = content[pos : pos+3] + } else { + curStr = content[pos:] + } + + i, err := strconv.Atoi(curStr) + if err != nil || i < 0 { + return nil, nil, fmt.Errorf("\"%s\" can not be encoded as %s", content, Numeric) + } + var bitCnt byte + switch len(curStr) % 3 { + case 0: + bitCnt = 10 + case 1: + bitCnt = 4 + break + case 2: + bitCnt = 7 + break + } + + res.AddBits(i, bitCnt) + } + + addPaddingAndTerminator(res, vi) + return res, vi, nil +} diff --git a/vendor/github.com/boombuler/barcode/qr/qrcode.go b/vendor/github.com/boombuler/barcode/qr/qrcode.go new file mode 100644 index 000000000000..b7ac26d74895 --- /dev/null +++ b/vendor/github.com/boombuler/barcode/qr/qrcode.go @@ -0,0 +1,166 @@ +package qr + +import ( + "image" + "image/color" + "math" + + "github.com/boombuler/barcode" + "github.com/boombuler/barcode/utils" +) + +type qrcode struct { + dimension int + data *utils.BitList + content string +} + +func (qr *qrcode) Content() string { + return qr.content +} + +func (qr *qrcode) Metadata() barcode.Metadata { + return barcode.Metadata{"QR Code", 2} +} + +func (qr *qrcode) ColorModel() color.Model { + return color.Gray16Model +} + +func (qr *qrcode) Bounds() image.Rectangle { + return image.Rect(0, 0, qr.dimension, qr.dimension) +} + +func (qr *qrcode) At(x, y int) color.Color { + if qr.Get(x, y) { + return color.Black + } + return color.White +} + +func (qr *qrcode) Get(x, y int) bool { + return qr.data.GetBit(x*qr.dimension + y) +} + +func (qr *qrcode) Set(x, y int, val bool) { + qr.data.SetBit(x*qr.dimension+y, val) +} + +func (qr *qrcode) calcPenalty() uint { + return qr.calcPenaltyRule1() + qr.calcPenaltyRule2() + qr.calcPenaltyRule3() + qr.calcPenaltyRule4() +} + +func (qr *qrcode) calcPenaltyRule1() uint { + var result uint + for x := 0; x < qr.dimension; x++ { + checkForX := false + var cntX uint + checkForY := false + var cntY uint + + for y := 0; y < qr.dimension; y++ { + if qr.Get(x, y) == checkForX { + cntX++ + } else { + checkForX = !checkForX + if cntX >= 5 { + result += cntX - 2 + } + cntX = 1 + } + + if qr.Get(y, x) == checkForY { + cntY++ + } else { + checkForY = !checkForY + if cntY >= 5 { + result += cntY - 2 + } + cntY = 1 + } + } + + if cntX >= 5 { + result += cntX - 2 + } + if cntY >= 5 { + result += cntY - 2 + } + } + + return result +} + +func (qr *qrcode) calcPenaltyRule2() uint { + var result uint + for x := 0; x < qr.dimension-1; x++ { + for y := 0; y < qr.dimension-1; y++ { + check := qr.Get(x, y) + if qr.Get(x, y+1) == check && qr.Get(x+1, y) == check && qr.Get(x+1, y+1) == check { + result += 3 + } + } + } + return result +} + +func (qr *qrcode) calcPenaltyRule3() uint { + pattern1 := []bool{true, false, true, true, true, false, true, false, false, false, false} + pattern2 := []bool{false, false, false, false, true, false, true, true, true, false, true} + + var result uint + for x := 0; x <= qr.dimension-len(pattern1); x++ { + for y := 0; y < qr.dimension; y++ { + pattern1XFound := true + pattern2XFound := true + pattern1YFound := true + pattern2YFound := true + + for i := 0; i < len(pattern1); i++ { + iv := qr.Get(x+i, y) + if iv != pattern1[i] { + pattern1XFound = false + } + if iv != pattern2[i] { + pattern2XFound = false + } + iv = qr.Get(y, x+i) + if iv != pattern1[i] { + pattern1YFound = false + } + if iv != pattern2[i] { + pattern2YFound = false + } + } + if pattern1XFound || pattern2XFound { + result += 40 + } + if pattern1YFound || pattern2YFound { + result += 40 + } + } + } + + return result +} + +func (qr *qrcode) calcPenaltyRule4() uint { + totalNum := qr.data.Len() + trueCnt := 0 + for i := 0; i < totalNum; i++ { + if qr.data.GetBit(i) { + trueCnt++ + } + } + percDark := float64(trueCnt) * 100 / float64(totalNum) + floor := math.Abs(math.Floor(percDark/5) - 10) + ceil := math.Abs(math.Ceil(percDark/5) - 10) + return uint(math.Min(floor, ceil) * 10) +} + +func newBarcode(dim int) *qrcode { + res := new(qrcode) + res.dimension = dim + res.data = utils.NewBitList(dim * dim) + return res +} diff --git a/vendor/github.com/boombuler/barcode/qr/unicode.go b/vendor/github.com/boombuler/barcode/qr/unicode.go new file mode 100644 index 000000000000..a9135ab6d967 --- /dev/null +++ b/vendor/github.com/boombuler/barcode/qr/unicode.go @@ -0,0 +1,27 @@ +package qr + +import ( + "errors" + + "github.com/boombuler/barcode/utils" +) + +func encodeUnicode(content string, ecl ErrorCorrectionLevel) (*utils.BitList, *versionInfo, error) { + data := []byte(content) + + vi := findSmallestVersionInfo(ecl, byteMode, len(data)*8) + if vi == nil { + return nil, nil, errors.New("To much data to encode") + } + + // It's not correct to add the unicode bytes to the result directly but most readers can't handle the + // required ECI header... + res := new(utils.BitList) + res.AddBits(int(byteMode), 4) + res.AddBits(len(content), vi.charCountBits(byteMode)) + for _, b := range data { + res.AddByte(b) + } + addPaddingAndTerminator(res, vi) + return res, vi, nil +} diff --git a/vendor/github.com/boombuler/barcode/qr/versioninfo.go b/vendor/github.com/boombuler/barcode/qr/versioninfo.go new file mode 100644 index 000000000000..6852a5766efe --- /dev/null +++ b/vendor/github.com/boombuler/barcode/qr/versioninfo.go @@ -0,0 +1,310 @@ +package qr + +import "math" + +// ErrorCorrectionLevel indicates the amount of "backup data" stored in the QR code +type ErrorCorrectionLevel byte + +const ( + // L recovers 7% of data + L ErrorCorrectionLevel = iota + // M recovers 15% of data + M + // Q recovers 25% of data + Q + // H recovers 30% of data + H +) + +func (ecl ErrorCorrectionLevel) String() string { + switch ecl { + case L: + return "L" + case M: + return "M" + case Q: + return "Q" + case H: + return "H" + } + return "unknown" +} + +type encodingMode byte + +const ( + numericMode encodingMode = 1 + alphaNumericMode encodingMode = 2 + byteMode encodingMode = 4 + kanjiMode encodingMode = 8 +) + +type versionInfo struct { + Version byte + Level ErrorCorrectionLevel + ErrorCorrectionCodewordsPerBlock byte + NumberOfBlocksInGroup1 byte + DataCodeWordsPerBlockInGroup1 byte + NumberOfBlocksInGroup2 byte + DataCodeWordsPerBlockInGroup2 byte +} + +var versionInfos = []*versionInfo{ + &versionInfo{1, L, 7, 1, 19, 0, 0}, + &versionInfo{1, M, 10, 1, 16, 0, 0}, + &versionInfo{1, Q, 13, 1, 13, 0, 0}, + &versionInfo{1, H, 17, 1, 9, 0, 0}, + &versionInfo{2, L, 10, 1, 34, 0, 0}, + &versionInfo{2, M, 16, 1, 28, 0, 0}, + &versionInfo{2, Q, 22, 1, 22, 0, 0}, + &versionInfo{2, H, 28, 1, 16, 0, 0}, + &versionInfo{3, L, 15, 1, 55, 0, 0}, + &versionInfo{3, M, 26, 1, 44, 0, 0}, + &versionInfo{3, Q, 18, 2, 17, 0, 0}, + &versionInfo{3, H, 22, 2, 13, 0, 0}, + &versionInfo{4, L, 20, 1, 80, 0, 0}, + &versionInfo{4, M, 18, 2, 32, 0, 0}, + &versionInfo{4, Q, 26, 2, 24, 0, 0}, + &versionInfo{4, H, 16, 4, 9, 0, 0}, + &versionInfo{5, L, 26, 1, 108, 0, 0}, + &versionInfo{5, M, 24, 2, 43, 0, 0}, + &versionInfo{5, Q, 18, 2, 15, 2, 16}, + &versionInfo{5, H, 22, 2, 11, 2, 12}, + &versionInfo{6, L, 18, 2, 68, 0, 0}, + &versionInfo{6, M, 16, 4, 27, 0, 0}, + &versionInfo{6, Q, 24, 4, 19, 0, 0}, + &versionInfo{6, H, 28, 4, 15, 0, 0}, + &versionInfo{7, L, 20, 2, 78, 0, 0}, + &versionInfo{7, M, 18, 4, 31, 0, 0}, + &versionInfo{7, Q, 18, 2, 14, 4, 15}, + &versionInfo{7, H, 26, 4, 13, 1, 14}, + &versionInfo{8, L, 24, 2, 97, 0, 0}, + &versionInfo{8, M, 22, 2, 38, 2, 39}, + &versionInfo{8, Q, 22, 4, 18, 2, 19}, + &versionInfo{8, H, 26, 4, 14, 2, 15}, + &versionInfo{9, L, 30, 2, 116, 0, 0}, + &versionInfo{9, M, 22, 3, 36, 2, 37}, + &versionInfo{9, Q, 20, 4, 16, 4, 17}, + &versionInfo{9, H, 24, 4, 12, 4, 13}, + &versionInfo{10, L, 18, 2, 68, 2, 69}, + &versionInfo{10, M, 26, 4, 43, 1, 44}, + &versionInfo{10, Q, 24, 6, 19, 2, 20}, + &versionInfo{10, H, 28, 6, 15, 2, 16}, + &versionInfo{11, L, 20, 4, 81, 0, 0}, + &versionInfo{11, M, 30, 1, 50, 4, 51}, + &versionInfo{11, Q, 28, 4, 22, 4, 23}, + &versionInfo{11, H, 24, 3, 12, 8, 13}, + &versionInfo{12, L, 24, 2, 92, 2, 93}, + &versionInfo{12, M, 22, 6, 36, 2, 37}, + &versionInfo{12, Q, 26, 4, 20, 6, 21}, + &versionInfo{12, H, 28, 7, 14, 4, 15}, + &versionInfo{13, L, 26, 4, 107, 0, 0}, + &versionInfo{13, M, 22, 8, 37, 1, 38}, + &versionInfo{13, Q, 24, 8, 20, 4, 21}, + &versionInfo{13, H, 22, 12, 11, 4, 12}, + &versionInfo{14, L, 30, 3, 115, 1, 116}, + &versionInfo{14, M, 24, 4, 40, 5, 41}, + &versionInfo{14, Q, 20, 11, 16, 5, 17}, + &versionInfo{14, H, 24, 11, 12, 5, 13}, + &versionInfo{15, L, 22, 5, 87, 1, 88}, + &versionInfo{15, M, 24, 5, 41, 5, 42}, + &versionInfo{15, Q, 30, 5, 24, 7, 25}, + &versionInfo{15, H, 24, 11, 12, 7, 13}, + &versionInfo{16, L, 24, 5, 98, 1, 99}, + &versionInfo{16, M, 28, 7, 45, 3, 46}, + &versionInfo{16, Q, 24, 15, 19, 2, 20}, + &versionInfo{16, H, 30, 3, 15, 13, 16}, + &versionInfo{17, L, 28, 1, 107, 5, 108}, + &versionInfo{17, M, 28, 10, 46, 1, 47}, + &versionInfo{17, Q, 28, 1, 22, 15, 23}, + &versionInfo{17, H, 28, 2, 14, 17, 15}, + &versionInfo{18, L, 30, 5, 120, 1, 121}, + &versionInfo{18, M, 26, 9, 43, 4, 44}, + &versionInfo{18, Q, 28, 17, 22, 1, 23}, + &versionInfo{18, H, 28, 2, 14, 19, 15}, + &versionInfo{19, L, 28, 3, 113, 4, 114}, + &versionInfo{19, M, 26, 3, 44, 11, 45}, + &versionInfo{19, Q, 26, 17, 21, 4, 22}, + &versionInfo{19, H, 26, 9, 13, 16, 14}, + &versionInfo{20, L, 28, 3, 107, 5, 108}, + &versionInfo{20, M, 26, 3, 41, 13, 42}, + &versionInfo{20, Q, 30, 15, 24, 5, 25}, + &versionInfo{20, H, 28, 15, 15, 10, 16}, + &versionInfo{21, L, 28, 4, 116, 4, 117}, + &versionInfo{21, M, 26, 17, 42, 0, 0}, + &versionInfo{21, Q, 28, 17, 22, 6, 23}, + &versionInfo{21, H, 30, 19, 16, 6, 17}, + &versionInfo{22, L, 28, 2, 111, 7, 112}, + &versionInfo{22, M, 28, 17, 46, 0, 0}, + &versionInfo{22, Q, 30, 7, 24, 16, 25}, + &versionInfo{22, H, 24, 34, 13, 0, 0}, + &versionInfo{23, L, 30, 4, 121, 5, 122}, + &versionInfo{23, M, 28, 4, 47, 14, 48}, + &versionInfo{23, Q, 30, 11, 24, 14, 25}, + &versionInfo{23, H, 30, 16, 15, 14, 16}, + &versionInfo{24, L, 30, 6, 117, 4, 118}, + &versionInfo{24, M, 28, 6, 45, 14, 46}, + &versionInfo{24, Q, 30, 11, 24, 16, 25}, + &versionInfo{24, H, 30, 30, 16, 2, 17}, + &versionInfo{25, L, 26, 8, 106, 4, 107}, + &versionInfo{25, M, 28, 8, 47, 13, 48}, + &versionInfo{25, Q, 30, 7, 24, 22, 25}, + &versionInfo{25, H, 30, 22, 15, 13, 16}, + &versionInfo{26, L, 28, 10, 114, 2, 115}, + &versionInfo{26, M, 28, 19, 46, 4, 47}, + &versionInfo{26, Q, 28, 28, 22, 6, 23}, + &versionInfo{26, H, 30, 33, 16, 4, 17}, + &versionInfo{27, L, 30, 8, 122, 4, 123}, + &versionInfo{27, M, 28, 22, 45, 3, 46}, + &versionInfo{27, Q, 30, 8, 23, 26, 24}, + &versionInfo{27, H, 30, 12, 15, 28, 16}, + &versionInfo{28, L, 30, 3, 117, 10, 118}, + &versionInfo{28, M, 28, 3, 45, 23, 46}, + &versionInfo{28, Q, 30, 4, 24, 31, 25}, + &versionInfo{28, H, 30, 11, 15, 31, 16}, + &versionInfo{29, L, 30, 7, 116, 7, 117}, + &versionInfo{29, M, 28, 21, 45, 7, 46}, + &versionInfo{29, Q, 30, 1, 23, 37, 24}, + &versionInfo{29, H, 30, 19, 15, 26, 16}, + &versionInfo{30, L, 30, 5, 115, 10, 116}, + &versionInfo{30, M, 28, 19, 47, 10, 48}, + &versionInfo{30, Q, 30, 15, 24, 25, 25}, + &versionInfo{30, H, 30, 23, 15, 25, 16}, + &versionInfo{31, L, 30, 13, 115, 3, 116}, + &versionInfo{31, M, 28, 2, 46, 29, 47}, + &versionInfo{31, Q, 30, 42, 24, 1, 25}, + &versionInfo{31, H, 30, 23, 15, 28, 16}, + &versionInfo{32, L, 30, 17, 115, 0, 0}, + &versionInfo{32, M, 28, 10, 46, 23, 47}, + &versionInfo{32, Q, 30, 10, 24, 35, 25}, + &versionInfo{32, H, 30, 19, 15, 35, 16}, + &versionInfo{33, L, 30, 17, 115, 1, 116}, + &versionInfo{33, M, 28, 14, 46, 21, 47}, + &versionInfo{33, Q, 30, 29, 24, 19, 25}, + &versionInfo{33, H, 30, 11, 15, 46, 16}, + &versionInfo{34, L, 30, 13, 115, 6, 116}, + &versionInfo{34, M, 28, 14, 46, 23, 47}, + &versionInfo{34, Q, 30, 44, 24, 7, 25}, + &versionInfo{34, H, 30, 59, 16, 1, 17}, + &versionInfo{35, L, 30, 12, 121, 7, 122}, + &versionInfo{35, M, 28, 12, 47, 26, 48}, + &versionInfo{35, Q, 30, 39, 24, 14, 25}, + &versionInfo{35, H, 30, 22, 15, 41, 16}, + &versionInfo{36, L, 30, 6, 121, 14, 122}, + &versionInfo{36, M, 28, 6, 47, 34, 48}, + &versionInfo{36, Q, 30, 46, 24, 10, 25}, + &versionInfo{36, H, 30, 2, 15, 64, 16}, + &versionInfo{37, L, 30, 17, 122, 4, 123}, + &versionInfo{37, M, 28, 29, 46, 14, 47}, + &versionInfo{37, Q, 30, 49, 24, 10, 25}, + &versionInfo{37, H, 30, 24, 15, 46, 16}, + &versionInfo{38, L, 30, 4, 122, 18, 123}, + &versionInfo{38, M, 28, 13, 46, 32, 47}, + &versionInfo{38, Q, 30, 48, 24, 14, 25}, + &versionInfo{38, H, 30, 42, 15, 32, 16}, + &versionInfo{39, L, 30, 20, 117, 4, 118}, + &versionInfo{39, M, 28, 40, 47, 7, 48}, + &versionInfo{39, Q, 30, 43, 24, 22, 25}, + &versionInfo{39, H, 30, 10, 15, 67, 16}, + &versionInfo{40, L, 30, 19, 118, 6, 119}, + &versionInfo{40, M, 28, 18, 47, 31, 48}, + &versionInfo{40, Q, 30, 34, 24, 34, 25}, + &versionInfo{40, H, 30, 20, 15, 61, 16}, +} + +func (vi *versionInfo) totalDataBytes() int { + g1Data := int(vi.NumberOfBlocksInGroup1) * int(vi.DataCodeWordsPerBlockInGroup1) + g2Data := int(vi.NumberOfBlocksInGroup2) * int(vi.DataCodeWordsPerBlockInGroup2) + return (g1Data + g2Data) +} + +func (vi *versionInfo) charCountBits(m encodingMode) byte { + switch m { + case numericMode: + if vi.Version < 10 { + return 10 + } else if vi.Version < 27 { + return 12 + } + return 14 + + case alphaNumericMode: + if vi.Version < 10 { + return 9 + } else if vi.Version < 27 { + return 11 + } + return 13 + + case byteMode: + if vi.Version < 10 { + return 8 + } + return 16 + + case kanjiMode: + if vi.Version < 10 { + return 8 + } else if vi.Version < 27 { + return 10 + } + return 12 + default: + return 0 + } +} + +func (vi *versionInfo) modulWidth() int { + return ((int(vi.Version) - 1) * 4) + 21 +} + +func (vi *versionInfo) alignmentPatternPlacements() []int { + if vi.Version == 1 { + return make([]int, 0) + } + + first := 6 + last := vi.modulWidth() - 7 + space := float64(last - first) + count := int(math.Ceil(space/28)) + 1 + + result := make([]int, count) + result[0] = first + result[len(result)-1] = last + if count > 2 { + step := int(math.Ceil(float64(last-first) / float64(count-1))) + if step%2 == 1 { + frac := float64(last-first) / float64(count-1) + _, x := math.Modf(frac) + if x >= 0.5 { + frac = math.Ceil(frac) + } else { + frac = math.Floor(frac) + } + + if int(frac)%2 == 0 { + step-- + } else { + step++ + } + } + + for i := 1; i <= count-2; i++ { + result[i] = last - (step * (count - 1 - i)) + } + } + + return result +} + +func findSmallestVersionInfo(ecl ErrorCorrectionLevel, mode encodingMode, dataBits int) *versionInfo { + dataBits = dataBits + 4 // mode indicator + for _, vi := range versionInfos { + if vi.Level == ecl { + if (vi.totalDataBytes() * 8) >= (dataBits + int(vi.charCountBits(mode))) { + return vi + } + } + } + return nil +} diff --git a/vendor/github.com/boombuler/barcode/scaledbarcode.go b/vendor/github.com/boombuler/barcode/scaledbarcode.go new file mode 100644 index 000000000000..152b18017480 --- /dev/null +++ b/vendor/github.com/boombuler/barcode/scaledbarcode.go @@ -0,0 +1,134 @@ +package barcode + +import ( + "errors" + "fmt" + "image" + "image/color" + "math" +) + +type wrapFunc func(x, y int) color.Color + +type scaledBarcode struct { + wrapped Barcode + wrapperFunc wrapFunc + rect image.Rectangle +} + +type intCSscaledBC struct { + scaledBarcode +} + +func (bc *scaledBarcode) Content() string { + return bc.wrapped.Content() +} + +func (bc *scaledBarcode) Metadata() Metadata { + return bc.wrapped.Metadata() +} + +func (bc *scaledBarcode) ColorModel() color.Model { + return bc.wrapped.ColorModel() +} + +func (bc *scaledBarcode) Bounds() image.Rectangle { + return bc.rect +} + +func (bc *scaledBarcode) At(x, y int) color.Color { + return bc.wrapperFunc(x, y) +} + +func (bc *intCSscaledBC) CheckSum() int { + if cs, ok := bc.wrapped.(BarcodeIntCS); ok { + return cs.CheckSum() + } + return 0 +} + +// Scale returns a resized barcode with the given width and height. +func Scale(bc Barcode, width, height int) (Barcode, error) { + switch bc.Metadata().Dimensions { + case 1: + return scale1DCode(bc, width, height) + case 2: + return scale2DCode(bc, width, height) + } + + return nil, errors.New("unsupported barcode format") +} + +func newScaledBC(wrapped Barcode, wrapperFunc wrapFunc, rect image.Rectangle) Barcode { + result := &scaledBarcode{ + wrapped: wrapped, + wrapperFunc: wrapperFunc, + rect: rect, + } + + if _, ok := wrapped.(BarcodeIntCS); ok { + return &intCSscaledBC{*result} + } + return result +} + +func scale2DCode(bc Barcode, width, height int) (Barcode, error) { + orgBounds := bc.Bounds() + orgWidth := orgBounds.Max.X - orgBounds.Min.X + orgHeight := orgBounds.Max.Y - orgBounds.Min.Y + + factor := int(math.Min(float64(width)/float64(orgWidth), float64(height)/float64(orgHeight))) + if factor <= 0 { + return nil, fmt.Errorf("can not scale barcode to an image smaller than %dx%d", orgWidth, orgHeight) + } + + offsetX := (width - (orgWidth * factor)) / 2 + offsetY := (height - (orgHeight * factor)) / 2 + + wrap := func(x, y int) color.Color { + if x < offsetX || y < offsetY { + return color.White + } + x = (x - offsetX) / factor + y = (y - offsetY) / factor + if x >= orgWidth || y >= orgHeight { + return color.White + } + return bc.At(x, y) + } + + return newScaledBC( + bc, + wrap, + image.Rect(0, 0, width, height), + ), nil +} + +func scale1DCode(bc Barcode, width, height int) (Barcode, error) { + orgBounds := bc.Bounds() + orgWidth := orgBounds.Max.X - orgBounds.Min.X + factor := int(float64(width) / float64(orgWidth)) + + if factor <= 0 { + return nil, fmt.Errorf("can not scale barcode to an image smaller than %dx1", orgWidth) + } + offsetX := (width - (orgWidth * factor)) / 2 + + wrap := func(x, y int) color.Color { + if x < offsetX { + return color.White + } + x = (x - offsetX) / factor + + if x >= orgWidth { + return color.White + } + return bc.At(x, 0) + } + + return newScaledBC( + bc, + wrap, + image.Rect(0, 0, width, height), + ), nil +} diff --git a/vendor/github.com/boombuler/barcode/utils/base1dcode.go b/vendor/github.com/boombuler/barcode/utils/base1dcode.go new file mode 100644 index 000000000000..75e50048c648 --- /dev/null +++ b/vendor/github.com/boombuler/barcode/utils/base1dcode.go @@ -0,0 +1,57 @@ +// Package utils contain some utilities which are needed to create barcodes +package utils + +import ( + "image" + "image/color" + + "github.com/boombuler/barcode" +) + +type base1DCode struct { + *BitList + kind string + content string +} + +type base1DCodeIntCS struct { + base1DCode + checksum int +} + +func (c *base1DCode) Content() string { + return c.content +} + +func (c *base1DCode) Metadata() barcode.Metadata { + return barcode.Metadata{c.kind, 1} +} + +func (c *base1DCode) ColorModel() color.Model { + return color.Gray16Model +} + +func (c *base1DCode) Bounds() image.Rectangle { + return image.Rect(0, 0, c.Len(), 1) +} + +func (c *base1DCode) At(x, y int) color.Color { + if c.GetBit(x) { + return color.Black + } + return color.White +} + +func (c *base1DCodeIntCS) CheckSum() int { + return c.checksum +} + +// New1DCode creates a new 1D barcode where the bars are represented by the bits in the bars BitList +func New1DCodeIntCheckSum(codeKind, content string, bars *BitList, checksum int) barcode.BarcodeIntCS { + return &base1DCodeIntCS{base1DCode{bars, codeKind, content}, checksum} +} + +// New1DCode creates a new 1D barcode where the bars are represented by the bits in the bars BitList +func New1DCode(codeKind, content string, bars *BitList) barcode.Barcode { + return &base1DCode{bars, codeKind, content} +} diff --git a/vendor/github.com/boombuler/barcode/utils/bitlist.go b/vendor/github.com/boombuler/barcode/utils/bitlist.go new file mode 100644 index 000000000000..bb05e53b5d75 --- /dev/null +++ b/vendor/github.com/boombuler/barcode/utils/bitlist.go @@ -0,0 +1,119 @@ +package utils + +// BitList is a list that contains bits +type BitList struct { + count int + data []int32 +} + +// NewBitList returns a new BitList with the given length +// all bits are initialize with false +func NewBitList(capacity int) *BitList { + bl := new(BitList) + bl.count = capacity + x := 0 + if capacity%32 != 0 { + x = 1 + } + bl.data = make([]int32, capacity/32+x) + return bl +} + +// Len returns the number of contained bits +func (bl *BitList) Len() int { + return bl.count +} + +func (bl *BitList) grow() { + growBy := len(bl.data) + if growBy < 128 { + growBy = 128 + } else if growBy >= 1024 { + growBy = 1024 + } + + nd := make([]int32, len(bl.data)+growBy) + copy(nd, bl.data) + bl.data = nd +} + +// AddBit appends the given bits to the end of the list +func (bl *BitList) AddBit(bits ...bool) { + for _, bit := range bits { + itmIndex := bl.count / 32 + for itmIndex >= len(bl.data) { + bl.grow() + } + bl.SetBit(bl.count, bit) + bl.count++ + } +} + +// SetBit sets the bit at the given index to the given value +func (bl *BitList) SetBit(index int, value bool) { + itmIndex := index / 32 + itmBitShift := 31 - (index % 32) + if value { + bl.data[itmIndex] = bl.data[itmIndex] | 1<> uint(itmBitShift)) & 1) == 1 +} + +// AddByte appends all 8 bits of the given byte to the end of the list +func (bl *BitList) AddByte(b byte) { + for i := 7; i >= 0; i-- { + bl.AddBit(((b >> uint(i)) & 1) == 1) + } +} + +// AddBits appends the last (LSB) 'count' bits of 'b' the the end of the list +func (bl *BitList) AddBits(b int, count byte) { + for i := int(count) - 1; i >= 0; i-- { + bl.AddBit(((b >> uint(i)) & 1) == 1) + } +} + +// GetBytes returns all bits of the BitList as a []byte +func (bl *BitList) GetBytes() []byte { + len := bl.count >> 3 + if (bl.count % 8) != 0 { + len++ + } + result := make([]byte, len) + for i := 0; i < len; i++ { + shift := (3 - (i % 4)) * 8 + result[i] = (byte)((bl.data[i/4] >> uint(shift)) & 0xFF) + } + return result +} + +// IterateBytes iterates through all bytes contained in the BitList +func (bl *BitList) IterateBytes() <-chan byte { + res := make(chan byte) + + go func() { + c := bl.count + shift := 24 + i := 0 + for c > 0 { + res <- byte((bl.data[i] >> uint(shift)) & 0xFF) + shift -= 8 + if shift < 0 { + shift = 24 + i++ + } + c -= 8 + } + close(res) + }() + + return res +} diff --git a/vendor/github.com/boombuler/barcode/utils/galoisfield.go b/vendor/github.com/boombuler/barcode/utils/galoisfield.go new file mode 100644 index 000000000000..68726fbfdef4 --- /dev/null +++ b/vendor/github.com/boombuler/barcode/utils/galoisfield.go @@ -0,0 +1,65 @@ +package utils + +// GaloisField encapsulates galois field arithmetics +type GaloisField struct { + Size int + Base int + ALogTbl []int + LogTbl []int +} + +// NewGaloisField creates a new galois field +func NewGaloisField(pp, fieldSize, b int) *GaloisField { + result := new(GaloisField) + + result.Size = fieldSize + result.Base = b + result.ALogTbl = make([]int, fieldSize) + result.LogTbl = make([]int, fieldSize) + + x := 1 + for i := 0; i < fieldSize; i++ { + result.ALogTbl[i] = x + x = x * 2 + if x >= fieldSize { + x = (x ^ pp) & (fieldSize - 1) + } + } + + for i := 0; i < fieldSize; i++ { + result.LogTbl[result.ALogTbl[i]] = int(i) + } + + return result +} + +func (gf *GaloisField) Zero() *GFPoly { + return NewGFPoly(gf, []int{0}) +} + +// AddOrSub add or substract two numbers +func (gf *GaloisField) AddOrSub(a, b int) int { + return a ^ b +} + +// Multiply multiplys two numbers +func (gf *GaloisField) Multiply(a, b int) int { + if a == 0 || b == 0 { + return 0 + } + return gf.ALogTbl[(gf.LogTbl[a]+gf.LogTbl[b])%(gf.Size-1)] +} + +// Divide divides two numbers +func (gf *GaloisField) Divide(a, b int) int { + if b == 0 { + panic("divide by zero") + } else if a == 0 { + return 0 + } + return gf.ALogTbl[(gf.LogTbl[a]-gf.LogTbl[b])%(gf.Size-1)] +} + +func (gf *GaloisField) Invers(num int) int { + return gf.ALogTbl[(gf.Size-1)-gf.LogTbl[num]] +} diff --git a/vendor/github.com/boombuler/barcode/utils/gfpoly.go b/vendor/github.com/boombuler/barcode/utils/gfpoly.go new file mode 100644 index 000000000000..c56bb40b9a90 --- /dev/null +++ b/vendor/github.com/boombuler/barcode/utils/gfpoly.go @@ -0,0 +1,103 @@ +package utils + +type GFPoly struct { + gf *GaloisField + Coefficients []int +} + +func (gp *GFPoly) Degree() int { + return len(gp.Coefficients) - 1 +} + +func (gp *GFPoly) Zero() bool { + return gp.Coefficients[0] == 0 +} + +// GetCoefficient returns the coefficient of x ^ degree +func (gp *GFPoly) GetCoefficient(degree int) int { + return gp.Coefficients[gp.Degree()-degree] +} + +func (gp *GFPoly) AddOrSubstract(other *GFPoly) *GFPoly { + if gp.Zero() { + return other + } else if other.Zero() { + return gp + } + smallCoeff := gp.Coefficients + largeCoeff := other.Coefficients + if len(smallCoeff) > len(largeCoeff) { + largeCoeff, smallCoeff = smallCoeff, largeCoeff + } + sumDiff := make([]int, len(largeCoeff)) + lenDiff := len(largeCoeff) - len(smallCoeff) + copy(sumDiff, largeCoeff[:lenDiff]) + for i := lenDiff; i < len(largeCoeff); i++ { + sumDiff[i] = int(gp.gf.AddOrSub(int(smallCoeff[i-lenDiff]), int(largeCoeff[i]))) + } + return NewGFPoly(gp.gf, sumDiff) +} + +func (gp *GFPoly) MultByMonominal(degree int, coeff int) *GFPoly { + if coeff == 0 { + return gp.gf.Zero() + } + size := len(gp.Coefficients) + result := make([]int, size+degree) + for i := 0; i < size; i++ { + result[i] = int(gp.gf.Multiply(int(gp.Coefficients[i]), int(coeff))) + } + return NewGFPoly(gp.gf, result) +} + +func (gp *GFPoly) Multiply(other *GFPoly) *GFPoly { + if gp.Zero() || other.Zero() { + return gp.gf.Zero() + } + aCoeff := gp.Coefficients + aLen := len(aCoeff) + bCoeff := other.Coefficients + bLen := len(bCoeff) + product := make([]int, aLen+bLen-1) + for i := 0; i < aLen; i++ { + ac := int(aCoeff[i]) + for j := 0; j < bLen; j++ { + bc := int(bCoeff[j]) + product[i+j] = int(gp.gf.AddOrSub(int(product[i+j]), gp.gf.Multiply(ac, bc))) + } + } + return NewGFPoly(gp.gf, product) +} + +func (gp *GFPoly) Divide(other *GFPoly) (quotient *GFPoly, remainder *GFPoly) { + quotient = gp.gf.Zero() + remainder = gp + fld := gp.gf + denomLeadTerm := other.GetCoefficient(other.Degree()) + inversDenomLeadTerm := fld.Invers(int(denomLeadTerm)) + for remainder.Degree() >= other.Degree() && !remainder.Zero() { + degreeDiff := remainder.Degree() - other.Degree() + scale := int(fld.Multiply(int(remainder.GetCoefficient(remainder.Degree())), inversDenomLeadTerm)) + term := other.MultByMonominal(degreeDiff, scale) + itQuot := NewMonominalPoly(fld, degreeDiff, scale) + quotient = quotient.AddOrSubstract(itQuot) + remainder = remainder.AddOrSubstract(term) + } + return +} + +func NewMonominalPoly(field *GaloisField, degree int, coeff int) *GFPoly { + if coeff == 0 { + return field.Zero() + } + result := make([]int, degree+1) + result[0] = coeff + return NewGFPoly(field, result) +} + +func NewGFPoly(field *GaloisField, coefficients []int) *GFPoly { + for len(coefficients) > 1 && coefficients[0] == 0 { + coefficients = coefficients[1:] + } + return &GFPoly{field, coefficients} +} diff --git a/vendor/github.com/boombuler/barcode/utils/reedsolomon.go b/vendor/github.com/boombuler/barcode/utils/reedsolomon.go new file mode 100644 index 000000000000..53af91ad446f --- /dev/null +++ b/vendor/github.com/boombuler/barcode/utils/reedsolomon.go @@ -0,0 +1,44 @@ +package utils + +import ( + "sync" +) + +type ReedSolomonEncoder struct { + gf *GaloisField + polynomes []*GFPoly + m *sync.Mutex +} + +func NewReedSolomonEncoder(gf *GaloisField) *ReedSolomonEncoder { + return &ReedSolomonEncoder{ + gf, []*GFPoly{NewGFPoly(gf, []int{1})}, new(sync.Mutex), + } +} + +func (rs *ReedSolomonEncoder) getPolynomial(degree int) *GFPoly { + rs.m.Lock() + defer rs.m.Unlock() + + if degree >= len(rs.polynomes) { + last := rs.polynomes[len(rs.polynomes)-1] + for d := len(rs.polynomes); d <= degree; d++ { + next := last.Multiply(NewGFPoly(rs.gf, []int{1, rs.gf.ALogTbl[d-1+rs.gf.Base]})) + rs.polynomes = append(rs.polynomes, next) + last = next + } + } + return rs.polynomes[degree] +} + +func (rs *ReedSolomonEncoder) Encode(data []int, eccCount int) []int { + generator := rs.getPolynomial(eccCount) + info := NewGFPoly(rs.gf, data) + info = info.MultByMonominal(eccCount, 1) + _, remainder := info.Divide(generator) + + result := make([]int, eccCount) + numZero := int(eccCount) - len(remainder.Coefficients) + copy(result[numZero:], remainder.Coefficients) + return result +} diff --git a/vendor/github.com/boombuler/barcode/utils/runeint.go b/vendor/github.com/boombuler/barcode/utils/runeint.go new file mode 100644 index 000000000000..d2e5e61e5671 --- /dev/null +++ b/vendor/github.com/boombuler/barcode/utils/runeint.go @@ -0,0 +1,19 @@ +package utils + +// RuneToInt converts a rune between '0' and '9' to an integer between 0 and 9 +// If the rune is outside of this range -1 is returned. +func RuneToInt(r rune) int { + if r >= '0' && r <= '9' { + return int(r - '0') + } + return -1 +} + +// IntToRune converts a digit 0 - 9 to the rune '0' - '9'. If the given int is outside +// of this range 'F' is returned! +func IntToRune(i int) rune { + if i >= 0 && i <= 9 { + return rune(i + '0') + } + return 'F' +} diff --git a/vendor/github.com/pquerna/otp/example/main.go b/vendor/github.com/pquerna/otp/example/main.go new file mode 100644 index 000000000000..77a3e3bebafc --- /dev/null +++ b/vendor/github.com/pquerna/otp/example/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" + + "bufio" + "bytes" + "fmt" + "image/png" + "io/ioutil" + "os" +) + +func display(key *otp.Key, data []byte) { + fmt.Printf("Issuer: %s\n", key.Issuer()) + fmt.Printf("Account Name: %s\n", key.AccountName()) + fmt.Printf("Secret: %s\n", key.Secret()) + fmt.Println("Writing PNG to qr-code.png....") + ioutil.WriteFile("qr-code.png", data, 0644) + fmt.Println("") + fmt.Println("Please add your TOTP to your OTP Application now!") + fmt.Println("") +} + +func promptForPasscode() string { + reader := bufio.NewReader(os.Stdin) + fmt.Print("Enter Passcode: ") + text, _ := reader.ReadString('\n') + return text +} + +func main() { + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: "Example.com", + AccountName: "alice@example.com", + }) + if err != nil { + panic(err) + } + // Convert TOTP key into a PNG + var buf bytes.Buffer + img, err := key.Image(200, 200) + if err != nil { + panic(err) + } + png.Encode(&buf, img) + + // display the QR code to the user. + display(key, buf.Bytes()) + + // Now Validate that the user's successfully added the passcode. + fmt.Println("Validating TOTP...") + passcode := promptForPasscode() + valid := totp.Validate(passcode, key.Secret()) + if valid { + println("Valid passcode!") + os.Exit(0) + } else { + println("Invalid passocde!") + os.Exit(1) + } +} diff --git a/vendor/vendor.json b/vendor/vendor.json index ee93f5e236b9..712557e84b2c 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -342,6 +342,24 @@ "revision": "675b82c74c0ed12283ee81ba8a534c8982c07b85", "revisionTime": "2016-10-13T10:26:35Z" }, + { + "checksumSHA1": "tLl952GRIVsso2Pk/IH3cMJaK8E=", + "path": "github.com/boombuler/barcode", + "revision": "c07adc95a07cb6213cf4c104caab763bce2d49c0", + "revisionTime": "2017-01-31T17:48:17Z" + }, + { + "checksumSHA1": "tbwzn+sWiZv6veyXae3qRfTjlcQ=", + "path": "github.com/boombuler/barcode/qr", + "revision": "c07adc95a07cb6213cf4c104caab763bce2d49c0", + "revisionTime": "2017-01-31T17:48:17Z" + }, + { + "checksumSHA1": "axe0OTdOjYa+XKDUYqzOv7FGaWo=", + "path": "github.com/boombuler/barcode/utils", + "revision": "c07adc95a07cb6213cf4c104caab763bce2d49c0", + "revisionTime": "2017-01-31T17:48:17Z" + }, { "checksumSHA1": "m6QMy+u/IX6knzP28DqMn4sdLSw=", "path": "github.com/cenk/backoff", @@ -1110,6 +1128,13 @@ "revision": "248dadf4e9068a0b3e79f02ed0a610d935de5302", "revisionTime": "2016-10-29T09:36:37Z" }, + { + "checksumSHA1": "Uldkr4yJ25TcI1+KPc6NP66c4Gg=", + "path": "github.com/pquerna/otp", + "revision": "9e1935374bc73ffe011187dafed51a412b90fe43", + "revisionTime": "2017-02-23T01:06:52Z", + "tree": true + }, { "checksumSHA1": "ZOhewV1DsQjTYlx8a+ifrZki2Vg=", "path": "github.com/ryanuber/columnize", From 3825e486b59ab6350cfe5d44601b458c64bbc95a Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Wed, 15 Mar 2017 12:45:57 -0700 Subject: [PATCH 19/43] Added additional tests for TOTP backend --- builtin/logical/totp/backend_test.go | 423 +++++++++++++++++++++++++-- 1 file changed, 391 insertions(+), 32 deletions(-) diff --git a/builtin/logical/totp/backend_test.go b/builtin/logical/totp/backend_test.go index d8d3cf19d6a5..d77fca1760f7 100644 --- a/builtin/logical/totp/backend_test.go +++ b/builtin/logical/totp/backend_test.go @@ -10,19 +10,19 @@ import ( "github.com/hashicorp/vault/logical" logicaltest "github.com/hashicorp/vault/logical/testing" "github.com/mitchellh/mapstructure" - otplib "github.com/pquerna/otp/" + otplib "github.com/pquerna/otp" totplib "github.com/pquerna/otp/totp" ) /* - Test each algorithm type - Test digits - Test periods - Test defaults - Test invalid period (negative) - Test invalid key - Test invalid account_name - Test invalid issuer + X Test each algorithm type + X Test digits + X Test periods + X Test defaults + X Test invalid period (negative) + X Test invalid key + X Test invalid account_name + X Test invalid issuer */ func createKey() (string, error) { @@ -36,7 +36,7 @@ func createKey() (string, error) { return key, err } -func TestBackend_basic(t *testing.T) { +func TestBackend_readCredentialsDefaultValues(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(config) @@ -53,11 +53,206 @@ func TestBackend_basic(t *testing.T) { "key": key, } + expected := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "digits": 6, + "period": 30, + "algorithm": "SHA1", + "key": key, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateRole(t, "test", roleData, false), + testAccStepReadRole(t, "test", expected), + testAccStepReadCreds(t, b, config.StorageView, "test", expected), + }, + }) +} + +func TestBackend_readCredentialsEightDigitsThirtySecondPeriod(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + // Generate a new shared key + key, _ := createKey() + + roleData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "key": key, + "digits": 8, + } + + expected := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "digits": 8, + "period": 30, + "algorithm": "SHA1", + "key": key, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateRole(t, "test", roleData, false), + testAccStepReadRole(t, "test", expected), + testAccStepReadCreds(t, b, config.StorageView, "test", expected), + }, + }) +} + +func TestBackend_readCredentialsSixDigitsNinetySecondPeriod(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + // Generate a new shared key + key, _ := createKey() + + roleData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "key": key, + "period": 90, + } + + expected := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "digits": 6, + "period": 90, + "algorithm": "SHA1", + "key": key, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateRole(t, "test", roleData, false), + testAccStepReadRole(t, "test", expected), + testAccStepReadCreds(t, b, config.StorageView, "test", expected), + }, + }) +} + +func TestBackend_readCredentialsSHA256(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + // Generate a new shared key + key, _ := createKey() + + roleData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "key": key, + "algorithm": "SHA256", + } + + expected := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "digits": 6, + "period": 30, + "algorithm": "SHA256", + "key": key, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateRole(t, "test", roleData, false), + testAccStepReadRole(t, "test", expected), + testAccStepReadCreds(t, b, config.StorageView, "test", expected), + }, + }) +} + +func TestBackend_readCredentialsSHA512(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + // Generate a new shared key + key, _ := createKey() + + roleData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "key": key, + "algorithm": "SHA512", + } + + expected := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "digits": 6, + "period": 30, + "algorithm": "SHA512", + "key": key, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateRole(t, "test", roleData, false), + testAccStepReadRole(t, "test", expected), + testAccStepReadCreds(t, b, config.StorageView, "test", expected), + }, + }) +} + +func TestBackend_readCredentialsMD5(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + // Generate a new shared key + key, _ := createKey() + + roleData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "key": key, + "algorithm": "MD5", + } + + expected := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "digits": 6, + "period": 30, + "algorithm": "MD5", + "key": key, + } + logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ testAccStepCreateRole(t, "test", roleData, false), - testAccStepReadCreds(t, b, config.StorageView, "test"), + testAccStepReadRole(t, "test", expected), + testAccStepReadCreds(t, b, config.StorageView, "test", expected), }, }) } @@ -84,6 +279,7 @@ func TestBackend_roleCrudDefaultValues(t *testing.T) { "digits": 6, "period": 30, "algorithm": "SHA1", + "key": key, } logicaltest.Test(t, logicaltest.TestCase{ @@ -97,6 +293,183 @@ func TestBackend_roleCrudDefaultValues(t *testing.T) { }) } +func TestBackend_createRoleInvalidPeriod(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + // Generate a new shared key + key, _ := createKey() + + roleData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "key": key, + "algorithm": "SHA256", + "period": -50, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateRole(t, "test", roleData, true), + testAccStepReadRole(t, "test", nil), + }, + }) +} + +func TestBackend_createRoleMissingKey(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + roleData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateRole(t, "test", roleData, true), + testAccStepReadRole(t, "test", nil), + }, + }) +} + +func TestBackend_createRoleInvalidKey(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + roleData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "key": "1", + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateRole(t, "test", roleData, true), + testAccStepReadRole(t, "test", nil), + }, + }) +} + +func TestBackend_createRoleInvalidAlgorithm(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + // Generate a new shared key + key, _ := createKey() + + roleData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "key": key, + "algorithm": "BADALGORITHM", + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateRole(t, "test", roleData, true), + testAccStepReadRole(t, "test", nil), + }, + }) +} + +func TestBackend_createRoleInvalidDigits(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + // Generate a new shared key + key, _ := createKey() + + roleData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "key": key, + "digits": 20, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateRole(t, "test", roleData, true), + testAccStepReadRole(t, "test", nil), + }, + }) +} + +func TestBackend_createRoleMissingAccountName(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + // Generate a new shared key + key, _ := createKey() + + roleData := map[string]interface{}{ + "issuer": "Vault", + "key": key, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateRole(t, "test", roleData, true), + testAccStepReadRole(t, "test", nil), + }, + }) +} + +func TestBackend_createRoleMissingIssuer(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + // Generate a new shared key + key, _ := createKey() + + roleData := map[string]interface{}{ + "account_name": "Test", + "key": key, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateRole(t, "test", roleData, true), + testAccStepReadRole(t, "test", nil), + }, + }) +} + func testAccStepCreateRole(t *testing.T, name string, roleData map[string]interface{}, expectFail bool) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, @@ -113,7 +486,7 @@ func testAccStepDeleteRole(t *testing.T, name string) logicaltest.TestStep { } } -func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, name string) logicaltest.TestStep { +func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, name string, validation map[string]interface{}) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.ReadOperation, Path: path.Join("creds", name), @@ -126,24 +499,9 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, na } log.Printf("[TRACE] Generated credentials: %v", d) - // Read saved role entry - entry, err := s.Get("role/" + name) - - if err != nil { - t.Fatalf("Error retrieving role.") - } - if entry == nil { - t.Fatalf("Retrieved role is nil.") - } - - var role roleEntry - if err := entry.DecodeJSON(&role); err != nil { - t.Fatalf("JSON decoding error while reading role entry.") - } - // Translate digits and algorithm to a format the totp library understands var digits otplib.Digits - switch role.Digits { + switch validation["digits"] { case 6: digits = otplib.DigitsSix case 8: @@ -151,7 +509,7 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, na } var algorithm otplib.Algorithm - switch role.Algorithm { + switch validation["algorithm"] { case "SHA1": algorithm = otplib.AlgorithmSHA1 case "SHA256": @@ -164,10 +522,11 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, na algorithm = otplib.AlgorithmSHA1 } - period := uint(role.Period) + period := validation["period"].(int) + key := validation["key"].(string) - valid := totplib.ValidateCustom(d.Token, role.Key, time.Now().UTC(), totplib.ValidateOpts{ - Period: period, + valid, _ := totplib.ValidateCustom(d.Token, key, time.Now().UTC(), totplib.ValidateOpts{ + Period: uint(period), Skew: 1, Digits: digits, Algorithm: algorithm, From 07d5a2f5a580afa1d60a8ef65b82a59fe0130ab3 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Wed, 15 Mar 2017 12:48:48 -0700 Subject: [PATCH 20/43] Cleaned up comments in TOTP backend_test.go --- builtin/logical/totp/backend_test.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/builtin/logical/totp/backend_test.go b/builtin/logical/totp/backend_test.go index d77fca1760f7..0c339d0a84bf 100644 --- a/builtin/logical/totp/backend_test.go +++ b/builtin/logical/totp/backend_test.go @@ -14,17 +14,6 @@ import ( totplib "github.com/pquerna/otp/totp" ) -/* - X Test each algorithm type - X Test digits - X Test periods - X Test defaults - X Test invalid period (negative) - X Test invalid key - X Test invalid account_name - X Test invalid issuer -*/ - func createKey() (string, error) { keyUrl, err := totplib.Generate(totplib.GenerateOpts{ Issuer: "Vault", From 9d2b862edffdbec76304b83d318d94e63fe58da4 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Fri, 17 Mar 2017 12:17:56 -0700 Subject: [PATCH 21/43] Added default values of period, algorithm and digits to field schema --- builtin/logical/totp/path_roles.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/builtin/logical/totp/path_roles.go b/builtin/logical/totp/path_roles.go index c6d7349562bb..eeaffc81dae9 100644 --- a/builtin/logical/totp/path_roles.go +++ b/builtin/logical/totp/path_roles.go @@ -47,16 +47,19 @@ func pathRoles(b *backend) *framework.Path { "period": { Type: framework.TypeInt, + Default: 30, Description: `The length of time used to generate a counter for the TOTP token calculation.`, }, "algorithm": { Type: framework.TypeString, + Default: "SHA1", Description: `The hashing algorithm used to generate the TOTP token.`, }, "digits": { Type: framework.TypeInt, + Default: 6, Description: `The number of digits in the generated TOTP token.`, }, }, From 0010233a07595c4f3f67dd7e29c788504081320d Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Fri, 17 Mar 2017 12:24:14 -0700 Subject: [PATCH 22/43] Changed account_name and issuer fields to optional --- builtin/logical/totp/backend_test.go | 50 ---------------------------- builtin/logical/totp/path_roles.go | 8 ----- 2 files changed, 58 deletions(-) diff --git a/builtin/logical/totp/backend_test.go b/builtin/logical/totp/backend_test.go index 0c339d0a84bf..78d5132b62af 100644 --- a/builtin/logical/totp/backend_test.go +++ b/builtin/logical/totp/backend_test.go @@ -409,56 +409,6 @@ func TestBackend_createRoleInvalidDigits(t *testing.T) { }) } -func TestBackend_createRoleMissingAccountName(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(config) - if err != nil { - t.Fatal(err) - } - - // Generate a new shared key - key, _ := createKey() - - roleData := map[string]interface{}{ - "issuer": "Vault", - "key": key, - } - - logicaltest.Test(t, logicaltest.TestCase{ - Backend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateRole(t, "test", roleData, true), - testAccStepReadRole(t, "test", nil), - }, - }) -} - -func TestBackend_createRoleMissingIssuer(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(config) - if err != nil { - t.Fatal(err) - } - - // Generate a new shared key - key, _ := createKey() - - roleData := map[string]interface{}{ - "account_name": "Test", - "key": key, - } - - logicaltest.Test(t, logicaltest.TestCase{ - Backend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateRole(t, "test", roleData, true), - testAccStepReadRole(t, "test", nil), - }, - }) -} - func testAccStepCreateRole(t *testing.T, name string, roleData map[string]interface{}, expectFail bool) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, diff --git a/builtin/logical/totp/path_roles.go b/builtin/logical/totp/path_roles.go index eeaffc81dae9..f2bca013c394 100644 --- a/builtin/logical/totp/path_roles.go +++ b/builtin/logical/totp/path_roles.go @@ -160,14 +160,6 @@ func (b *backend) pathRoleCreate( return logical.ErrorResponse("The period value must be greater than zero."), nil } - if issuer == "" { - return logical.ErrorResponse("The issuer value is required."), nil - } - - if account_name == "" { - return logical.ErrorResponse("The account_name value is required."), nil - } - // Set optional parameters if neccessary if period == 0 { period = 30 From bf3d7a0040f57267021f141266089d96d1f1c805 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Fri, 17 Mar 2017 12:30:51 -0700 Subject: [PATCH 23/43] Removed MD5 as a hash algorithm option --- builtin/logical/totp/backend_test.go | 47 ++---------------------- builtin/logical/totp/path_role_create.go | 2 - builtin/logical/totp/path_roles.go | 2 +- 3 files changed, 4 insertions(+), 47 deletions(-) diff --git a/builtin/logical/totp/backend_test.go b/builtin/logical/totp/backend_test.go index 78d5132b62af..3972a65ca992 100644 --- a/builtin/logical/totp/backend_test.go +++ b/builtin/logical/totp/backend_test.go @@ -37,14 +37,12 @@ func TestBackend_readCredentialsDefaultValues(t *testing.T) { key, _ := createKey() roleData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "key": key, + "key": key, } expected := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", + "issuer": "", + "account_name": "", "digits": 6, "period": 30, "algorithm": "SHA1", @@ -209,43 +207,6 @@ func TestBackend_readCredentialsSHA512(t *testing.T) { }) } -func TestBackend_readCredentialsMD5(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(config) - if err != nil { - t.Fatal(err) - } - - // Generate a new shared key - key, _ := createKey() - - roleData := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "key": key, - "algorithm": "MD5", - } - - expected := map[string]interface{}{ - "issuer": "Vault", - "account_name": "Test", - "digits": 6, - "period": 30, - "algorithm": "MD5", - "key": key, - } - - logicaltest.Test(t, logicaltest.TestCase{ - Backend: b, - Steps: []logicaltest.TestStep{ - testAccStepCreateRole(t, "test", roleData, false), - testAccStepReadRole(t, "test", expected), - testAccStepReadCreds(t, b, config.StorageView, "test", expected), - }, - }) -} - func TestBackend_roleCrudDefaultValues(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} @@ -455,8 +416,6 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, na algorithm = otplib.AlgorithmSHA256 case "SHA512": algorithm = otplib.AlgorithmSHA512 - case "MD5": - algorithm = otplib.AlgorithmMD5 default: algorithm = otplib.AlgorithmSHA1 } diff --git a/builtin/logical/totp/path_role_create.go b/builtin/logical/totp/path_role_create.go index 336ffa6f3598..867606bc6f13 100644 --- a/builtin/logical/totp/path_role_create.go +++ b/builtin/logical/totp/path_role_create.go @@ -63,8 +63,6 @@ func (b *backend) pathRoleCreateRead( algorithm = otplib.AlgorithmSHA256 case "SHA512": algorithm = otplib.AlgorithmSHA512 - case "MD5": - algorithm = otplib.AlgorithmMD5 default: algorithm = otplib.AlgorithmSHA1 } diff --git a/builtin/logical/totp/path_roles.go b/builtin/logical/totp/path_roles.go index f2bca013c394..be4a006c8c42 100644 --- a/builtin/logical/totp/path_roles.go +++ b/builtin/logical/totp/path_roles.go @@ -166,7 +166,7 @@ func (b *backend) pathRoleCreate( } switch algorithm { - case "SHA1", "SHA256", "SHA512", "MD5": + case "SHA1", "SHA256", "SHA512": case "": algorithm = "SHA1" default: From 6dadf12f7b0ec89202d092333015e6ccc76647d4 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Mon, 20 Mar 2017 12:40:06 -0700 Subject: [PATCH 24/43] Implemented requested pull request changes --- builtin/logical/totp/backend_test.go | 18 ++++++------ builtin/logical/totp/path_role_create.go | 6 ++-- builtin/logical/totp/path_roles.go | 37 +++++++++--------------- 3 files changed, 27 insertions(+), 34 deletions(-) diff --git a/builtin/logical/totp/backend_test.go b/builtin/logical/totp/backend_test.go index 3972a65ca992..4c6100113f06 100644 --- a/builtin/logical/totp/backend_test.go +++ b/builtin/logical/totp/backend_test.go @@ -392,7 +392,7 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, na Path: path.Join("creds", name), Check: func(resp *logical.Response) error { var d struct { - Token string `mapstructure:"token"` + Code string `mapstructure:"code"` } if err := mapstructure.Decode(resp.Data, &d); err != nil { return err @@ -423,7 +423,7 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, na period := validation["period"].(int) key := validation["key"].(string) - valid, _ := totplib.ValidateCustom(d.Token, key, time.Now().UTC(), totplib.ValidateOpts{ + valid, _ := totplib.ValidateCustom(d.Code, key, time.Now().UTC(), totplib.ValidateOpts{ Period: uint(period), Skew: 1, Digits: digits, @@ -431,7 +431,7 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, na }) if !valid { - t.Fatalf("Generated token isn't valid.") + t.Fatalf("Generated code isn't valid.") } return nil @@ -452,11 +452,11 @@ func testAccStepReadRole(t *testing.T, name string, expected map[string]interfac } var d struct { - Issuer string `mapstructure:"issuer"` - Account_Name string `mapstructure:"account_name"` - Period int `mapstructure:"period"` - Algorithm string `mapstructure:"algorithm"` - Digits int `mapstructure:"digits"` + Issuer string `mapstructure:"issuer"` + AccountName string `mapstructure:"account_name"` + Period int `mapstructure:"period"` + Algorithm string `mapstructure:"algorithm"` + Digits int `mapstructure:"digits"` } if err := mapstructure.Decode(resp.Data, &d); err != nil { @@ -466,7 +466,7 @@ func testAccStepReadRole(t *testing.T, name string, expected map[string]interfac switch { case d.Issuer != expected["issuer"]: return fmt.Errorf("Issuer should equal: %s", expected["issuer"]) - case d.Account_Name != expected["account_name"]: + case d.AccountName != expected["account_name"]: return fmt.Errorf("Account_Name should equal: %s", expected["account_name"]) case d.Period != expected["period"]: return fmt.Errorf("Period should equal: %i", expected["period"]) diff --git a/builtin/logical/totp/path_role_create.go b/builtin/logical/totp/path_role_create.go index 867606bc6f13..52cb56aca0fc 100644 --- a/builtin/logical/totp/path_role_create.go +++ b/builtin/logical/totp/path_role_create.go @@ -53,6 +53,8 @@ func (b *backend) pathRoleCreateRead( digits = otplib.DigitsSix case 8: digits = otplib.DigitsEight + default: + return logical.ErrorResponse("The digit value can only be 6 or 8."), nil } var algorithm otplib.Algorithm @@ -64,7 +66,7 @@ func (b *backend) pathRoleCreateRead( case "SHA512": algorithm = otplib.AlgorithmSHA512 default: - algorithm = otplib.AlgorithmSHA1 + return logical.ErrorResponse("The algorithm value is not valid."), nil } period := uint(role.Period) @@ -85,7 +87,7 @@ func (b *backend) pathRoleCreateRead( resp, err := &logical.Response{ Data: map[string]interface{}{ - "token": totpToken, + "code": totpToken, }, }, nil diff --git a/builtin/logical/totp/path_roles.go b/builtin/logical/totp/path_roles.go index be4a006c8c42..32614c7fe36c 100644 --- a/builtin/logical/totp/path_roles.go +++ b/builtin/logical/totp/path_roles.go @@ -116,7 +116,7 @@ func (b *backend) pathRoleRead( return &logical.Response{ Data: map[string]interface{}{ "issuer": role.Issuer, - "account_name": role.Account_Name, + "account_name": role.AccountName, "period": role.Period, "algorithm": role.Algorithm, "digits": role.Digits, @@ -156,39 +156,30 @@ func (b *backend) pathRoleCreate( "Invalid key value: %s", err)), nil } - if period < 0 { + if period <= 0 { return logical.ErrorResponse("The period value must be greater than zero."), nil } - // Set optional parameters if neccessary - if period == 0 { - period = 30 - } - switch algorithm { case "SHA1", "SHA256", "SHA512": - case "": - algorithm = "SHA1" default: return logical.ErrorResponse("The algorithm value is not valid."), nil } switch digits { case 6, 8: - case 0: - digits = 6 default: return logical.ErrorResponse("The digit value can only be 6 or 8."), nil } // Store it entry, err := logical.StorageEntryJSON("role/"+name, &roleEntry{ - Key: key, - Issuer: issuer, - Account_Name: account_name, - Period: period, - Algorithm: algorithm, - Digits: digits, + Key: key, + Issuer: issuer, + AccountName: account_name, + Period: period, + Algorithm: algorithm, + Digits: digits, }) if err != nil { return nil, err @@ -201,12 +192,12 @@ func (b *backend) pathRoleCreate( } type roleEntry struct { - Key string `json:"key" mapstructure:"key" structs:"key"` - Issuer string `json:"issuer" mapstructure:"issuer" structs:"issuer"` - Account_Name string `json:"account_name" mapstructure:"account_name" structs:"account_name"` - Period int `json:"period" mapstructure:"period" structs:"period"` - Algorithm string `json:"algorithm" mapstructure:"algorithm" structs:"algorithm"` - Digits int `json:"digits" mapstructure:"digits" structs:"digits"` + Key string `json:"key" mapstructure:"key" structs:"key"` + Issuer string `json:"issuer" mapstructure:"issuer" structs:"issuer"` + AccountName string `json:"account_name" mapstructure:"account_name" structs:"account_name"` + Period int `json:"period" mapstructure:"period" structs:"period"` + Algorithm string `json:"algorithm" mapstructure:"algorithm" structs:"algorithm"` + Digits int `json:"digits" mapstructure:"digits" structs:"digits"` } const pathRoleHelpSyn = ` From dcdc86baed9a4c01f18f0c6845fe02cd400b5f46 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Mon, 3 Apr 2017 12:56:24 -0700 Subject: [PATCH 25/43] Added ability to validate TOTP codes --- builtin/logical/totp/backend_test.go | 94 ++++++++++-------------- builtin/logical/totp/path_role_create.go | 92 +++++++++++++---------- builtin/logical/totp/path_roles.go | 59 +++++++++------ 3 files changed, 128 insertions(+), 117 deletions(-) diff --git a/builtin/logical/totp/backend_test.go b/builtin/logical/totp/backend_test.go index 4c6100113f06..d85403060de1 100644 --- a/builtin/logical/totp/backend_test.go +++ b/builtin/logical/totp/backend_test.go @@ -43,9 +43,9 @@ func TestBackend_readCredentialsDefaultValues(t *testing.T) { expected := map[string]interface{}{ "issuer": "", "account_name": "", - "digits": 6, + "digits": otplib.DigitsSix, "period": 30, - "algorithm": "SHA1", + "algorithm": otplib.AlgorithmSHA1, "key": key, } @@ -80,9 +80,9 @@ func TestBackend_readCredentialsEightDigitsThirtySecondPeriod(t *testing.T) { expected := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", - "digits": 8, + "digits": otplib.DigitsEight, "period": 30, - "algorithm": "SHA1", + "algorithm": otplib.AlgorithmSHA1, "key": key, } @@ -117,9 +117,9 @@ func TestBackend_readCredentialsSixDigitsNinetySecondPeriod(t *testing.T) { expected := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", - "digits": 6, + "digits": otplib.DigitsSix, "period": 90, - "algorithm": "SHA1", + "algorithm": otplib.AlgorithmSHA1, "key": key, } @@ -154,9 +154,9 @@ func TestBackend_readCredentialsSHA256(t *testing.T) { expected := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", - "digits": 6, + "digits": otplib.DigitsSix, "period": 30, - "algorithm": "SHA256", + "algorithm": otplib.AlgorithmSHA256, "key": key, } @@ -191,9 +191,9 @@ func TestBackend_readCredentialsSHA512(t *testing.T) { expected := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", - "digits": 6, + "digits": otplib.DigitsSix, "period": 30, - "algorithm": "SHA512", + "algorithm": otplib.AlgorithmSHA512, "key": key, } @@ -226,9 +226,9 @@ func TestBackend_roleCrudDefaultValues(t *testing.T) { expected := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", - "digits": 6, + "digits": otplib.DigitsSix, "period": 30, - "algorithm": "SHA1", + "algorithm": otplib.AlgorithmSHA1, "key": key, } @@ -243,7 +243,7 @@ func TestBackend_roleCrudDefaultValues(t *testing.T) { }) } -func TestBackend_createRoleInvalidPeriod(t *testing.T) { +func TestBackend_createRoleMissingKey(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(config) @@ -251,15 +251,9 @@ func TestBackend_createRoleInvalidPeriod(t *testing.T) { t.Fatal(err) } - // Generate a new shared key - key, _ := createKey() - roleData := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", - "key": key, - "algorithm": "SHA256", - "period": -50, } logicaltest.Test(t, logicaltest.TestCase{ @@ -271,7 +265,7 @@ func TestBackend_createRoleInvalidPeriod(t *testing.T) { }) } -func TestBackend_createRoleMissingKey(t *testing.T) { +func TestBackend_createRoleInvalidKey(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(config) @@ -282,6 +276,7 @@ func TestBackend_createRoleMissingKey(t *testing.T) { roleData := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", + "key": "1", } logicaltest.Test(t, logicaltest.TestCase{ @@ -293,7 +288,7 @@ func TestBackend_createRoleMissingKey(t *testing.T) { }) } -func TestBackend_createRoleInvalidKey(t *testing.T) { +func TestBackend_createRoleInvalidAlgorithm(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(config) @@ -301,10 +296,14 @@ func TestBackend_createRoleInvalidKey(t *testing.T) { t.Fatal(err) } + // Generate a new shared key + key, _ := createKey() + roleData := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", - "key": "1", + "key": key, + "algorithm": "BADALGORITHM", } logicaltest.Test(t, logicaltest.TestCase{ @@ -316,7 +315,7 @@ func TestBackend_createRoleInvalidKey(t *testing.T) { }) } -func TestBackend_createRoleInvalidAlgorithm(t *testing.T) { +func TestBackend_createRoleInvalidPeriod(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(config) @@ -331,7 +330,7 @@ func TestBackend_createRoleInvalidAlgorithm(t *testing.T) { "issuer": "Vault", "account_name": "Test", "key": key, - "algorithm": "BADALGORITHM", + "period": -1, } logicaltest.Test(t, logicaltest.TestCase{ @@ -373,7 +372,7 @@ func TestBackend_createRoleInvalidDigits(t *testing.T) { func testAccStepCreateRole(t *testing.T, name string, roleData map[string]interface{}, expectFail bool) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, - Path: path.Join("roles", name), + Path: path.Join("keys", name), Data: roleData, ErrorOk: expectFail, } @@ -382,14 +381,14 @@ func testAccStepCreateRole(t *testing.T, name string, roleData map[string]interf func testAccStepDeleteRole(t *testing.T, name string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.DeleteOperation, - Path: path.Join("roles", name), + Path: path.Join("keys", name), } } func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, name string, validation map[string]interface{}) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.ReadOperation, - Path: path.Join("creds", name), + Path: path.Join("code", name), Check: func(resp *logical.Response) error { var d struct { Code string `mapstructure:"code"` @@ -399,31 +398,12 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, na } log.Printf("[TRACE] Generated credentials: %v", d) - // Translate digits and algorithm to a format the totp library understands - var digits otplib.Digits - switch validation["digits"] { - case 6: - digits = otplib.DigitsSix - case 8: - digits = otplib.DigitsEight - } - - var algorithm otplib.Algorithm - switch validation["algorithm"] { - case "SHA1": - algorithm = otplib.AlgorithmSHA1 - case "SHA256": - algorithm = otplib.AlgorithmSHA256 - case "SHA512": - algorithm = otplib.AlgorithmSHA512 - default: - algorithm = otplib.AlgorithmSHA1 - } - period := validation["period"].(int) key := validation["key"].(string) + algorithm := validation["algorithm"].(otplib.Algorithm) + digits := validation["digits"].(otplib.Digits) - valid, _ := totplib.ValidateCustom(d.Code, key, time.Now().UTC(), totplib.ValidateOpts{ + valid, _ := totplib.ValidateCustom(d.Code, key, time.Now(), totplib.ValidateOpts{ Period: uint(period), Skew: 1, Digits: digits, @@ -442,7 +422,7 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, na func testAccStepReadRole(t *testing.T, name string, expected map[string]interface{}) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.ReadOperation, - Path: "roles/" + name, + Path: "keys/" + name, Check: func(resp *logical.Response) error { if resp == nil { if expected == nil { @@ -452,23 +432,25 @@ func testAccStepReadRole(t *testing.T, name string, expected map[string]interfac } var d struct { - Issuer string `mapstructure:"issuer"` - AccountName string `mapstructure:"account_name"` - Period int `mapstructure:"period"` - Algorithm string `mapstructure:"algorithm"` - Digits int `mapstructure:"digits"` + Issuer string `mapstructure:"issuer"` + AccountName string `mapstructure:"account_name"` + Period uint `mapstructure:"period"` + Algorithm otplib.Algorithm `mapstructure:"algorithm"` + Digits otplib.Digits `mapstructure:"digits"` } if err := mapstructure.Decode(resp.Data, &d); err != nil { return err } + period := expected["period"].(int) + switch { case d.Issuer != expected["issuer"]: return fmt.Errorf("Issuer should equal: %s", expected["issuer"]) case d.AccountName != expected["account_name"]: return fmt.Errorf("Account_Name should equal: %s", expected["account_name"]) - case d.Period != expected["period"]: + case d.Period != uint(period): return fmt.Errorf("Period should equal: %i", expected["period"]) case d.Algorithm != expected["algorithm"]: return fmt.Errorf("Algorithm should equal: %s", expected["algorithm"]) diff --git a/builtin/logical/totp/path_role_create.go b/builtin/logical/totp/path_role_create.go index 52cb56aca0fc..d0d5bc98e488 100644 --- a/builtin/logical/totp/path_role_create.go +++ b/builtin/logical/totp/path_role_create.go @@ -6,22 +6,26 @@ import ( "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" - otplib "github.com/pquerna/otp" totplib "github.com/pquerna/otp/totp" ) func pathRoleCreate(b *backend) *framework.Path { return &framework.Path{ - Pattern: "creds/" + framework.GenericNameRegex("name"), + Pattern: "code/" + framework.GenericNameRegex("name"), Fields: map[string]*framework.FieldSchema{ "name": &framework.FieldSchema{ Type: framework.TypeString, Description: "Name of the role.", }, + "code": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "TOTP code to be validated.", + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.pathRoleCreateRead, + logical.ReadOperation: b.pathReadCode, + logical.UpdateOperation: b.pathValidateCode, }, HelpSynopsis: pathRoleCreateReadHelpSyn, @@ -29,53 +33,28 @@ func pathRoleCreate(b *backend) *framework.Path { } } -func (b *backend) pathRoleCreateRead( +func (b *backend) pathReadCode( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - b.logger.Trace("totp/pathRoleCreateRead: enter") - defer b.logger.Trace("totp/pathRoleCreateRead: exit") + b.logger.Trace("totp/pathReadCode: enter") + defer b.logger.Trace("totp/pathReadCode: exit") name := data.Get("name").(string) - // Get the role - b.logger.Trace("totp/pathRoleCreateRead: getting role") + // Get the key + b.logger.Trace("totp/pathReadCode: getting key") role, err := b.Role(req.Storage, name) if err != nil { return nil, err } if role == nil { - return logical.ErrorResponse(fmt.Sprintf("unknown role: %s", name)), nil - } - - // Translate digits and algorithm to a format the totp library understands - var digits otplib.Digits - switch role.Digits { - case 6: - digits = otplib.DigitsSix - case 8: - digits = otplib.DigitsEight - default: - return logical.ErrorResponse("The digit value can only be 6 or 8."), nil + return logical.ErrorResponse(fmt.Sprintf("unknown key: %s", name)), nil } - var algorithm otplib.Algorithm - switch role.Algorithm { - case "SHA1": - algorithm = otplib.AlgorithmSHA1 - case "SHA256": - algorithm = otplib.AlgorithmSHA256 - case "SHA512": - algorithm = otplib.AlgorithmSHA512 - default: - return logical.ErrorResponse("The algorithm value is not valid."), nil - } - - period := uint(role.Period) - // Generate password using totp library - totpToken, err := totplib.GenerateCodeCustom(role.Key, time.Now().UTC(), totplib.ValidateOpts{ - Period: period, - Digits: digits, - Algorithm: algorithm, + totpToken, err := totplib.GenerateCodeCustom(role.Key, time.Now(), totplib.ValidateOpts{ + Period: role.Period, + Digits: role.Digits, + Algorithm: role.Algorithm, }) if err != nil { @@ -83,7 +62,7 @@ func (b *backend) pathRoleCreateRead( } // Return the secret - b.logger.Trace("totp/pathRoleCreateRead: generating secret") + b.logger.Trace("totp/pathReadCode: generating secret") resp, err := &logical.Response{ Data: map[string]interface{}{ @@ -94,6 +73,41 @@ func (b *backend) pathRoleCreateRead( return resp, nil } +func (b *backend) pathValidateCode( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + name := data.Get("name").(string) + code := data.Get("code").(string) + + // Enforce input value requirements + if code == "" { + return logical.ErrorResponse("The code value is required."), nil + } + + // Get the key's stored values + role, err := b.Role(req.Storage, name) + if err != nil { + return nil, err + } + if role == nil { + return logical.ErrorResponse(fmt.Sprintf("unknown key: %s", name)), nil + } + + valid, err := totplib.ValidateCustom(code, role.Key, time.Now(), totplib.ValidateOpts{ + Period: role.Period, + Skew: 1, + Digits: role.Digits, + Algorithm: role.Algorithm, + }) + + resp, err := &logical.Response{ + Data: map[string]interface{}{ + "code": valid, + }, + }, nil + + return resp, nil +} + const pathRoleCreateReadHelpSyn = ` Request time-based one-time use password for a certain role. ` diff --git a/builtin/logical/totp/path_roles.go b/builtin/logical/totp/path_roles.go index 32614c7fe36c..b27ea10ed8b8 100644 --- a/builtin/logical/totp/path_roles.go +++ b/builtin/logical/totp/path_roles.go @@ -6,11 +6,12 @@ import ( "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" + otplib "github.com/pquerna/otp" ) func pathListRoles(b *backend) *framework.Path { return &framework.Path{ - Pattern: "roles/?$", + Pattern: "keys/?$", Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ListOperation: b.pathRoleList, @@ -23,7 +24,7 @@ func pathListRoles(b *backend) *framework.Path { func pathRoles(b *backend) *framework.Path { return &framework.Path{ - Pattern: "roles/" + framework.GenericNameRegex("name"), + Pattern: "keys/" + framework.GenericNameRegex("name"), Fields: map[string]*framework.FieldSchema{ "name": { Type: framework.TypeString, @@ -76,7 +77,7 @@ func pathRoles(b *backend) *framework.Path { } func (b *backend) Role(s logical.Storage, n string) (*roleEntry, error) { - entry, err := s.Get("role/" + n) + entry, err := s.Get("key/" + n) if err != nil { return nil, err } @@ -94,7 +95,7 @@ func (b *backend) Role(s logical.Storage, n string) (*roleEntry, error) { func (b *backend) pathRoleDelete( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - err := req.Storage.Delete("role/" + data.Get("name").(string)) + err := req.Storage.Delete("key/" + data.Get("name").(string)) if err != nil { return nil, err } @@ -112,7 +113,7 @@ func (b *backend) pathRoleRead( return nil, nil } - // Return values of role + // Return values of key return &logical.Response{ Data: map[string]interface{}{ "issuer": role.Issuer, @@ -126,7 +127,7 @@ func (b *backend) pathRoleRead( func (b *backend) pathRoleList( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - entries, err := req.Storage.List("role/") + entries, err := req.Storage.List("key/") if err != nil { return nil, err } @@ -160,26 +161,40 @@ func (b *backend) pathRoleCreate( return logical.ErrorResponse("The period value must be greater than zero."), nil } - switch algorithm { - case "SHA1", "SHA256", "SHA512": + // Translate digits and algorithm to a format the totp library understands + var role_digits otplib.Digits + switch digits { + case 6: + role_digits = otplib.DigitsSix + case 8: + role_digits = otplib.DigitsEight default: - return logical.ErrorResponse("The algorithm value is not valid."), nil + return logical.ErrorResponse("The digit value can only be 6 or 8."), nil } - switch digits { - case 6, 8: + var role_algorithm otplib.Algorithm + switch algorithm { + case "SHA1": + role_algorithm = otplib.AlgorithmSHA1 + case "SHA256": + role_algorithm = otplib.AlgorithmSHA256 + case "SHA512": + role_algorithm = otplib.AlgorithmSHA512 default: - return logical.ErrorResponse("The digit value can only be 6 or 8."), nil + return logical.ErrorResponse("The algorithm value is not valid."), nil } + // Period needs to be an unsigned int + uint_period := uint(period) + // Store it - entry, err := logical.StorageEntryJSON("role/"+name, &roleEntry{ + entry, err := logical.StorageEntryJSON("key/"+name, &roleEntry{ Key: key, Issuer: issuer, AccountName: account_name, - Period: period, - Algorithm: algorithm, - Digits: digits, + Period: uint_period, + Algorithm: role_algorithm, + Digits: role_digits, }) if err != nil { return nil, err @@ -192,12 +207,12 @@ func (b *backend) pathRoleCreate( } type roleEntry struct { - Key string `json:"key" mapstructure:"key" structs:"key"` - Issuer string `json:"issuer" mapstructure:"issuer" structs:"issuer"` - AccountName string `json:"account_name" mapstructure:"account_name" structs:"account_name"` - Period int `json:"period" mapstructure:"period" structs:"period"` - Algorithm string `json:"algorithm" mapstructure:"algorithm" structs:"algorithm"` - Digits int `json:"digits" mapstructure:"digits" structs:"digits"` + Key string `json:"key" mapstructure:"key" structs:"key"` + Issuer string `json:"issuer" mapstructure:"issuer" structs:"issuer"` + AccountName string `json:"account_name" mapstructure:"account_name" structs:"account_name"` + Period uint `json:"period" mapstructure:"period" structs:"period"` + Algorithm otplib.Algorithm `json:"algorithm" mapstructure:"algorithm" structs:"algorithm"` + Digits otplib.Digits `json:"digits" mapstructure:"digits" structs:"digits"` } const pathRoleHelpSyn = ` From 748e51d3a8182bca29afdd9eb4a8b066b5a57d73 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Tue, 4 Apr 2017 13:01:22 -0700 Subject: [PATCH 26/43] Added ability to have a key generated --- builtin/logical/totp/path_roles.go | 62 ++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/builtin/logical/totp/path_roles.go b/builtin/logical/totp/path_roles.go index b27ea10ed8b8..865dd4dfcec9 100644 --- a/builtin/logical/totp/path_roles.go +++ b/builtin/logical/totp/path_roles.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" otplib "github.com/pquerna/otp" + totplib "github.com/pquerna/otp/totp" ) func pathListRoles(b *backend) *framework.Path { @@ -31,6 +32,12 @@ func pathRoles(b *backend) *framework.Path { Description: "Name of the role.", }, + "create": { + Type: framework.TypeBool, + Default: false, + Description: "Determines if a key is generated by vault or another service.", + }, + "key": { Type: framework.TypeString, Description: "The shared master key used to generate a TOTP token.", @@ -113,13 +120,16 @@ func (b *backend) pathRoleRead( return nil, nil } + // Translate algorithm back to string + algorithm := role.Algorithm.String() + // Return values of key return &logical.Response{ Data: map[string]interface{}{ "issuer": role.Issuer, "account_name": role.AccountName, "period": role.Period, - "algorithm": role.Algorithm, + "algorithm": algorithm, "digits": role.Digits, }, }, nil @@ -138,6 +148,7 @@ func (b *backend) pathRoleList( func (b *backend) pathRoleCreate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { name := data.Get("name").(string) + create := data.Get("create").(bool) key := data.Get("key").(string) issuer := data.Get("issuer").(string) account_name := data.Get("account_name").(string) @@ -145,22 +156,6 @@ func (b *backend) pathRoleCreate( algorithm := data.Get("algorithm").(string) digits := data.Get("digits").(int) - // Enforce input value requirements - if key == "" { - return logical.ErrorResponse("The key value is required."), nil - } - - _, err := base32.StdEncoding.DecodeString(key) - - if err != nil { - return logical.ErrorResponse(fmt.Sprintf( - "Invalid key value: %s", err)), nil - } - - if period <= 0 { - return logical.ErrorResponse("The period value must be greater than zero."), nil - } - // Translate digits and algorithm to a format the totp library understands var role_digits otplib.Digits switch digits { @@ -184,9 +179,42 @@ func (b *backend) pathRoleCreate( return logical.ErrorResponse("The algorithm value is not valid."), nil } + // Enforce input value requirements + if period <= 0 { + return logical.ErrorResponse("The period value must be greater than zero."), nil + } + // Period needs to be an unsigned int uint_period := uint(period) + if create { + // Generate a new key + keyUrl, err := totplib.Generate(totplib.GenerateOpts{ + Issuer: issuer, + AccountName: account_name, + Period: uint_period, + Digits: role_digits, + Algorithm: role_algorithm, + }) + + if err != nil { + return logical.ErrorResponse("An error occured while generating a key."), nil + } + + key = keyUrl.Secret() + } else { + if key == "" { + return logical.ErrorResponse("The key value is required."), nil + } + + _, err := base32.StdEncoding.DecodeString(key) + + if err != nil { + return logical.ErrorResponse(fmt.Sprintf( + "Invalid key value: %s", err)), nil + } + } + // Store it entry, err := logical.StorageEntryJSON("key/"+name, &roleEntry{ Key: key, From 4d94de0c9c0bd2d0ff091f0dea6fdf3edad06720 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sun, 9 Apr 2017 19:04:15 -0700 Subject: [PATCH 27/43] Added skew, qr size and key size parameters --- builtin/logical/totp/path_role_create.go | 4 +- builtin/logical/totp/path_roles.go | 135 +++++++++++++++++++---- 2 files changed, 114 insertions(+), 25 deletions(-) diff --git a/builtin/logical/totp/path_role_create.go b/builtin/logical/totp/path_role_create.go index d0d5bc98e488..4ee65c2c0b18 100644 --- a/builtin/logical/totp/path_role_create.go +++ b/builtin/logical/totp/path_role_create.go @@ -94,14 +94,14 @@ func (b *backend) pathValidateCode( valid, err := totplib.ValidateCustom(code, role.Key, time.Now(), totplib.ValidateOpts{ Period: role.Period, - Skew: 1, + Skew: role.Skew, Digits: role.Digits, Algorithm: role.Algorithm, }) resp, err := &logical.Response{ Data: map[string]interface{}{ - "code": valid, + "valid": valid, }, }, nil diff --git a/builtin/logical/totp/path_roles.go b/builtin/logical/totp/path_roles.go index 865dd4dfcec9..9374d1d089c4 100644 --- a/builtin/logical/totp/path_roles.go +++ b/builtin/logical/totp/path_roles.go @@ -1,8 +1,11 @@ package totp import ( + "bytes" "encoding/base32" + "encoding/base64" "fmt" + "image/png" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" @@ -32,12 +35,18 @@ func pathRoles(b *backend) *framework.Path { Description: "Name of the role.", }, - "create": { + "generate": { Type: framework.TypeBool, Default: false, Description: "Determines if a key is generated by vault or another service.", }, + "key_size": { + Type: framework.TypeInt, + Default: 20, + Description: "Determines the size in bytes of the generated key.", + }, + "key": { Type: framework.TypeString, Description: "The shared master key used to generate a TOTP token.", @@ -70,6 +79,18 @@ func pathRoles(b *backend) *framework.Path { Default: 6, Description: `The number of digits in the generated TOTP token.`, }, + + "skew": { + Type: framework.TypeInt, + Default: 0, + Description: `The number of delay periods that are allowed when validating a TOTP token.`, + }, + + "qr_size": { + Type: framework.TypeInt, + Default: 200, + Description: `The pixel size of the generated square QR code.`, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -120,19 +141,46 @@ func (b *backend) pathRoleRead( return nil, nil } - // Translate algorithm back to string - algorithm := role.Algorithm.String() + switch role.Generate { + case true: + key_object, err := otplib.NewKeyFromURL(role.URL) - // Return values of key - return &logical.Response{ - Data: map[string]interface{}{ - "issuer": role.Issuer, - "account_name": role.AccountName, - "period": role.Period, - "algorithm": algorithm, - "digits": role.Digits, - }, - }, nil + if err != nil { + return logical.ErrorResponse("An error occured while generating a Key object."), nil + } + + barcode, err := key_object.Image(role.QRSize, role.QRSize) + + if err != nil { + return logical.ErrorResponse("An error occured while generating a QR code image."), nil + } + + var buff bytes.Buffer + png.Encode(&buff, barcode) + b64Barcode := base64.StdEncoding.EncodeToString(buff.Bytes()) + return &logical.Response{ + Data: map[string]interface{}{ + "url": key_object.String(), + "barcode": b64Barcode, + "skew": role.Skew, + "qr_size": role.QRSize, + }, + }, nil + default: + // Translate algorithm back to string + algorithm := role.Algorithm.String() + + // Return values of key + return &logical.Response{ + Data: map[string]interface{}{ + "issuer": role.Issuer, + "account_name": role.AccountName, + "period": role.Period, + "algorithm": algorithm, + "digits": role.Digits, + }, + }, nil + } } func (b *backend) pathRoleList( @@ -148,13 +196,16 @@ func (b *backend) pathRoleList( func (b *backend) pathRoleCreate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { name := data.Get("name").(string) - create := data.Get("create").(bool) - key := data.Get("key").(string) + generate := data.Get("generate").(bool) + key_string := data.Get("key").(string) issuer := data.Get("issuer").(string) account_name := data.Get("account_name").(string) period := data.Get("period").(int) algorithm := data.Get("algorithm").(string) digits := data.Get("digits").(int) + skew := data.Get("skew").(int) + qr_size := data.Get("qr_size").(int) + key_size := data.Get("key_size").(int) // Translate digits and algorithm to a format the totp library understands var role_digits otplib.Digits @@ -184,30 +235,60 @@ func (b *backend) pathRoleCreate( return logical.ErrorResponse("The period value must be greater than zero."), nil } - // Period needs to be an unsigned int + if skew < 0 { + return logical.ErrorResponse("The skew value must be greater than zero."), nil + } + + if qr_size <= 0 { + return logical.ErrorResponse("The qr_size value must be greater than zero."), nil + } + + if key_size <= 0 { + return logical.ErrorResponse("The key_size value must be greater than zero."), nil + } + + // If the key is generated, Account Name and Issuer are required. + if generate { + if account_name == "" { + return logical.ErrorResponse("The account_name value is required for generated keys."), nil + } + + if issuer == "" { + return logical.ErrorResponse("The issuer value is required for generated keys."), nil + } + } + + // Period, Skew and Key Size need to be unsigned ints uint_period := uint(period) + uint_skew := uint(skew) + uint_key_size := uint(key_size) + + url := "" - if create { + switch generate { + case true: // Generate a new key - keyUrl, err := totplib.Generate(totplib.GenerateOpts{ + key_object, err := totplib.Generate(totplib.GenerateOpts{ Issuer: issuer, AccountName: account_name, Period: uint_period, Digits: role_digits, Algorithm: role_algorithm, + SecretSize: uint_key_size, }) if err != nil { return logical.ErrorResponse("An error occured while generating a key."), nil } - key = keyUrl.Secret() - } else { - if key == "" { + url = key_object.String() + key_string = key_object.Secret() + case false: + if key_string == "" { return logical.ErrorResponse("The key value is required."), nil } - _, err := base32.StdEncoding.DecodeString(key) + _, err := base32.StdEncoding.DecodeString(key_string) if err != nil { return logical.ErrorResponse(fmt.Sprintf( @@ -217,12 +298,16 @@ func (b *backend) pathRoleCreate( // Store it entry, err := logical.StorageEntryJSON("key/"+name, &roleEntry{ - Key: key, + Key: key_string, Issuer: issuer, AccountName: account_name, Period: uint_period, Algorithm: role_algorithm, Digits: role_digits, + Skew: uint_skew, + QRSize: qr_size, + URL: url, + Generate: generate, }) if err != nil { return nil, err @@ -241,6 +326,10 @@ type roleEntry struct { Period uint `json:"period" mapstructure:"period" structs:"period"` Algorithm otplib.Algorithm `json:"algorithm" mapstructure:"algorithm" structs:"algorithm"` Digits otplib.Digits `json:"digits" mapstructure:"digits" structs:"digits"` + Skew uint `json:"skew" mapstructure:"skew" structs:"skew"` + QRSize int `json:"qr_size" mapstructure:"qr_size" structs:"qr_size"` + URL string `json:"url" mapstructure:"url" structs:"url"` + Generate bool `json:"generate" mapstructure:"generate" structs:"generate"` } const pathRoleHelpSyn = ` From dda68be8b4c4501620f43bce7e17e262a80be846 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Wed, 12 Apr 2017 12:19:49 -0700 Subject: [PATCH 28/43] Reset vendor.json prior to merge --- vendor/vendor.json | 1013 +++++++++++++++++++++++--------------------- 1 file changed, 521 insertions(+), 492 deletions(-) diff --git a/vendor/vendor.json b/vendor/vendor.json index 712557e84b2c..324af66c5f7c 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -21,32 +21,32 @@ { "checksumSHA1": "ZLRh6zW4/DnVsGpgtt+ZiIaEFKc=", "path": "cloud.google.com/go/compute/metadata", - "revision": "9b68cf4865e93f379a337b7e957f33de60397a48", - "revisionTime": "2017-02-23T18:50:28Z" + "revision": "7751b7da830fbc9965347aab707e7721c9e877fa", + "revisionTime": "2017-02-25T21:39:49Z" }, { - "checksumSHA1": "4iounbuF7SMZdx/MlKSUuhnV848=", + "checksumSHA1": "Y3Ps0pphldAXTIX/D243xWrRP+A=", "path": "cloud.google.com/go/internal", - "revision": "9b68cf4865e93f379a337b7e957f33de60397a48", - "revisionTime": "2017-02-23T18:50:28Z" + "revision": "7751b7da830fbc9965347aab707e7721c9e877fa", + "revisionTime": "2017-02-25T21:39:49Z" }, { "checksumSHA1": "W2xJ0+fvugRhRi1PMi64bYofBbU=", "path": "cloud.google.com/go/internal/optional", - "revision": "9b68cf4865e93f379a337b7e957f33de60397a48", - "revisionTime": "2017-02-23T18:50:28Z" + "revision": "7751b7da830fbc9965347aab707e7721c9e877fa", + "revisionTime": "2017-02-25T21:39:49Z" }, { - "checksumSHA1": "MJSEDjd8OT7lz9wIemq8zz7R4ok=", + "checksumSHA1": "nvLWIxiQNGILLoJJL4ZbIb0BOSY=", "path": "cloud.google.com/go/storage", - "revision": "9b68cf4865e93f379a337b7e957f33de60397a48", - "revisionTime": "2017-02-23T18:50:28Z" + "revision": "7751b7da830fbc9965347aab707e7721c9e877fa", + "revisionTime": "2017-02-25T21:39:49Z" }, { - "checksumSHA1": "rK3ght7KTtHGdm0V4+U7fv9+tUU=", + "checksumSHA1": "BTVNbDogrQcCq3C/5kgm9Cvdm3o=", "path": "github.com/Azure/azure-sdk-for-go/storage", - "revision": "8e625d1702a32d01cef05a9252198d231c4af113", - "revisionTime": "2017-02-08T01:01:20Z" + "revision": "4897648e310020dae650a89c31ff633284c13a24", + "revisionTime": "2017-03-02T18:56:14Z" }, { "checksumSHA1": "23FJUX+AInYeEM2hoUMvYZtXZd4=", @@ -61,10 +61,10 @@ "revisionTime": "2016-06-22T17:32:16Z" }, { - "checksumSHA1": "W/iDPqbPMS1ZDGpXVJIST55OAuY=", + "checksumSHA1": "l6kv4OCHCG6DAuPYi0H3aCfnKtQ=", "path": "github.com/Jeffail/gabs", - "revision": "ede33bf4edd022d9d8693603bb9af6c1cbadcfae", - "revisionTime": "2017-02-15T20:05:58Z" + "revision": "c9fb5b700cddb62013b2ee9dfc9ce011caea55e0", + "revisionTime": "2017-03-22T22:36:35Z" }, { "checksumSHA1": "L9njXCkN30+qHaLayiiA2Q9jDlY=", @@ -103,22 +103,10 @@ "revisionTime": "2016-12-05T22:51:55Z" }, { - "checksumSHA1": "XFl7nlkh1EUa3RvYEsT8Q4O1Mvc=", + "checksumSHA1": "ZKxETlJdB2XubMrZnXB0FQimVA8=", "path": "github.com/Sirupsen/logrus", - "revision": "7f4b1adc791766938c29457bed0703fb9134421a", - "revisionTime": "2017-02-15T16:43:24Z" - }, - { - "checksumSHA1": "LLVyR2dAgkihu0+HdZF+JK0gMMs=", - "path": "github.com/agl/ed25519", - "revision": "5312a61534124124185d41f09206b9fef1d88403", - "revisionTime": "2017-01-16T20:05:12Z" - }, - { - "checksumSHA1": "30PBqj9BW03KCVqASvLg3bR+xYc=", - "path": "github.com/agl/ed25519/edwards25519", - "revision": "5312a61534124124185d41f09206b9fef1d88403", - "revisionTime": "2017-01-16T20:05:12Z" + "revision": "10f801ebc38b33738c9d17d50860f484a0988ff5", + "revisionTime": "2017-03-17T14:32:14Z" }, { "checksumSHA1": "YfhpW3cu1CHWX7lUCRparOJ6Vy4=", @@ -139,202 +127,196 @@ "revisionTime": "2016-01-15T23:47:25Z" }, { - "checksumSHA1": "dPI35hOM4TMrLugm6M1js14jHVQ=", + "checksumSHA1": "YL0j2l6a5wmMjbt8S8O8lwCIkys=", "path": "github.com/asaskevich/govalidator", - "revision": "fdf19785fd3558d619ef81212f5edf1d6c2a5911", - "revisionTime": "2017-01-04T21:11:26Z" + "revision": "872bb01704d183fe276bce6fa429408a87661998", + "revisionTime": "2017-03-20T17:24:29Z" }, { - "checksumSHA1": "ixQyUx38QwxxAA8VOYDXB9vlIuA=", + "checksumSHA1": "qrnuuXh2xcSiGlDa1k7fi75EbY8=", "path": "github.com/aws/aws-sdk-go/aws", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { "checksumSHA1": "Y9W+4GimK4Fuxq+vyIskVYFRnX4=", "path": "github.com/aws/aws-sdk-go/aws/awserr", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { "checksumSHA1": "yyYr41HZ1Aq0hWc3J5ijXwYEcac=", "path": "github.com/aws/aws-sdk-go/aws/awsutil", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { "checksumSHA1": "iThCyNRL/oQFD9CF2SYgBGl+aww=", "path": "github.com/aws/aws-sdk-go/aws/client", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { "checksumSHA1": "ieAJ+Cvp/PKv1LpUEnUXpc3OI6E=", "path": "github.com/aws/aws-sdk-go/aws/client/metadata", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { - "checksumSHA1": "Fl8vRSCY0MbM04cmiz/0MID+goA=", + "checksumSHA1": "0Gfk83qXYimO87ZoK1lL9+ifWHo=", "path": "github.com/aws/aws-sdk-go/aws/corehandlers", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { - "checksumSHA1": "zu5C95rmCZff6NYZb62lEaT5ibE=", + "checksumSHA1": "P7gt3PNk6bDOoTZ2N9QOonkaGWw=", "path": "github.com/aws/aws-sdk-go/aws/credentials", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { "checksumSHA1": "u3GOAJLmdvbuNUeUEcZSEAOeL/0=", "path": "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { "checksumSHA1": "NUJUTWlc1sV8b7WjfiYc4JZbXl0=", "path": "github.com/aws/aws-sdk-go/aws/credentials/endpointcreds", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { "checksumSHA1": "6cj/zsRmcxkE1TLS+v910GbQYg0=", "path": "github.com/aws/aws-sdk-go/aws/credentials/stscreds", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { - "checksumSHA1": "lqh3fG7wCochvB4iHAZJuhhEJW0=", + "checksumSHA1": "l2O7P/kvovK2zxKhuFehFNXLk+Q=", "path": "github.com/aws/aws-sdk-go/aws/defaults", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { "checksumSHA1": "/EXbk/z2TWjWc1Hvb4QYs3Wmhb8=", "path": "github.com/aws/aws-sdk-go/aws/ec2metadata", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { - "checksumSHA1": "JTrzEDPXL3pUUH+dMCixz9T9rLY=", + "checksumSHA1": "31f7CpCeRUQrUidzHj2uNwpBfCY=", "path": "github.com/aws/aws-sdk-go/aws/endpoints", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { - "checksumSHA1": "M78rTxU55Qagqr3MYj91im2031E=", + "checksumSHA1": "2NJ4UGzNlIlRoeeKJQC3LlajlOo=", "path": "github.com/aws/aws-sdk-go/aws/request", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { - "checksumSHA1": "u6tKvFGcRQ1xtby1ONjgyUTgcpg=", + "checksumSHA1": "5pzA5afgeU1alfACFh8z2CDUMao=", "path": "github.com/aws/aws-sdk-go/aws/session", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { - "checksumSHA1": "0FvPLvkBUpTElfUc/FZtPsJfuV0=", + "checksumSHA1": "SvIsunO8D9MEKbetMENA4WRnyeE=", "path": "github.com/aws/aws-sdk-go/aws/signer/v4", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { "checksumSHA1": "wk7EyvDaHwb5qqoOP/4d3cV0708=", "path": "github.com/aws/aws-sdk-go/private/protocol", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { "checksumSHA1": "1QmQ3FqV37w0Zi44qv8pA1GeR0A=", "path": "github.com/aws/aws-sdk-go/private/protocol/ec2query", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { "checksumSHA1": "O6hcK24yI6w7FA+g4Pbr+eQ7pys=", "path": "github.com/aws/aws-sdk-go/private/protocol/json/jsonutil", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { "checksumSHA1": "R00RL5jJXRYq1iiK1+PGvMfvXyM=", "path": "github.com/aws/aws-sdk-go/private/protocol/jsonrpc", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { "checksumSHA1": "ZqY5RWavBLWTo6j9xqdyBEaNFRk=", "path": "github.com/aws/aws-sdk-go/private/protocol/query", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { - "checksumSHA1": "hqTEmgtchF9SwVTW0IQId2eLUKM=", + "checksumSHA1": "Drt1JfLMa0DQEZLWrnMlTWaIcC8=", "path": "github.com/aws/aws-sdk-go/private/protocol/query/queryutil", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { - "checksumSHA1": "szZSLm3BlYkL3vqlZhNAlYk8iwM=", + "checksumSHA1": "VCTh+dEaqqhog5ncy/WTt9+/gFM=", "path": "github.com/aws/aws-sdk-go/private/protocol/rest", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { "checksumSHA1": "ODo+ko8D6unAxZuN1jGzMcN4QCc=", "path": "github.com/aws/aws-sdk-go/private/protocol/restxml", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { "checksumSHA1": "lZ1z4xAbT8euCzKoAsnEYic60VE=", "path": "github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" - }, - { - "checksumSHA1": "Eo9yODN5U99BK0pMzoqnBm7PCrY=", - "path": "github.com/aws/aws-sdk-go/private/waiter", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { - "checksumSHA1": "nz8TKu6v2dXW8kvHln2VguNmUuw=", + "checksumSHA1": "tLbkWFyWabb5nTjWK8TkebNleow=", "path": "github.com/aws/aws-sdk-go/service/dynamodb", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { - "checksumSHA1": "+Dp6gjxIwrUbWBdNhCUiq4cMZIM=", + "checksumSHA1": "uM9wwB9CYnMTeZrbOVEVjCVSXNQ=", "path": "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { - "checksumSHA1": "ecCVL8+SptmQlojrGtL8mQdaJ6E=", + "checksumSHA1": "iGQsCJG1uWs7c2gmI1+UBazpi1Q=", "path": "github.com/aws/aws-sdk-go/service/ec2", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { - "checksumSHA1": "vtaKEwBXDiWg000PcDBPbnOZEoY=", + "checksumSHA1": "j5RsXMJ6SEdU7CFpIU1bSXRK350=", "path": "github.com/aws/aws-sdk-go/service/iam", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { - "checksumSHA1": "eEWM4wKzVbRqAwIy3MdMCDUGs2s=", + "checksumSHA1": "PV//kD4iz+XElmZmoj2NQ8g2ZGo=", "path": "github.com/aws/aws-sdk-go/service/s3", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { - "checksumSHA1": "Knj17ZMPWkGYTm2hZxEgnuboMM4=", + "checksumSHA1": "o8O6Fn1ThDBMRb5OKU3UBziey+A=", "path": "github.com/aws/aws-sdk-go/service/sts", - "revision": "106ddd8e308f1f57a8907b191a4ea94db104b3da", - "revisionTime": "2017-02-24T04:30:59Z" + "revision": "5b6ffcee96d98de885687e3060545ae032bf313b", + "revisionTime": "2017-03-30T21:40:34Z" }, { "checksumSHA1": "Isa9x3nvIJ12hvgdvUUBty+yplU=", @@ -343,52 +325,34 @@ "revisionTime": "2016-10-13T10:26:35Z" }, { - "checksumSHA1": "tLl952GRIVsso2Pk/IH3cMJaK8E=", - "path": "github.com/boombuler/barcode", - "revision": "c07adc95a07cb6213cf4c104caab763bce2d49c0", - "revisionTime": "2017-01-31T17:48:17Z" - }, - { - "checksumSHA1": "tbwzn+sWiZv6veyXae3qRfTjlcQ=", - "path": "github.com/boombuler/barcode/qr", - "revision": "c07adc95a07cb6213cf4c104caab763bce2d49c0", - "revisionTime": "2017-01-31T17:48:17Z" - }, - { - "checksumSHA1": "axe0OTdOjYa+XKDUYqzOv7FGaWo=", - "path": "github.com/boombuler/barcode/utils", - "revision": "c07adc95a07cb6213cf4c104caab763bce2d49c0", - "revisionTime": "2017-01-31T17:48:17Z" - }, - { - "checksumSHA1": "m6QMy+u/IX6knzP28DqMn4sdLSw=", + "checksumSHA1": "gX06B03sIRw/1yCms1kMwKX8krE=", "path": "github.com/cenk/backoff", - "revision": "b02f2bbce11d7ea6b97f282ef1771b0fe2f65ef3", - "revisionTime": "2016-10-20T19:44:10Z" + "revision": "5d150e7eec023ce7a124856b37c68e54b4050ac7", + "revisionTime": "2017-03-29T03:22:34Z" }, { - "checksumSHA1": "nRYz071oypKp0bIsnN7K4kQ/Rlk=", + "checksumSHA1": "Ymghbn2vkOAdT9rNQxKR2qNuxtA=", "path": "github.com/circonus-labs/circonus-gometrics", - "revision": "dbab9a33438e3f8317407ef5d3a51c29340541db", - "revisionTime": "2017-02-21T20:27:28Z" + "revision": "55add91cfb689b0fd6e9fa67c58c7a948310a80e", + "revisionTime": "2017-03-17T00:26:31Z" }, { - "checksumSHA1": "PDusd0EuHz0oKiQKwKxFhbETxd8=", + "checksumSHA1": "6fUPaqXabil0m2nqKONt9lOmo4c=", "path": "github.com/circonus-labs/circonus-gometrics/api", - "revision": "dbab9a33438e3f8317407ef5d3a51c29340541db", - "revisionTime": "2017-02-21T20:27:28Z" + "revision": "55add91cfb689b0fd6e9fa67c58c7a948310a80e", + "revisionTime": "2017-03-17T00:26:31Z" }, { "checksumSHA1": "bQhz/fcyZPmuHSH2qwC4ZtATy5c=", "path": "github.com/circonus-labs/circonus-gometrics/api/config", - "revision": "dbab9a33438e3f8317407ef5d3a51c29340541db", - "revisionTime": "2017-02-21T20:27:28Z" + "revision": "55add91cfb689b0fd6e9fa67c58c7a948310a80e", + "revisionTime": "2017-03-17T00:26:31Z" }, { "checksumSHA1": "b9sBHPSxquftpsRrrpeDDloiErs=", "path": "github.com/circonus-labs/circonus-gometrics/checkmgr", - "revision": "dbab9a33438e3f8317407ef5d3a51c29340541db", - "revisionTime": "2017-02-21T20:27:28Z" + "revision": "55add91cfb689b0fd6e9fa67c58c7a948310a80e", + "revisionTime": "2017-03-17T00:26:31Z" }, { "checksumSHA1": "VbfeVqeOM+dTNxCmpvmYS0LwQn0=", @@ -399,74 +363,74 @@ { "checksumSHA1": "7uspQtEpYuBxaxrBTcxa+ZfiuJo=", "path": "github.com/coreos/etcd/auth/authpb", - "revision": "833aa518d840b5d58fa79f7f4ce570526f4e22fe", - "revisionTime": "2017-02-24T01:42:35Z" + "revision": "ca22c4c384bd713ba433f0aee87c103bd6f8badb", + "revisionTime": "2017-03-30T22:13:46Z" }, { "checksumSHA1": "Zgqp9pp3G4cSZbepa6tzhNeHZOI=", "path": "github.com/coreos/etcd/client", - "revision": "833aa518d840b5d58fa79f7f4ce570526f4e22fe", - "revisionTime": "2017-02-24T01:42:35Z" + "revision": "ca22c4c384bd713ba433f0aee87c103bd6f8badb", + "revisionTime": "2017-03-30T22:13:46Z" }, { - "checksumSHA1": "zvwzR7OOVY/HZmcS/kDhXhOsEu8=", + "checksumSHA1": "2df9k3kk7bAiVg/5jz/7MyQpzgM=", "path": "github.com/coreos/etcd/clientv3", - "revision": "833aa518d840b5d58fa79f7f4ce570526f4e22fe", - "revisionTime": "2017-02-24T01:42:35Z" + "revision": "ca22c4c384bd713ba433f0aee87c103bd6f8badb", + "revisionTime": "2017-03-30T22:13:46Z" }, { - "checksumSHA1": "tHT9JXi1O4YtDEJJTPMDwtyf8lQ=", + "checksumSHA1": "IJmn/DOYP6/X4ELeLZ9v50yHceY=", "path": "github.com/coreos/etcd/clientv3/concurrency", - "revision": "833aa518d840b5d58fa79f7f4ce570526f4e22fe", - "revisionTime": "2017-02-24T01:42:35Z" + "revision": "ca22c4c384bd713ba433f0aee87c103bd6f8badb", + "revisionTime": "2017-03-30T22:13:46Z" }, { - "checksumSHA1": "lY/Ca8yCqHV9cv0BQU88br59VeI=", + "checksumSHA1": "PnBbh5xS4/RtluD+Vatock/WZts=", "path": "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes", - "revision": "833aa518d840b5d58fa79f7f4ce570526f4e22fe", - "revisionTime": "2017-02-24T01:42:35Z" + "revision": "ca22c4c384bd713ba433f0aee87c103bd6f8badb", + "revisionTime": "2017-03-30T22:13:46Z" }, { - "checksumSHA1": "LYsCyIHMs+etSiWMrtKwQT5LOpU=", + "checksumSHA1": "n/cz6Sj/WWU+TwsQnASyEsNZdhk=", "path": "github.com/coreos/etcd/etcdserver/etcdserverpb", - "revision": "833aa518d840b5d58fa79f7f4ce570526f4e22fe", - "revisionTime": "2017-02-24T01:42:35Z" + "revision": "ca22c4c384bd713ba433f0aee87c103bd6f8badb", + "revisionTime": "2017-03-30T22:13:46Z" }, { "checksumSHA1": "PAnQN6F8iZuIfu9HHOORyVFaaOA=", "path": "github.com/coreos/etcd/mvcc/mvccpb", - "revision": "833aa518d840b5d58fa79f7f4ce570526f4e22fe", - "revisionTime": "2017-02-24T01:42:35Z" + "revision": "ca22c4c384bd713ba433f0aee87c103bd6f8badb", + "revisionTime": "2017-03-30T22:13:46Z" }, { "checksumSHA1": "mKIXx1kDwmVmdIpZ3pJtRBuUKso=", "path": "github.com/coreos/etcd/pkg/pathutil", - "revision": "833aa518d840b5d58fa79f7f4ce570526f4e22fe", - "revisionTime": "2017-02-24T01:42:35Z" + "revision": "ca22c4c384bd713ba433f0aee87c103bd6f8badb", + "revisionTime": "2017-03-30T22:13:46Z" }, { "checksumSHA1": "rMyIh9PsSvPs6Yd+YgKITQzQJx8=", "path": "github.com/coreos/etcd/pkg/tlsutil", - "revision": "833aa518d840b5d58fa79f7f4ce570526f4e22fe", - "revisionTime": "2017-02-24T01:42:35Z" + "revision": "ca22c4c384bd713ba433f0aee87c103bd6f8badb", + "revisionTime": "2017-03-30T22:13:46Z" }, { - "checksumSHA1": "45jhT/Qs5OuOJYSO9v27kQSKwWk=", + "checksumSHA1": "F2yucoqG6enNY2XDQGbXC3o4AHk=", "path": "github.com/coreos/etcd/pkg/transport", - "revision": "833aa518d840b5d58fa79f7f4ce570526f4e22fe", - "revisionTime": "2017-02-24T01:42:35Z" + "revision": "ca22c4c384bd713ba433f0aee87c103bd6f8badb", + "revisionTime": "2017-03-30T22:13:46Z" }, { "checksumSHA1": "gx1gJIMU6T0UNQ0bPZ/drQ8cpCI=", "path": "github.com/coreos/etcd/pkg/types", - "revision": "833aa518d840b5d58fa79f7f4ce570526f4e22fe", - "revisionTime": "2017-02-24T01:42:35Z" + "revision": "ca22c4c384bd713ba433f0aee87c103bd6f8badb", + "revisionTime": "2017-03-30T22:13:46Z" }, { "checksumSHA1": "sp2FkEyaIGiQFOEZCTDkBZgyHOs=", "path": "github.com/coreos/etcd/version", - "revision": "833aa518d840b5d58fa79f7f4ce570526f4e22fe", - "revisionTime": "2017-02-24T01:42:35Z" + "revision": "ca22c4c384bd713ba433f0aee87c103bd6f8badb", + "revisionTime": "2017-03-30T22:13:46Z" }, { "checksumSHA1": "nux9tCYmTA5bOCVODF3Rqzcu60U=", @@ -475,166 +439,166 @@ "revisionTime": "2017-02-09T20:17:57Z" }, { - "checksumSHA1": "HG4+dlJq5h+07VI1rOlpc5KBlfg=", + "checksumSHA1": "lqlfYr2E+/271+/dWF0nc6AuHdA=", "path": "github.com/denisenkom/go-mssqldb", - "revision": "9e40d9d5d325edfaa84d3374bfde6e1adce02d58", - "revisionTime": "2017-01-17T17:39:00Z" + "revision": "efb3002072bc69d12d92dfc70290ccfdaa5403cf", + "revisionTime": "2017-03-07T22:49:21Z" }, { - "checksumSHA1": "fCeKpGccEDm8jEZDGS15JtjLpeM=", + "checksumSHA1": "Ev1UrlC4YMoTR0XDP0JQ5+SJJoU=", "path": "github.com/docker/docker/api/types", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { "checksumSHA1": "jVJDbe0IcyjoKc2xbohwzQr+FF0=", "path": "github.com/docker/docker/api/types/blkiodev", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { - "checksumSHA1": "Vso/6NenP1G74lQjvyvzOdfIZ28=", + "checksumSHA1": "On8Z2ak/FE4i11b4owiJ7z8zXn8=", "path": "github.com/docker/docker/api/types/container", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { "checksumSHA1": "J2OKngfI3vgswudr9PZVUFcRRu0=", "path": "github.com/docker/docker/api/types/filters", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { - "checksumSHA1": "OXsrx4ynzLV+6/6vUeyru0Fprx8=", + "checksumSHA1": "UK+VdM648oWzyqE4OqttgmPqjoA=", "path": "github.com/docker/docker/api/types/mount", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { - "checksumSHA1": "dgDuXxG0tFyNEs+iDZ9IVZwix5g=", + "checksumSHA1": "oC2We2SzAYMULIfr7fXaoofnF+c=", "path": "github.com/docker/docker/api/types/network", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { "checksumSHA1": "93qfc2M61FdF62qeGXZHWpVSK8k=", "path": "github.com/docker/docker/api/types/registry", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { "checksumSHA1": "VTxWyFud/RedrpllGdQonVtGM/A=", "path": "github.com/docker/docker/api/types/strslice", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { - "checksumSHA1": "ZW8oay0NPuG8ffyP8EvEv3kS65g=", + "checksumSHA1": "bKh3YycNbUBom6hEaFGoTXVgfqM=", "path": "github.com/docker/docker/api/types/swarm", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { "checksumSHA1": "uDPQ3nHsrvGQc9tg/J9OSC4N5dQ=", "path": "github.com/docker/docker/api/types/versions", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { - "checksumSHA1": "wbEdvTNYWys36WGPQZke6tg16u8=", + "checksumSHA1": "6wMJSKoQxVgqd9mm4RUj8b36Bw8=", "path": "github.com/docker/docker/opts", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { - "checksumSHA1": "yaC0MhHtv8iaVuW25WxQf3x5hI4=", + "checksumSHA1": "KCIpVXqifFOzzpWcOYBxKudWGcI=", "path": "github.com/docker/docker/pkg/archive", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { - "checksumSHA1": "QRLZqpWT1chXnBrRBFdX/IrUyOY=", + "checksumSHA1": "NC4Tz59SrBVdPxt12GjYCxX60eU=", "path": "github.com/docker/docker/pkg/fileutils", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { "checksumSHA1": "W7Rnb5YoC9u0vT9eLKoezV3N4E8=", "path": "github.com/docker/docker/pkg/homedir", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { "checksumSHA1": "bfvwzIbEFwclBKJSNa6oaA27DgE=", "path": "github.com/docker/docker/pkg/idtools", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { "checksumSHA1": "MgSwrRRAfCQrrToxPAoMCmbieLE=", "path": "github.com/docker/docker/pkg/ioutils", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { - "checksumSHA1": "BlFSSK7zUjPzPuxkLmM/0wpvku8=", + "checksumSHA1": "kp20bhjkvJ06uW6DRfVIZbCj8SY=", "path": "github.com/docker/docker/pkg/jsonlog", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { "checksumSHA1": "IpcW+FAHu0DmbvbhqXuP42f4FCo=", "path": "github.com/docker/docker/pkg/jsonmessage", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { "checksumSHA1": "ndnAFCfsGC3upNQ6jAEwzxcurww=", "path": "github.com/docker/docker/pkg/longpath", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { "checksumSHA1": "oHqQiESBEfIJ+uu1+ue7PHvJ6CI=", "path": "github.com/docker/docker/pkg/pools", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { "checksumSHA1": "txf3EORYff4hO6PEvwBm2lyh1MU=", "path": "github.com/docker/docker/pkg/promise", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { "checksumSHA1": "H1rrbVmeE1z2TnkF7tSrfh+qUOY=", "path": "github.com/docker/docker/pkg/stdcopy", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { - "checksumSHA1": "NGqzajRG0Vri8ConQuHY3cCs1RA=", + "checksumSHA1": "D8GuQnZAnYS4isVu3iePaeqTsKM=", "path": "github.com/docker/docker/pkg/system", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { "checksumSHA1": "Nfp/0W+HK8ZEgklbSWmjJGXTJew=", "path": "github.com/docker/docker/pkg/term", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { "checksumSHA1": "EMjTiUTHNxqSsmdMB1V29hRvJbQ=", "path": "github.com/docker/docker/pkg/term/windows", - "revision": "60a94f7a04748490beaa3a2bad02734ad603b26c", - "revisionTime": "2017-02-26T15:06:39Z" + "revision": "9c0473fa652307c842f0f16297c4a38d409dfb60", + "revisionTime": "2017-03-30T19:09:39Z" }, { "checksumSHA1": "JbiWTzH699Sqz25XmDlsARpMN9w=", "path": "github.com/docker/go-connections/nat", - "revision": "1b14b2d192e2f91cdc2bc6bf9aee0b0e116eed42", - "revisionTime": "2017-02-22T21:12:45Z" + "revision": "a2afab9802043837035592f1c24827fb70766de9", + "revisionTime": "2017-03-01T23:41:00Z" }, { "checksumSHA1": "UmXGieuTJQOzJPspPJTVKKKMiUA=", @@ -661,10 +625,10 @@ "revisionTime": "2017-01-03T08:10:50Z" }, { - "checksumSHA1": "/wGNBzRJdmss307xZyBfYaE0c7Q=", + "checksumSHA1": "DG6hthmfdziVR23DRCqZkaYTDKc=", "path": "github.com/fsouza/go-dockerclient", - "revision": "54fbd1ff920ca5fd5ec53068d513c14c3a25bba9", - "revisionTime": "2017-02-21T19:53:20Z" + "revision": "87c7e50e0bcf800ed863c3c3b0fbcc67e3029140", + "revisionTime": "2017-03-16T00:43:41Z" }, { "checksumSHA1": "wP7Xw4W1DUgRAOLkXMAlGGGTgaU=", @@ -673,10 +637,10 @@ "revisionTime": "2017-02-09T01:19:05Z" }, { - "checksumSHA1": "muGVyM8mY3/gcap6kr4Ib3F5Xn4=", + "checksumSHA1": "ImX1uv6O09ggFeBPUJJ2nu7MPSA=", "path": "github.com/ghodss/yaml", - "revision": "04f313413ffd65ce25f2541bfd2b2ceec5c0908c", - "revisionTime": "2016-12-07T00:33:20Z" + "revision": "0ca9ea5df5451ffdf184b4428c902747c2c11cd7", + "revisionTime": "2017-03-27T23:54:44Z" }, { "checksumSHA1": "SUYEFYVLIO7x9+EBKlDv0QPe4CU=", @@ -685,58 +649,58 @@ "revisionTime": "2016-12-05T22:32:45Z" }, { - "checksumSHA1": "Cu67hH9Sg86TBZb4An5owwfzEN0=", + "checksumSHA1": "+IH9gXMht4fL/fxKRZ4sqGBps1g=", "path": "github.com/go-ini/ini", - "revision": "c437d20015c2ab6454b7a66a13109ff0fb99e17a", - "revisionTime": "2017-02-23T22:22:15Z" + "revision": "e7fea39b01aea8d5671f6858f0532f56e8bff3a5", + "revisionTime": "2017-03-28T15:39:02Z" }, { - "checksumSHA1": "ezktpcI0As/fZ0rN7BnBNtUfIsQ=", + "checksumSHA1": "DrtqZNwpWSsx6MajaEv2cTNTx/Y=", "path": "github.com/go-ldap/ldap", - "revision": "931bf64d9da4d3d241757d12f1f1420b8605f6fa", - "revisionTime": "2017-02-02T15:09:24Z" + "revision": "13cedcf58a1ea124045dea529a66c849d3444c8e", + "revisionTime": "2017-03-05T04:08:57Z" }, { - "checksumSHA1": "QD6LqgLz2JMxXqns8TaxtK9AuHs=", + "checksumSHA1": "42vkdsxNaLyPu+FktCzZ/8zsNSE=", "path": "github.com/go-sql-driver/mysql", - "revision": "2e00b5cd70399450106cec6431c2e2ce3cae5034", - "revisionTime": "2016-12-24T12:10:19Z" + "revision": "9dee4ca50b83acdf57a35fb9e6fb4be640afa2f3", + "revisionTime": "2017-03-27T11:30:21Z" }, { - "checksumSHA1": "7BEKq0xoRvV1whMiWU0aV3WubXE=", + "checksumSHA1": "DCDl7i51+23OFgdGMGwiqxJ7h4I=", "path": "github.com/gocql/gocql", - "revision": "1f874493e9e5aebe46b312593cbd9cb5d3946eda", - "revisionTime": "2017-02-15T00:22:50Z" + "revision": "f057eea9273ef3a2a6c231fae990238c6b6dc40b", + "revisionTime": "2017-03-29T18:05:04Z" }, { - "checksumSHA1": "Z3N6HDGWcvcNu0FloZRq54uO3h4=", + "checksumSHA1": "7RlYIbPYgPkxDDCSEuE6bvYEEeU=", "path": "github.com/gocql/gocql/internal/lru", - "revision": "1f874493e9e5aebe46b312593cbd9cb5d3946eda", - "revisionTime": "2017-02-15T00:22:50Z" + "revision": "f057eea9273ef3a2a6c231fae990238c6b6dc40b", + "revisionTime": "2017-03-29T18:05:04Z" }, { "checksumSHA1": "ctK9mwZKnt/8dHxx2Ef6nZTljZs=", "path": "github.com/gocql/gocql/internal/murmur", - "revision": "1f874493e9e5aebe46b312593cbd9cb5d3946eda", - "revisionTime": "2017-02-15T00:22:50Z" + "revision": "f057eea9273ef3a2a6c231fae990238c6b6dc40b", + "revisionTime": "2017-03-29T18:05:04Z" }, { "checksumSHA1": "tZQDfMMTKrYMXqen0zjJWLtOf1A=", "path": "github.com/gocql/gocql/internal/streams", - "revision": "1f874493e9e5aebe46b312593cbd9cb5d3946eda", - "revisionTime": "2017-02-15T00:22:50Z" + "revision": "f057eea9273ef3a2a6c231fae990238c6b6dc40b", + "revisionTime": "2017-03-29T18:05:04Z" }, { "checksumSHA1": "APDDi2ohrU7OkChQCekD9tSVUhs=", "path": "github.com/golang/protobuf/jsonpb", - "revision": "69b215d01a5606c843240eab4937eab3acee6530", - "revisionTime": "2017-02-17T23:44:32Z" + "revision": "e514ff57e81961e29ca4d63ea13e7e300a01fb53", + "revisionTime": "2017-03-30T23:11:32Z" }, { "checksumSHA1": "kBeNcaKk56FguvPSUCEaH6AxpRc=", "path": "github.com/golang/protobuf/proto", - "revision": "69b215d01a5606c843240eab4937eab3acee6530", - "revisionTime": "2017-02-17T23:44:32Z" + "revision": "e514ff57e81961e29ca4d63ea13e7e300a01fb53", + "revisionTime": "2017-03-30T23:11:32Z" }, { "checksumSHA1": "p/8vSviYF91gFflhrt5vkyksroo=", @@ -745,10 +709,10 @@ "revisionTime": "2017-02-15T23:32:05Z" }, { - "checksumSHA1": "5418jsN59UajTA7Ib9TYiq/XJIo=", + "checksumSHA1": "cVm28zOA1aijLNbnFtVKFo5Jazw=", "path": "github.com/google/go-github/github", - "revision": "b59d3a6479bd00374a1523ad032da4621d9dd84a", - "revisionTime": "2017-02-23T16:22:02Z" + "revision": "e96a1efec3efbfec63cf32984b21bf98933042a2", + "revisionTime": "2017-03-30T22:50:55Z" }, { "checksumSHA1": "p3IB18uJRs4dL2K5yx24MrLYE9A=", @@ -757,28 +721,28 @@ "revisionTime": "2017-01-11T10:11:55Z" }, { - "checksumSHA1": "V/53BpqgOkSDZCX6snQCAkdO2fM=", + "checksumSHA1": "0x0CoHbgoWngucjKSDPFodeL8ek=", "path": "github.com/googleapis/gax-go", - "revision": "da06d194a00e19ce00d9011a13931c3f6f6887c7", - "revisionTime": "2016-11-07T00:24:06Z" + "revision": "9af46dd5a1713e8b5cd71106287eba3cefdde50b", + "revisionTime": "2017-03-21T00:53:43Z" }, { - "checksumSHA1": "SeAuuufiXeiMVlYi/j8YT9kPrCM=", + "checksumSHA1": "wFjV19ovhVabAqB6NTF8sWAJDIA=", "path": "github.com/grpc-ecosystem/grpc-gateway/runtime", - "revision": "f3aa758b8ae01abecf727dba035444545dbe41de", - "revisionTime": "2017-02-24T19:04:31Z" + "revision": "04870f0741c24d2bfc76a7c7db112f4e107e9ada", + "revisionTime": "2017-03-24T13:56:43Z" }, { - "checksumSHA1": "x396LPNfci/5x8aVJbliQHH11HQ=", + "checksumSHA1": "et8uiXNw6yM96U/UZgn2vKtyr3Q=", "path": "github.com/grpc-ecosystem/grpc-gateway/runtime/internal", - "revision": "f3aa758b8ae01abecf727dba035444545dbe41de", - "revisionTime": "2017-02-24T19:04:31Z" + "revision": "04870f0741c24d2bfc76a7c7db112f4e107e9ada", + "revisionTime": "2017-03-24T13:56:43Z" }, { "checksumSHA1": "vqiK5r5dntV7JNZ+ZsGlD0Samos=", "path": "github.com/grpc-ecosystem/grpc-gateway/utilities", - "revision": "f3aa758b8ae01abecf727dba035444545dbe41de", - "revisionTime": "2017-02-24T19:04:31Z" + "revision": "04870f0741c24d2bfc76a7c7db112f4e107e9ada", + "revisionTime": "2017-03-24T13:56:43Z" }, { "checksumSHA1": "O0r0hj4YL+jSRNjnshkeH4GY+4s=", @@ -787,16 +751,16 @@ "revisionTime": "2016-01-25T11:53:50Z" }, { - "checksumSHA1": "AQMAS0aD5VifN0ugbv8xNTt7mM4=", + "checksumSHA1": "GsJ84gKbQno8KbojhVTgSVWNues=", "path": "github.com/hashicorp/consul/api", - "revision": "f5fe659497cecbc6947e0ad06af295e4633a3c30", - "revisionTime": "2017-02-24T17:54:33Z" + "revision": "21b5570cb4eb2adb39e5d674b18119c036e232a7", + "revisionTime": "2017-03-30T23:24:08Z" }, { - "checksumSHA1": "HDRi8BjyCm/zCYGA8l/40GMuWN8=", + "checksumSHA1": "XTA8JEhsuJGTUTchjM++oEG7B14=", "path": "github.com/hashicorp/consul/lib", - "revision": "f5fe659497cecbc6947e0ad06af295e4633a3c30", - "revisionTime": "2017-02-24T17:54:33Z" + "revision": "21b5570cb4eb2adb39e5d674b18119c036e232a7", + "revisionTime": "2017-03-30T23:24:08Z" }, { "checksumSHA1": "cdOCt0Yb+hdErz8NAQqayxPmRsY=", @@ -933,8 +897,8 @@ { "checksumSHA1": "E3Xcanc9ouQwL+CZGOUyA/+giLg=", "path": "github.com/hashicorp/serf/coordinate", - "revision": "838c422c419776dd868ade08da6b11be7a71bf99", - "revisionTime": "2017-02-23T17:44:37Z" + "revision": "19f2c401e122352c047a84d6584dd51e2fb8fcc4", + "revisionTime": "2017-03-08T19:39:51Z" }, { "checksumSHA1": "ZhK6IO2XN81Y+3RAjTcVm1Ic7oU=", @@ -957,92 +921,104 @@ { "checksumSHA1": "VJk3rOWfxEV9Ilig5lgzH1qg8Ss=", "path": "github.com/keybase/go-crypto/brainpool", - "revision": "95833b1d77cdcc574fa3f0c85fd216598f289ab5", - "revisionTime": "2017-02-24T12:24:59Z" + "revision": "3e9e98fd9221edf772b06f22e9a447fbbac18804", + "revisionTime": "2017-03-29T14:02:33Z" }, { "checksumSHA1": "rnRjEJs5luF+DIXp2J6LFcQk8Gg=", "path": "github.com/keybase/go-crypto/cast5", - "revision": "95833b1d77cdcc574fa3f0c85fd216598f289ab5", - "revisionTime": "2017-02-24T12:24:59Z" + "revision": "3e9e98fd9221edf772b06f22e9a447fbbac18804", + "revisionTime": "2017-03-29T14:02:33Z" }, { "checksumSHA1": "F5++ZQS5Vt7hd6lxPCKTffvph1A=", "path": "github.com/keybase/go-crypto/curve25519", - "revision": "95833b1d77cdcc574fa3f0c85fd216598f289ab5", - "revisionTime": "2017-02-24T12:24:59Z" + "revision": "3e9e98fd9221edf772b06f22e9a447fbbac18804", + "revisionTime": "2017-03-29T14:02:33Z" }, { - "checksumSHA1": "tU/72BMqzPlbK6qMeq7cklYwLWY=", + "checksumSHA1": "IvrDXwIixB5yPPbo6tq1/1cSn78=", + "path": "github.com/keybase/go-crypto/ed25519", + "revision": "3e9e98fd9221edf772b06f22e9a447fbbac18804", + "revisionTime": "2017-03-29T14:02:33Z" + }, + { + "checksumSHA1": "4+fslB6pCbplNq4viy6CrOkkY6Y=", + "path": "github.com/keybase/go-crypto/ed25519/internal/edwards25519", + "revision": "3e9e98fd9221edf772b06f22e9a447fbbac18804", + "revisionTime": "2017-03-29T14:02:33Z" + }, + { + "checksumSHA1": "xNtNchw3ma1ufM4XJYSh6jYnnH0=", "path": "github.com/keybase/go-crypto/openpgp", - "revision": "95833b1d77cdcc574fa3f0c85fd216598f289ab5", - "revisionTime": "2017-02-24T12:24:59Z" + "revision": "3e9e98fd9221edf772b06f22e9a447fbbac18804", + "revisionTime": "2017-03-29T14:02:33Z" }, { "checksumSHA1": "y61I7+hCekP1Rk0qxgUQ+iozXak=", "path": "github.com/keybase/go-crypto/openpgp/armor", - "revision": "95833b1d77cdcc574fa3f0c85fd216598f289ab5", - "revisionTime": "2017-02-24T12:24:59Z" + "revision": "3e9e98fd9221edf772b06f22e9a447fbbac18804", + "revisionTime": "2017-03-29T14:02:33Z" }, { "checksumSHA1": "nWhmwjBJqPSvkCWqaap2Z9EiS1k=", "path": "github.com/keybase/go-crypto/openpgp/ecdh", - "revision": "95833b1d77cdcc574fa3f0c85fd216598f289ab5", - "revisionTime": "2017-02-24T12:24:59Z" + "revision": "3e9e98fd9221edf772b06f22e9a447fbbac18804", + "revisionTime": "2017-03-29T14:02:33Z" }, { "checksumSHA1": "uxXG9IC/XF8jwwvZUbW65+x8/+M=", "path": "github.com/keybase/go-crypto/openpgp/elgamal", - "revision": "95833b1d77cdcc574fa3f0c85fd216598f289ab5", - "revisionTime": "2017-02-24T12:24:59Z" + "revision": "3e9e98fd9221edf772b06f22e9a447fbbac18804", + "revisionTime": "2017-03-29T14:02:33Z" }, { "checksumSHA1": "EyUf82Yknzc75m8RcA21CNQINw0=", "path": "github.com/keybase/go-crypto/openpgp/errors", - "revision": "95833b1d77cdcc574fa3f0c85fd216598f289ab5", - "revisionTime": "2017-02-24T12:24:59Z" + "revision": "3e9e98fd9221edf772b06f22e9a447fbbac18804", + "revisionTime": "2017-03-29T14:02:33Z" }, { - "checksumSHA1": "esp0WrWFo5Ohk/bXhjC5NrzWnS0=", + "checksumSHA1": "dJEz4E5pG9DX0Lfpo5s3UjB2FnY=", "path": "github.com/keybase/go-crypto/openpgp/packet", - "revision": "95833b1d77cdcc574fa3f0c85fd216598f289ab5", - "revisionTime": "2017-02-24T12:24:59Z" + "revision": "3e9e98fd9221edf772b06f22e9a447fbbac18804", + "revisionTime": "2017-03-29T14:02:33Z" }, { "checksumSHA1": "BGDxg1Xtsz0DSPzdQGJLLQqfYc8=", "path": "github.com/keybase/go-crypto/openpgp/s2k", - "revision": "95833b1d77cdcc574fa3f0c85fd216598f289ab5", - "revisionTime": "2017-02-24T12:24:59Z" + "revision": "3e9e98fd9221edf772b06f22e9a447fbbac18804", + "revisionTime": "2017-03-29T14:02:33Z" }, { "checksumSHA1": "rE3pp7b3gfcmBregzpIvN5IdFhY=", "path": "github.com/keybase/go-crypto/rsa", - "revision": "95833b1d77cdcc574fa3f0c85fd216598f289ab5", - "revisionTime": "2017-02-24T12:24:59Z" + "revision": "3e9e98fd9221edf772b06f22e9a447fbbac18804", + "revisionTime": "2017-03-29T14:02:33Z" }, { - "checksumSHA1": "FZem1OXCd+ogu1yeG11GMlGiGVA=", + "checksumSHA1": "uTUsjF7bymOuKvXbW2BpkK/w4Vg=", "path": "github.com/lib/pq", - "revision": "ba5d4f7a35561e22fbdf7a39aa0070f4d460cfc0", - "revisionTime": "2017-02-13T22:10:49Z" + "revision": "2704adc878c21e1329f46f6e56a1c387d788ff94", + "revisionTime": "2017-03-24T20:46:54Z" }, { - "checksumSHA1": "xppHi82MLqVx1eyQmbhTesAEjx8=", + "checksumSHA1": "Gk3jTNQ5uGDUE0WMJFWcYz9PMps=", "path": "github.com/lib/pq/oid", - "revision": "ba5d4f7a35561e22fbdf7a39aa0070f4d460cfc0", - "revisionTime": "2017-02-13T22:10:49Z" + "revision": "2704adc878c21e1329f46f6e56a1c387d788ff94", + "revisionTime": "2017-03-24T20:46:54Z" }, { - "checksumSHA1": "QPtwwLkTyoqQoigevgprwudIdXk=", + "checksumSHA1": "uAVg4Tj5Fxi62z7/ScDEkHr6Ue8=", "path": "github.com/mattn/go-colorable", - "revision": "d898aa9fb31c91f35dd28ca75db377eff023c076", - "revisionTime": "2017-02-21T00:41:37Z" + "revision": "ded68f7a9561c023e790de24279db7ebf473ea80", + "revisionTime": "2017-03-27T08:33:44Z" }, { - "checksumSHA1": "Jyv1TH1YHrCVewmVE8FgSh1+92s=", + "checksumSHA1": "U6lX43KDDlNOn+Z0Yyww+ZzHfFo=", "path": "github.com/mattn/go-isatty", - "revision": "dda3de49cbfcec471bd7a70e6cc01fcc3ff90109", - "revisionTime": "2017-02-16T23:59:08Z" + "revision": "fc9e8d8ef48496124e79ae0df75490096eccf6fe", + "revisionTime": "2017-03-22T23:44:13Z" }, { "checksumSHA1": "CIK3BBNX3nuUQCmNqTQydNfMNKI=", @@ -1057,16 +1033,16 @@ "revisionTime": "2016-10-27T14:08:23Z" }, { - "checksumSHA1": "aEQOJ5PDOybetOpUvOo7HqicJ1s=", + "checksumSHA1": "YISEamyRa3rb0qrdSoUHmVIR5WA=", "path": "github.com/michaelklishin/rabbit-hole", - "revision": "6e6fd8776f56a98f5b3b22085e2d5745d2195aeb", - "revisionTime": "2017-02-02T07:45:48Z" + "revision": "c5c30aa7b85409c68b9ba3f81dab4f24117dab85", + "revisionTime": "2017-03-25T20:30:01Z" }, { - "checksumSHA1": "PfnjkP75J4WIemUueJcrH2VbRHY=", + "checksumSHA1": "UP+pXl+ic9y6qrpZA5MqDIAuGfw=", "path": "github.com/mitchellh/cli", - "revision": "4e26b972666dd9db4e1ffee38c8dd0c675bd245d", - "revisionTime": "2017-02-22T21:41:24Z" + "revision": "ee8578a9c12a5bb9d55303b9665cc448772c81b8", + "revisionTime": "2017-03-28T05:23:52Z" }, { "checksumSHA1": "kSmDazz+cokgcHQT7q56Na+IBe0=", @@ -1081,10 +1057,10 @@ "revisionTime": "2016-12-03T19:45:07Z" }, { - "checksumSHA1": "wTMmmuol+KHkz9EwVKaOjd2O4cs=", + "checksumSHA1": "MlX15lJuV8DYARX5RJY8rqrSEWQ=", "path": "github.com/mitchellh/mapstructure", - "revision": "db1efb556f84b25a0a13a04aad883943538ad2e0", - "revisionTime": "2017-01-25T05:19:37Z" + "revision": "53818660ed4955e899c0bcafa97299a388bd7c8e", + "revisionTime": "2017-03-07T20:11:23Z" }, { "checksumSHA1": "67Y6z6rHipvOvFwCZZXqKH+TWao=", @@ -1093,28 +1069,28 @@ "revisionTime": "2017-01-10T16:52:07Z" }, { - "checksumSHA1": "Uk/gjQ0znqKhWKrVklXcxejBJOQ=", + "checksumSHA1": "BxxkAJ/Nm61PybCXvQIZJwyTj3Y=", "path": "github.com/ncw/swift", - "revision": "6c1b1510538e1f00d49a558b7b9b87d71bc454d6", - "revisionTime": "2016-12-13T16:20:37Z" + "revision": "8e9b10220613abdbc2896808ee6b43e411a4fa6c", + "revisionTime": "2017-03-15T10:59:10Z" }, { "checksumSHA1": "Ssz9STAL5P2+1poiM63klFjhqhE=", "path": "github.com/opencontainers/runc/libcontainer/system", - "revision": "cf883a87e762961a5b97fad993aa377750512b28", - "revisionTime": "2017-02-25T05:48:51Z" + "revision": "653207bc29a6d2d62b5d4f55b596467cb715a128", + "revisionTime": "2017-03-27T18:58:03Z" }, { - "checksumSHA1": "KVZRinwxB3LsuMUkOZw5/U2WgE8=", + "checksumSHA1": "MA07KaAp1aPVheuopyrFr7pxANs=", "path": "github.com/opencontainers/runc/libcontainer/user", - "revision": "cf883a87e762961a5b97fad993aa377750512b28", - "revisionTime": "2017-02-25T05:48:51Z" + "revision": "653207bc29a6d2d62b5d4f55b596467cb715a128", + "revisionTime": "2017-03-27T18:58:03Z" }, { "checksumSHA1": "wJWRH5ORhyIO29LxvA/Sug1skF0=", "path": "github.com/ory-am/common/env", - "revision": "c6ab75d78d2c26a3cbfb37d742155cfc3ad74e23", - "revisionTime": "2017-02-20T09:14:30Z" + "revision": "ba06ec2f738cb3a55608657c2e998a1eef675423", + "revisionTime": "2017-03-06T12:24:56Z" }, { "checksumSHA1": "Se195FlZ160eaEk/uVx4KdTPSxU=", @@ -1125,15 +1101,8 @@ { "checksumSHA1": "ynJSWoF6v+3zMnh9R0QmmG6iGV8=", "path": "github.com/pkg/errors", - "revision": "248dadf4e9068a0b3e79f02ed0a610d935de5302", - "revisionTime": "2016-10-29T09:36:37Z" - }, - { - "checksumSHA1": "Uldkr4yJ25TcI1+KPc6NP66c4Gg=", - "path": "github.com/pquerna/otp", - "revision": "9e1935374bc73ffe011187dafed51a412b90fe43", - "revisionTime": "2017-02-23T01:06:52Z", - "tree": true + "revision": "ff09b135c25aae272398c51a07235b90a75aa4f0", + "revisionTime": "2017-03-16T20:15:38Z" }, { "checksumSHA1": "ZOhewV1DsQjTYlx8a+ifrZki2Vg=", @@ -1160,358 +1129,418 @@ "revisionTime": "2016-10-03T17:19:47Z" }, { - "checksumSHA1": "CoxdaTYdPZNJXr8mJfLxye428N0=", + "checksumSHA1": "MxLnUmfrP+r5HfCZM29+WPKebn8=", "path": "github.com/ugorji/go/codec", - "revision": "c88ee250d0221a57af388746f5cf03768c21d6e2", - "revisionTime": "2017-02-15T20:11:44Z" + "revision": "708a42d246822952f38190a8d8c4e6b16a0e600c", + "revisionTime": "2017-03-12T11:21:14Z" }, { "checksumSHA1": "vE43s37+4CJ2CDU6TlOUOYE0K9c=", "path": "golang.org/x/crypto/bcrypt", - "revision": "453249f01cfeb54c3d549ddb75ff152ca243f9d8", - "revisionTime": "2017-02-08T20:51:15Z" + "revision": "3cb07270c9455e8ad27956a70891c962d121a228", + "revisionTime": "2017-03-30T16:02:45Z" }, { "checksumSHA1": "JsJdKXhz87gWenMwBeejTOeNE7k=", "path": "golang.org/x/crypto/blowfish", - "revision": "453249f01cfeb54c3d549ddb75ff152ca243f9d8", - "revisionTime": "2017-02-08T20:51:15Z" + "revision": "3cb07270c9455e8ad27956a70891c962d121a228", + "revisionTime": "2017-03-30T16:02:45Z" }, { "checksumSHA1": "C1KKOxFoW7/W/NFNpiXK+boguNo=", "path": "golang.org/x/crypto/curve25519", - "revision": "453249f01cfeb54c3d549ddb75ff152ca243f9d8", - "revisionTime": "2017-02-08T20:51:15Z" + "revision": "3cb07270c9455e8ad27956a70891c962d121a228", + "revisionTime": "2017-03-30T16:02:45Z" }, { "checksumSHA1": "wGb//LjBPNxYHqk+dcLo7BjPXK8=", "path": "golang.org/x/crypto/ed25519", - "revision": "453249f01cfeb54c3d549ddb75ff152ca243f9d8", - "revisionTime": "2017-02-08T20:51:15Z" + "revision": "3cb07270c9455e8ad27956a70891c962d121a228", + "revisionTime": "2017-03-30T16:02:45Z" }, { "checksumSHA1": "LXFcVx8I587SnWmKycSDEq9yvK8=", "path": "golang.org/x/crypto/ed25519/internal/edwards25519", - "revision": "453249f01cfeb54c3d549ddb75ff152ca243f9d8", - "revisionTime": "2017-02-08T20:51:15Z" + "revision": "3cb07270c9455e8ad27956a70891c962d121a228", + "revisionTime": "2017-03-30T16:02:45Z" }, { "checksumSHA1": "4D8hxMIaSDEW5pCQk22Xj4DcDh4=", "path": "golang.org/x/crypto/hkdf", - "revision": "453249f01cfeb54c3d549ddb75ff152ca243f9d8", - "revisionTime": "2017-02-08T20:51:15Z" + "revision": "3cb07270c9455e8ad27956a70891c962d121a228", + "revisionTime": "2017-03-30T16:02:45Z" }, { "checksumSHA1": "MCeXr2RNeiG1XG6V+er1OR0qyeo=", "path": "golang.org/x/crypto/md4", - "revision": "453249f01cfeb54c3d549ddb75ff152ca243f9d8", - "revisionTime": "2017-02-08T20:51:15Z" + "revision": "3cb07270c9455e8ad27956a70891c962d121a228", + "revisionTime": "2017-03-30T16:02:45Z" }, { - "checksumSHA1": "fsrFs762jlaILyqqQImS1GfvIvw=", + "checksumSHA1": "PHfYk3eOBa0UyRGMgeXDF3i/QxM=", "path": "golang.org/x/crypto/ssh", - "revision": "453249f01cfeb54c3d549ddb75ff152ca243f9d8", - "revisionTime": "2017-02-08T20:51:15Z" + "revision": "3cb07270c9455e8ad27956a70891c962d121a228", + "revisionTime": "2017-03-30T16:02:45Z" }, { "checksumSHA1": "SJ3Ma3Ozavxpbh1usZWBCnzMKIc=", "path": "golang.org/x/crypto/ssh/agent", - "revision": "453249f01cfeb54c3d549ddb75ff152ca243f9d8", - "revisionTime": "2017-02-08T20:51:15Z" + "revision": "3cb07270c9455e8ad27956a70891c962d121a228", + "revisionTime": "2017-03-30T16:02:45Z" }, { "checksumSHA1": "xiderUuvye8Kpn7yX3niiJg32bE=", "path": "golang.org/x/crypto/ssh/terminal", - "revision": "453249f01cfeb54c3d549ddb75ff152ca243f9d8", - "revisionTime": "2017-02-08T20:51:15Z" + "revision": "3cb07270c9455e8ad27956a70891c962d121a228", + "revisionTime": "2017-03-30T16:02:45Z" }, { "checksumSHA1": "Y+HGqEkYM15ir+J93MEaHdyFy0c=", "path": "golang.org/x/net/context", - "revision": "dd2d9a67c97da0afa00d5726e28086007a0acce5", - "revisionTime": "2017-02-23T02:43:42Z" + "revision": "ffcf1bedda3b04ebb15a168a59800a73d6dc0f4d", + "revisionTime": "2017-03-29T01:43:45Z" }, { "checksumSHA1": "WHc3uByvGaMcnSoI21fhzYgbOgg=", "path": "golang.org/x/net/context/ctxhttp", - "revision": "dd2d9a67c97da0afa00d5726e28086007a0acce5", - "revisionTime": "2017-02-23T02:43:42Z" + "revision": "ffcf1bedda3b04ebb15a168a59800a73d6dc0f4d", + "revisionTime": "2017-03-29T01:43:45Z" }, { - "checksumSHA1": "HwLsnapk27meOcqcx4G2iaAysq4=", + "checksumSHA1": "cdT+oqPhYKGf3r+HkPo51IauZnA=", "path": "golang.org/x/net/http2", - "revision": "906cda9512f77671ab44f8c8563b13a8e707b230", - "revisionTime": "2017-02-18T20:36:51Z" + "revision": "ffcf1bedda3b04ebb15a168a59800a73d6dc0f4d", + "revisionTime": "2017-03-29T01:43:45Z" }, { - "checksumSHA1": "kyClpesDqa6LA7CFPGgKrX9NKkA=", + "checksumSHA1": "CBoO1DFToP49Ezph60yZ4J4PBBs=", "path": "golang.org/x/net/http2/hpack", - "revision": "906cda9512f77671ab44f8c8563b13a8e707b230", - "revisionTime": "2017-02-18T20:36:51Z" + "revision": "ffcf1bedda3b04ebb15a168a59800a73d6dc0f4d", + "revisionTime": "2017-03-29T01:43:45Z" }, { - "checksumSHA1": "GIGmSrYACByf5JDIP9ByBZksY80=", + "checksumSHA1": "VrzPJyWI6disCgYuVEQzkjqUsJk=", "path": "golang.org/x/net/idna", - "revision": "dd2d9a67c97da0afa00d5726e28086007a0acce5", - "revisionTime": "2017-02-23T02:43:42Z" + "revision": "ffcf1bedda3b04ebb15a168a59800a73d6dc0f4d", + "revisionTime": "2017-03-29T01:43:45Z" }, { "checksumSHA1": "UxahDzW2v4mf/+aFxruuupaoIwo=", "path": "golang.org/x/net/internal/timeseries", - "revision": "dd2d9a67c97da0afa00d5726e28086007a0acce5", - "revisionTime": "2017-02-23T02:43:42Z" + "revision": "ffcf1bedda3b04ebb15a168a59800a73d6dc0f4d", + "revisionTime": "2017-03-29T01:43:45Z" }, { "checksumSHA1": "3xyuaSNmClqG4YWC7g0isQIbUTc=", "path": "golang.org/x/net/lex/httplex", - "revision": "dd2d9a67c97da0afa00d5726e28086007a0acce5", - "revisionTime": "2017-02-23T02:43:42Z" + "revision": "ffcf1bedda3b04ebb15a168a59800a73d6dc0f4d", + "revisionTime": "2017-03-29T01:43:45Z" }, { "checksumSHA1": "GQHKESPeCcAsnerZPtHadvKUIzs=", "path": "golang.org/x/net/trace", - "revision": "dd2d9a67c97da0afa00d5726e28086007a0acce5", - "revisionTime": "2017-02-23T02:43:42Z" + "revision": "ffcf1bedda3b04ebb15a168a59800a73d6dc0f4d", + "revisionTime": "2017-03-29T01:43:45Z" }, { - "checksumSHA1": "Zt7DIRCaUg5qfhfxyR1wCA+EjCE=", + "checksumSHA1": "SjCoL7KD7qBmgSuqGTCAuUhigDk=", "path": "golang.org/x/oauth2", - "revision": "b9780ec78894ab900c062d58ee3076cd9b2a4501", - "revisionTime": "2017-02-14T22:24:16Z" + "revision": "7fdf09982454086d5570c7db3e11f360194830ca", + "revisionTime": "2017-03-21T01:28:43Z" }, { - "checksumSHA1": "1TbkNRLtD4BFSBT1+dZ19RbGd/g=", + "checksumSHA1": "ma8yoPKIsJlRiVBDUEdX78kyAdU=", "path": "golang.org/x/oauth2/google", - "revision": "b9780ec78894ab900c062d58ee3076cd9b2a4501", - "revisionTime": "2017-02-14T22:24:16Z" + "revision": "7fdf09982454086d5570c7db3e11f360194830ca", + "revisionTime": "2017-03-21T01:28:43Z" }, { - "checksumSHA1": "gChvVZYdb6Bw/vjIpfYJfNvXPoU=", + "checksumSHA1": "BAkyxbaxkrZbzGtfG5iX8v6ypIo=", "path": "golang.org/x/oauth2/internal", - "revision": "b9780ec78894ab900c062d58ee3076cd9b2a4501", - "revisionTime": "2017-02-14T22:24:16Z" + "revision": "7fdf09982454086d5570c7db3e11f360194830ca", + "revisionTime": "2017-03-21T01:28:43Z" }, { "checksumSHA1": "huVltYnXdRFDJLgp/ZP9IALzG7g=", "path": "golang.org/x/oauth2/jws", - "revision": "b9780ec78894ab900c062d58ee3076cd9b2a4501", - "revisionTime": "2017-02-14T22:24:16Z" + "revision": "7fdf09982454086d5570c7db3e11f360194830ca", + "revisionTime": "2017-03-21T01:28:43Z" }, { "checksumSHA1": "/eV4E08BY+f1ZikiR7OOMJAj3m0=", "path": "golang.org/x/oauth2/jwt", - "revision": "b9780ec78894ab900c062d58ee3076cd9b2a4501", - "revisionTime": "2017-02-14T22:24:16Z" + "revision": "7fdf09982454086d5570c7db3e11f360194830ca", + "revisionTime": "2017-03-21T01:28:43Z" }, { - "checksumSHA1": "oVd5OHIvNHR5bhUvnBMPLVjaL6E=", + "checksumSHA1": "mIZhS4hXW3qVQxsJ6tqztjJoJ8A=", "path": "golang.org/x/sys/unix", - "revision": "e4594059fe4cde2daf423055a596c2cd1e6c9adf", - "revisionTime": "2017-02-23T22:36:15Z" + "revision": "9a7256cb28ed514b4e1e5f68959914c4c28a92e0", + "revisionTime": "2017-03-29T05:31:33Z" }, { "checksumSHA1": "kQB2wRB3twjUp615F6zEwGHjNe0=", "path": "golang.org/x/sys/windows", - "revision": "e4594059fe4cde2daf423055a596c2cd1e6c9adf", - "revisionTime": "2017-02-23T22:36:15Z" + "revision": "9a7256cb28ed514b4e1e5f68959914c4c28a92e0", + "revisionTime": "2017-03-29T05:31:33Z" + }, + { + "checksumSHA1": "ZQdHbB9VYCXwQ+9/CmZPhJv0+SM=", + "path": "golang.org/x/text/internal/gen", + "revision": "65f4f820a7954b82e5c9325e1e088a4fda098f36", + "revisionTime": "2017-03-30T05:43:03Z" + }, + { + "checksumSHA1": "47nwiUyVBY2RKoEGXmCSvusY4Js=", + "path": "golang.org/x/text/internal/triegen", + "revision": "65f4f820a7954b82e5c9325e1e088a4fda098f36", + "revisionTime": "2017-03-30T05:43:03Z" + }, + { + "checksumSHA1": "Yd5wMObzagIfCiKLpZbtBIrOUA4=", + "path": "golang.org/x/text/internal/ucd", + "revision": "65f4f820a7954b82e5c9325e1e088a4fda098f36", + "revisionTime": "2017-03-30T05:43:03Z" + }, + { + "checksumSHA1": "faFDXp++cLjLBlvsr+izZ+go1WU=", + "path": "golang.org/x/text/secure/bidirule", + "revision": "65f4f820a7954b82e5c9325e1e088a4fda098f36", + "revisionTime": "2017-03-30T05:43:03Z" + }, + { + "checksumSHA1": "ziMb9+ANGRJSSIuxYdRbA+cDRBQ=", + "path": "golang.org/x/text/transform", + "revision": "65f4f820a7954b82e5c9325e1e088a4fda098f36", + "revisionTime": "2017-03-30T05:43:03Z" + }, + { + "checksumSHA1": "KG+XZAbxdkpBm3Fa3bJ3Ylq8CKI=", + "path": "golang.org/x/text/unicode/bidi", + "revision": "65f4f820a7954b82e5c9325e1e088a4fda098f36", + "revisionTime": "2017-03-30T05:43:03Z" + }, + { + "checksumSHA1": "ZbYsJjfj1rPbHN+0baD1rg09PXQ=", + "path": "golang.org/x/text/unicode/cldr", + "revision": "65f4f820a7954b82e5c9325e1e088a4fda098f36", + "revisionTime": "2017-03-30T05:43:03Z" + }, + { + "checksumSHA1": "gYoNrZgxCQAHutg2rGHcFoKJtpA=", + "path": "golang.org/x/text/unicode/norm", + "revision": "65f4f820a7954b82e5c9325e1e088a4fda098f36", + "revisionTime": "2017-03-30T05:43:03Z" + }, + { + "checksumSHA1": "5R2IZ5umPfkD5QKt3pwrbIgmrDk=", + "path": "golang.org/x/text/unicode/rangetable", + "revision": "65f4f820a7954b82e5c9325e1e088a4fda098f36", + "revisionTime": "2017-03-30T05:43:03Z" }, { "checksumSHA1": "C7k1pbU/WU4CBoBwA4EBUnV/iek=", "path": "google.golang.org/api/gensupport", - "revision": "64485db7e8c8be51e572801d06cdbcfadd3546c1", - "revisionTime": "2017-02-23T23:41:36Z" + "revision": "48e49d1645e228d1c50c3d54fb476b2224477303", + "revisionTime": "2017-03-27T17:36:32Z" }, { "checksumSHA1": "BWKmb7kGYbfbvXO6E7tCpTh9zKE=", "path": "google.golang.org/api/googleapi", - "revision": "64485db7e8c8be51e572801d06cdbcfadd3546c1", - "revisionTime": "2017-02-23T23:41:36Z" + "revision": "48e49d1645e228d1c50c3d54fb476b2224477303", + "revisionTime": "2017-03-27T17:36:32Z" }, { "checksumSHA1": "1K0JxrUfDqAB3MyRiU1LKjfHyf4=", "path": "google.golang.org/api/googleapi/internal/uritemplates", - "revision": "64485db7e8c8be51e572801d06cdbcfadd3546c1", - "revisionTime": "2017-02-23T23:41:36Z" + "revision": "48e49d1645e228d1c50c3d54fb476b2224477303", + "revisionTime": "2017-03-27T17:36:32Z" }, { "checksumSHA1": "Mr2fXhMRzlQCgANFm91s536pG7E=", "path": "google.golang.org/api/googleapi/transport", - "revision": "64485db7e8c8be51e572801d06cdbcfadd3546c1", - "revisionTime": "2017-02-23T23:41:36Z" + "revision": "48e49d1645e228d1c50c3d54fb476b2224477303", + "revisionTime": "2017-03-27T17:36:32Z" }, { "checksumSHA1": "GAKy8Id2Qx7BI0kZPRjGn1RjVQo=", "path": "google.golang.org/api/internal", - "revision": "64485db7e8c8be51e572801d06cdbcfadd3546c1", - "revisionTime": "2017-02-23T23:41:36Z" + "revision": "48e49d1645e228d1c50c3d54fb476b2224477303", + "revisionTime": "2017-03-27T17:36:32Z" }, { "checksumSHA1": "slcGOTGSdukEPPSN81Q5WZGmhog=", "path": "google.golang.org/api/iterator", - "revision": "64485db7e8c8be51e572801d06cdbcfadd3546c1", - "revisionTime": "2017-02-23T23:41:36Z" + "revision": "48e49d1645e228d1c50c3d54fb476b2224477303", + "revisionTime": "2017-03-27T17:36:32Z" }, { "checksumSHA1": "hZ9zds+/FPwSGEiti5lGaZL3e6w=", "path": "google.golang.org/api/option", - "revision": "64485db7e8c8be51e572801d06cdbcfadd3546c1", - "revisionTime": "2017-02-23T23:41:36Z" + "revision": "48e49d1645e228d1c50c3d54fb476b2224477303", + "revisionTime": "2017-03-27T17:36:32Z" }, { - "checksumSHA1": "fxbPUw1XvbSMz6Atx6OOMkRdJ0g=", + "checksumSHA1": "yIPzm3VYyxZPSpzH3atwT4e07Dg=", "path": "google.golang.org/api/storage/v1", - "revision": "64485db7e8c8be51e572801d06cdbcfadd3546c1", - "revisionTime": "2017-02-23T23:41:36Z" + "revision": "48e49d1645e228d1c50c3d54fb476b2224477303", + "revisionTime": "2017-03-27T17:36:32Z" }, { - "checksumSHA1": "VRcIlsL2liHgqTdy5ad7gqdARHU=", + "checksumSHA1": "0RKN6+swqO9e59gDtaIx1kkt8no=", "path": "google.golang.org/api/transport", - "revision": "64485db7e8c8be51e572801d06cdbcfadd3546c1", - "revisionTime": "2017-02-23T23:41:36Z" + "revision": "48e49d1645e228d1c50c3d54fb476b2224477303", + "revisionTime": "2017-03-27T17:36:32Z" }, { - "checksumSHA1": "8K4KAebYh3WnPh0swPzOgl1pRD0=", + "checksumSHA1": "WPEbk80NB3Esdh4Yk0PXr2K7xVU=", "path": "google.golang.org/appengine", - "revision": "3a452f9e00122ead39586d68ffdb9c6e1326af3c", - "revisionTime": "2017-02-22T22:47:31Z" + "revision": "56d253d1dd14aa01937e12c73a0971bcfd797ff2", + "revisionTime": "2017-03-21T22:24:30Z" }, { "checksumSHA1": "R8rc2A/LgT4IRS6TzUZfhkUVQzQ=", "path": "google.golang.org/appengine/internal", - "revision": "3a452f9e00122ead39586d68ffdb9c6e1326af3c", - "revisionTime": "2017-02-22T22:47:31Z" + "revision": "56d253d1dd14aa01937e12c73a0971bcfd797ff2", + "revisionTime": "2017-03-21T22:24:30Z" }, { "checksumSHA1": "x6Thdfyasqd68dWZWqzWWeIfAfI=", "path": "google.golang.org/appengine/internal/app_identity", - "revision": "3a452f9e00122ead39586d68ffdb9c6e1326af3c", - "revisionTime": "2017-02-22T22:47:31Z" + "revision": "56d253d1dd14aa01937e12c73a0971bcfd797ff2", + "revisionTime": "2017-03-21T22:24:30Z" }, { "checksumSHA1": "TsNO8P0xUlLNyh3Ic/tzSp/fDWM=", "path": "google.golang.org/appengine/internal/base", - "revision": "3a452f9e00122ead39586d68ffdb9c6e1326af3c", - "revisionTime": "2017-02-22T22:47:31Z" + "revision": "56d253d1dd14aa01937e12c73a0971bcfd797ff2", + "revisionTime": "2017-03-21T22:24:30Z" }, { "checksumSHA1": "5QsV5oLGSfKZqTCVXP6NRz5T4Tw=", "path": "google.golang.org/appengine/internal/datastore", - "revision": "3a452f9e00122ead39586d68ffdb9c6e1326af3c", - "revisionTime": "2017-02-22T22:47:31Z" + "revision": "56d253d1dd14aa01937e12c73a0971bcfd797ff2", + "revisionTime": "2017-03-21T22:24:30Z" }, { "checksumSHA1": "Gep2T9zmVYV8qZfK2gu3zrmG6QE=", "path": "google.golang.org/appengine/internal/log", - "revision": "3a452f9e00122ead39586d68ffdb9c6e1326af3c", - "revisionTime": "2017-02-22T22:47:31Z" + "revision": "56d253d1dd14aa01937e12c73a0971bcfd797ff2", + "revisionTime": "2017-03-21T22:24:30Z" }, { "checksumSHA1": "eLZVX1EHLclFtQnjDIszsdyWRHo=", "path": "google.golang.org/appengine/internal/modules", - "revision": "3a452f9e00122ead39586d68ffdb9c6e1326af3c", - "revisionTime": "2017-02-22T22:47:31Z" + "revision": "56d253d1dd14aa01937e12c73a0971bcfd797ff2", + "revisionTime": "2017-03-21T22:24:30Z" }, { "checksumSHA1": "a1XY7rz3BieOVqVI2Et6rKiwQCk=", "path": "google.golang.org/appengine/internal/remote_api", - "revision": "3a452f9e00122ead39586d68ffdb9c6e1326af3c", - "revisionTime": "2017-02-22T22:47:31Z" + "revision": "56d253d1dd14aa01937e12c73a0971bcfd797ff2", + "revisionTime": "2017-03-21T22:24:30Z" }, { "checksumSHA1": "VA88sOHmVuIslrbHaWx9yEvjGjM=", "path": "google.golang.org/appengine/internal/socket", - "revision": "3a452f9e00122ead39586d68ffdb9c6e1326af3c", - "revisionTime": "2017-02-22T22:47:31Z" + "revision": "56d253d1dd14aa01937e12c73a0971bcfd797ff2", + "revisionTime": "2017-03-21T22:24:30Z" }, { "checksumSHA1": "QtAbHtHmDzcf6vOV9eqlCpKgjiw=", "path": "google.golang.org/appengine/internal/urlfetch", - "revision": "3a452f9e00122ead39586d68ffdb9c6e1326af3c", - "revisionTime": "2017-02-22T22:47:31Z" + "revision": "56d253d1dd14aa01937e12c73a0971bcfd797ff2", + "revisionTime": "2017-03-21T22:24:30Z" }, { "checksumSHA1": "MharNMGnQusRPdmBYXDxz2cCHPU=", "path": "google.golang.org/appengine/socket", - "revision": "3a452f9e00122ead39586d68ffdb9c6e1326af3c", - "revisionTime": "2017-02-22T22:47:31Z" + "revision": "56d253d1dd14aa01937e12c73a0971bcfd797ff2", + "revisionTime": "2017-03-21T22:24:30Z" }, { "checksumSHA1": "akOV9pYnCbcPA8wJUutSQVibdyg=", "path": "google.golang.org/appengine/urlfetch", - "revision": "3a452f9e00122ead39586d68ffdb9c6e1326af3c", - "revisionTime": "2017-02-22T22:47:31Z" + "revision": "56d253d1dd14aa01937e12c73a0971bcfd797ff2", + "revisionTime": "2017-03-21T22:24:30Z" }, { - "checksumSHA1": "8Z43m+zNTbxHUHQPMUNirEp1BLg=", + "checksumSHA1": "cT33dN5Yu3TUNLlp1WO08MdmLlc=", "path": "google.golang.org/grpc", - "revision": "34384f34de585705f1a6783a158d2ec8af29f618", - "revisionTime": "2017-02-24T00:04:25Z" + "revision": "7fc29d0caa400efc53212b222356f99efe87d7d5", + "revisionTime": "2017-03-28T19:10:54Z" }, { "checksumSHA1": "08icuA15HRkdYCt6H+Cs90RPQsY=", "path": "google.golang.org/grpc/codes", - "revision": "34384f34de585705f1a6783a158d2ec8af29f618", - "revisionTime": "2017-02-24T00:04:25Z" + "revision": "7fc29d0caa400efc53212b222356f99efe87d7d5", + "revisionTime": "2017-03-28T19:10:54Z" }, { - "checksumSHA1": "GHCDufYjDX7weDH2udXY1pkyvEo=", + "checksumSHA1": "K99T+YYvCBu0O1I3zuRcGhM5ADY=", "path": "google.golang.org/grpc/credentials", - "revision": "34384f34de585705f1a6783a158d2ec8af29f618", - "revisionTime": "2017-02-24T00:04:25Z" + "revision": "7fc29d0caa400efc53212b222356f99efe87d7d5", + "revisionTime": "2017-03-28T19:10:54Z" }, { "checksumSHA1": "bg3wIPzajKt3QZfTG70EPaxDtpk=", "path": "google.golang.org/grpc/credentials/oauth", - "revision": "34384f34de585705f1a6783a158d2ec8af29f618", - "revisionTime": "2017-02-24T00:04:25Z" + "revision": "7fc29d0caa400efc53212b222356f99efe87d7d5", + "revisionTime": "2017-03-28T19:10:54Z" }, { "checksumSHA1": "3Lt5hNAG8qJAYSsNghR5uA1zQns=", "path": "google.golang.org/grpc/grpclog", - "revision": "34384f34de585705f1a6783a158d2ec8af29f618", - "revisionTime": "2017-02-24T00:04:25Z" + "revision": "7fc29d0caa400efc53212b222356f99efe87d7d5", + "revisionTime": "2017-03-28T19:10:54Z" }, { "checksumSHA1": "T3Q0p8kzvXFnRkMaK/G8mCv6mc0=", "path": "google.golang.org/grpc/internal", - "revision": "34384f34de585705f1a6783a158d2ec8af29f618", - "revisionTime": "2017-02-24T00:04:25Z" + "revision": "7fc29d0caa400efc53212b222356f99efe87d7d5", + "revisionTime": "2017-03-28T19:10:54Z" + }, + { + "checksumSHA1": "NnarQ6O9NqkBg2g6jc1PkciTpnY=", + "path": "google.golang.org/grpc/keepalive", + "revision": "7fc29d0caa400efc53212b222356f99efe87d7d5", + "revisionTime": "2017-03-28T19:10:54Z" }, { "checksumSHA1": "T05Mzg3hEv2Vxao9hZn0Kv+nwUQ=", "path": "google.golang.org/grpc/metadata", - "revision": "34384f34de585705f1a6783a158d2ec8af29f618", - "revisionTime": "2017-02-24T00:04:25Z" + "revision": "7fc29d0caa400efc53212b222356f99efe87d7d5", + "revisionTime": "2017-03-28T19:10:54Z" }, { "checksumSHA1": "4GSUFhOQ0kdFlBH4D5OTeKy78z0=", "path": "google.golang.org/grpc/naming", - "revision": "34384f34de585705f1a6783a158d2ec8af29f618", - "revisionTime": "2017-02-24T00:04:25Z" + "revision": "7fc29d0caa400efc53212b222356f99efe87d7d5", + "revisionTime": "2017-03-28T19:10:54Z" }, { "checksumSHA1": "3RRoLeH6X2//7tVClOVzxW2bY+E=", "path": "google.golang.org/grpc/peer", - "revision": "34384f34de585705f1a6783a158d2ec8af29f618", - "revisionTime": "2017-02-24T00:04:25Z" + "revision": "7fc29d0caa400efc53212b222356f99efe87d7d5", + "revisionTime": "2017-03-28T19:10:54Z" }, { "checksumSHA1": "wzkOAxlah+y75EpH0QVgzb8hdfc=", "path": "google.golang.org/grpc/stats", - "revision": "34384f34de585705f1a6783a158d2ec8af29f618", - "revisionTime": "2017-02-24T00:04:25Z" + "revision": "7fc29d0caa400efc53212b222356f99efe87d7d5", + "revisionTime": "2017-03-28T19:10:54Z" }, { "checksumSHA1": "N0TftT6/CyWqp6VRi2DqDx60+Fo=", "path": "google.golang.org/grpc/tap", - "revision": "34384f34de585705f1a6783a158d2ec8af29f618", - "revisionTime": "2017-02-24T00:04:25Z" + "revision": "7fc29d0caa400efc53212b222356f99efe87d7d5", + "revisionTime": "2017-03-28T19:10:54Z" }, { - "checksumSHA1": "b9RDUPOmWBfHe9cM8UVilfyF/vE=", + "checksumSHA1": "bOEEMNqZp+DkHYfapMsEvVVxoSw=", "path": "google.golang.org/grpc/transport", - "revision": "34384f34de585705f1a6783a158d2ec8af29f618", - "revisionTime": "2017-02-24T00:04:25Z" + "revision": "7fc29d0caa400efc53212b222356f99efe87d7d5", + "revisionTime": "2017-03-28T19:10:54Z" }, { "checksumSHA1": "wSu8owMAP7GixsYoSZ4CmKUVhnU=", @@ -1581,4 +1610,4 @@ } ], "rootPath": "github.com/hashicorp/vault" -} +} \ No newline at end of file From 6e63357adb3bb0505401bdc2d32f85de17c89763 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Wed, 12 Apr 2017 12:29:06 -0700 Subject: [PATCH 29/43] Readded otp and barcode libraries to vendor.json --- vendor/vendor.json | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/vendor/vendor.json b/vendor/vendor.json index b16d636b0de2..083fdee3bf49 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -324,6 +324,18 @@ "revision": "675b82c74c0ed12283ee81ba8a534c8982c07b85", "revisionTime": "2016-10-13T10:26:35Z" }, + { + "checksumSHA1": "tLl952GRIVsso2Pk/IH3cMJaK8E=", + "path": "github.com/boombuler/barcode", + "revision": "9fb68fa6ca3535187c2a32f11a25e8d58f294bed", + "revisionTime": "2017-04-12T13:03:35Z" + }, + { + "checksumSHA1": "tbwzn+sWiZv6veyXae3qRfTjlcQ=", + "path": "github.com/boombuler/barcode/qr", + "revision": "9fb68fa6ca3535187c2a32f11a25e8d58f294bed", + "revisionTime": "2017-04-12T13:03:35Z" + }, { "checksumSHA1": "gX06B03sIRw/1yCms1kMwKX8krE=", "path": "github.com/cenk/backoff", @@ -1104,6 +1116,30 @@ "revision": "ff09b135c25aae272398c51a07235b90a75aa4f0", "revisionTime": "2017-03-16T20:15:38Z" }, + { + "checksumSHA1": "woY3inKe+d7B1jPTFxVKNCCFH9c=", + "path": "github.com/pquerna/otp", + "revision": "9e1935374bc73ffe011187dafed51a412b90fe43", + "revisionTime": "2017-02-23T01:06:52Z" + }, + { + "checksumSHA1": "5xpnYLhCOqNnsgykOk85MnTqVu0=", + "path": "github.com/pquerna/otp/example", + "revision": "9e1935374bc73ffe011187dafed51a412b90fe43", + "revisionTime": "2017-02-23T01:06:52Z" + }, + { + "checksumSHA1": "xo32aXW4ZXXRHJ/9E6m10vXJZAo=", + "path": "github.com/pquerna/otp/hotp", + "revision": "9e1935374bc73ffe011187dafed51a412b90fe43", + "revisionTime": "2017-02-23T01:06:52Z" + }, + { + "checksumSHA1": "Ie55pTQw1rnOZ8KDekSDXUWDT1I=", + "path": "github.com/pquerna/otp/totp", + "revision": "9e1935374bc73ffe011187dafed51a412b90fe43", + "revisionTime": "2017-02-23T01:06:52Z" + }, { "checksumSHA1": "ZOhewV1DsQjTYlx8a+ifrZki2Vg=", "path": "github.com/ryanuber/columnize", From 0073a9e725a160daa271766f32fae5f16ace8fc1 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Wed, 12 Apr 2017 12:58:25 -0700 Subject: [PATCH 30/43] Modified help strings for path_role_create.go --- builtin/logical/totp/path_role_create.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/logical/totp/path_role_create.go b/builtin/logical/totp/path_role_create.go index 4ee65c2c0b18..45979f5ee90a 100644 --- a/builtin/logical/totp/path_role_create.go +++ b/builtin/logical/totp/path_role_create.go @@ -109,9 +109,9 @@ func (b *backend) pathValidateCode( } const pathRoleCreateReadHelpSyn = ` -Request time-based one-time use password for a certain role. +Request time-based one-time use password or validate a password for a certain role . ` const pathRoleCreateReadHelpDesc = ` -This path generates a time-based one-time use password for a certain role. +This path generates and validates time-based one-time use passwords for a certain role. ` From a5aeda5360365020b753f3d71723908a6fb765cb Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Wed, 12 Apr 2017 20:22:47 -0700 Subject: [PATCH 31/43] Fixed test issue in testAccStepReadRole --- builtin/logical/totp/backend_test.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/builtin/logical/totp/backend_test.go b/builtin/logical/totp/backend_test.go index d85403060de1..19ecdb29092e 100644 --- a/builtin/logical/totp/backend_test.go +++ b/builtin/logical/totp/backend_test.go @@ -432,17 +432,27 @@ func testAccStepReadRole(t *testing.T, name string, expected map[string]interfac } var d struct { - Issuer string `mapstructure:"issuer"` - AccountName string `mapstructure:"account_name"` - Period uint `mapstructure:"period"` - Algorithm otplib.Algorithm `mapstructure:"algorithm"` - Digits otplib.Digits `mapstructure:"digits"` + Issuer string `mapstructure:"issuer"` + AccountName string `mapstructure:"account_name"` + Period uint `mapstructure:"period"` + Algorithm string `mapstructure:"algorithm"` + Digits otplib.Digits `mapstructure:"digits"` } if err := mapstructure.Decode(resp.Data, &d); err != nil { return err } + var role_algorithm otplib.Algorithm + switch d.Algorithm { + case "SHA1": + role_algorithm = otplib.AlgorithmSHA1 + case "SHA256": + role_algorithm = otplib.AlgorithmSHA256 + case "SHA512": + role_algorithm = otplib.AlgorithmSHA512 + } + period := expected["period"].(int) switch { @@ -452,7 +462,7 @@ func testAccStepReadRole(t *testing.T, name string, expected map[string]interfac return fmt.Errorf("Account_Name should equal: %s", expected["account_name"]) case d.Period != uint(period): return fmt.Errorf("Period should equal: %i", expected["period"]) - case d.Algorithm != expected["algorithm"]: + case role_algorithm != expected["algorithm"]: return fmt.Errorf("Algorithm should equal: %s", expected["algorithm"]) case d.Digits != expected["digits"]: return fmt.Errorf("Digits should equal: %i", expected["digits"]) From 2c696814a836a19beb8a2f736e4dc0ac5bc5e1b5 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sat, 15 Apr 2017 15:41:36 -0700 Subject: [PATCH 32/43] Cleaned up error formatting, variable names and path names. Also added some additional documentation --- builtin/logical/totp/backend.go | 6 +- builtin/logical/totp/backend_test.go | 112 ++++++------ .../{path_role_create.go => path_code.go} | 44 ++--- .../totp/{path_roles.go => path_keys.go} | 172 +++++++++--------- 4 files changed, 167 insertions(+), 167 deletions(-) rename builtin/logical/totp/{path_role_create.go => path_code.go} (70%) rename builtin/logical/totp/{path_roles.go => path_keys.go} (59%) diff --git a/builtin/logical/totp/backend.go b/builtin/logical/totp/backend.go index 940d6bc92438..889863652302 100644 --- a/builtin/logical/totp/backend.go +++ b/builtin/logical/totp/backend.go @@ -19,9 +19,9 @@ func Backend(conf *logical.BackendConfig) *backend { Help: strings.TrimSpace(backendHelp), Paths: []*framework.Path{ - pathListRoles(&b), - pathRoles(&b), - pathRoleCreate(&b), + pathListKeys(&b), + pathKeys(&b), + pathCode(&b), }, Secrets: []*framework.Secret{}, diff --git a/builtin/logical/totp/backend_test.go b/builtin/logical/totp/backend_test.go index 19ecdb29092e..c71a99d3c795 100644 --- a/builtin/logical/totp/backend_test.go +++ b/builtin/logical/totp/backend_test.go @@ -36,7 +36,7 @@ func TestBackend_readCredentialsDefaultValues(t *testing.T) { // Generate a new shared key key, _ := createKey() - roleData := map[string]interface{}{ + keyData := map[string]interface{}{ "key": key, } @@ -52,8 +52,8 @@ func TestBackend_readCredentialsDefaultValues(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ - testAccStepCreateRole(t, "test", roleData, false), - testAccStepReadRole(t, "test", expected), + testAccStepCreateKey(t, "test", keyData, false), + testAccStepReadKey(t, "test", expected), testAccStepReadCreds(t, b, config.StorageView, "test", expected), }, }) @@ -70,7 +70,7 @@ func TestBackend_readCredentialsEightDigitsThirtySecondPeriod(t *testing.T) { // Generate a new shared key key, _ := createKey() - roleData := map[string]interface{}{ + keyData := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", "key": key, @@ -89,8 +89,8 @@ func TestBackend_readCredentialsEightDigitsThirtySecondPeriod(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ - testAccStepCreateRole(t, "test", roleData, false), - testAccStepReadRole(t, "test", expected), + testAccStepCreateKey(t, "test", keyData, false), + testAccStepReadKey(t, "test", expected), testAccStepReadCreds(t, b, config.StorageView, "test", expected), }, }) @@ -107,7 +107,7 @@ func TestBackend_readCredentialsSixDigitsNinetySecondPeriod(t *testing.T) { // Generate a new shared key key, _ := createKey() - roleData := map[string]interface{}{ + keyData := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", "key": key, @@ -126,8 +126,8 @@ func TestBackend_readCredentialsSixDigitsNinetySecondPeriod(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ - testAccStepCreateRole(t, "test", roleData, false), - testAccStepReadRole(t, "test", expected), + testAccStepCreateKey(t, "test", keyData, false), + testAccStepReadKey(t, "test", expected), testAccStepReadCreds(t, b, config.StorageView, "test", expected), }, }) @@ -144,7 +144,7 @@ func TestBackend_readCredentialsSHA256(t *testing.T) { // Generate a new shared key key, _ := createKey() - roleData := map[string]interface{}{ + keyData := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", "key": key, @@ -163,8 +163,8 @@ func TestBackend_readCredentialsSHA256(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ - testAccStepCreateRole(t, "test", roleData, false), - testAccStepReadRole(t, "test", expected), + testAccStepCreateKey(t, "test", keyData, false), + testAccStepReadKey(t, "test", expected), testAccStepReadCreds(t, b, config.StorageView, "test", expected), }, }) @@ -181,7 +181,7 @@ func TestBackend_readCredentialsSHA512(t *testing.T) { // Generate a new shared key key, _ := createKey() - roleData := map[string]interface{}{ + keyData := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", "key": key, @@ -200,14 +200,14 @@ func TestBackend_readCredentialsSHA512(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ - testAccStepCreateRole(t, "test", roleData, false), - testAccStepReadRole(t, "test", expected), + testAccStepCreateKey(t, "test", keyData, false), + testAccStepReadKey(t, "test", expected), testAccStepReadCreds(t, b, config.StorageView, "test", expected), }, }) } -func TestBackend_roleCrudDefaultValues(t *testing.T) { +func TestBackend_keyCrudDefaultValues(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(config) @@ -217,7 +217,7 @@ func TestBackend_roleCrudDefaultValues(t *testing.T) { key, _ := createKey() - roleData := map[string]interface{}{ + keyData := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", "key": key, @@ -235,15 +235,15 @@ func TestBackend_roleCrudDefaultValues(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ - testAccStepCreateRole(t, "test", roleData, false), - testAccStepReadRole(t, "test", expected), - testAccStepDeleteRole(t, "test"), - testAccStepReadRole(t, "test", nil), + testAccStepCreateKey(t, "test", keyData, false), + testAccStepReadKey(t, "test", expected), + testAccStepDeleteKey(t, "test"), + testAccStepReadKey(t, "test", nil), }, }) } -func TestBackend_createRoleMissingKey(t *testing.T) { +func TestBackend_createKeyMissingKeyValue(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(config) @@ -251,7 +251,7 @@ func TestBackend_createRoleMissingKey(t *testing.T) { t.Fatal(err) } - roleData := map[string]interface{}{ + keyData := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", } @@ -259,13 +259,13 @@ func TestBackend_createRoleMissingKey(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ - testAccStepCreateRole(t, "test", roleData, true), - testAccStepReadRole(t, "test", nil), + testAccStepCreateKey(t, "test", keyData, true), + testAccStepReadKey(t, "test", nil), }, }) } -func TestBackend_createRoleInvalidKey(t *testing.T) { +func TestBackend_createKeyInvalidKeyValue(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(config) @@ -273,7 +273,7 @@ func TestBackend_createRoleInvalidKey(t *testing.T) { t.Fatal(err) } - roleData := map[string]interface{}{ + keyData := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", "key": "1", @@ -282,13 +282,13 @@ func TestBackend_createRoleInvalidKey(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ - testAccStepCreateRole(t, "test", roleData, true), - testAccStepReadRole(t, "test", nil), + testAccStepCreateKey(t, "test", keyData, true), + testAccStepReadKey(t, "test", nil), }, }) } -func TestBackend_createRoleInvalidAlgorithm(t *testing.T) { +func TestBackend_createKeyInvalidAlgorithm(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(config) @@ -299,7 +299,7 @@ func TestBackend_createRoleInvalidAlgorithm(t *testing.T) { // Generate a new shared key key, _ := createKey() - roleData := map[string]interface{}{ + keyData := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", "key": key, @@ -309,13 +309,13 @@ func TestBackend_createRoleInvalidAlgorithm(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ - testAccStepCreateRole(t, "test", roleData, true), - testAccStepReadRole(t, "test", nil), + testAccStepCreateKey(t, "test", keyData, true), + testAccStepReadKey(t, "test", nil), }, }) } -func TestBackend_createRoleInvalidPeriod(t *testing.T) { +func TestBackend_createKeyInvalidPeriod(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(config) @@ -326,7 +326,7 @@ func TestBackend_createRoleInvalidPeriod(t *testing.T) { // Generate a new shared key key, _ := createKey() - roleData := map[string]interface{}{ + keyData := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", "key": key, @@ -336,13 +336,13 @@ func TestBackend_createRoleInvalidPeriod(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ - testAccStepCreateRole(t, "test", roleData, true), - testAccStepReadRole(t, "test", nil), + testAccStepCreateKey(t, "test", keyData, true), + testAccStepReadKey(t, "test", nil), }, }) } -func TestBackend_createRoleInvalidDigits(t *testing.T) { +func TestBackend_createKeyInvalidDigits(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(config) @@ -353,7 +353,7 @@ func TestBackend_createRoleInvalidDigits(t *testing.T) { // Generate a new shared key key, _ := createKey() - roleData := map[string]interface{}{ + keyData := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", "key": key, @@ -363,22 +363,22 @@ func TestBackend_createRoleInvalidDigits(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ - testAccStepCreateRole(t, "test", roleData, true), - testAccStepReadRole(t, "test", nil), + testAccStepCreateKey(t, "test", keyData, true), + testAccStepReadKey(t, "test", nil), }, }) } -func testAccStepCreateRole(t *testing.T, name string, roleData map[string]interface{}, expectFail bool) logicaltest.TestStep { +func testAccStepCreateKey(t *testing.T, name string, keyData map[string]interface{}, expectFail bool) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: path.Join("keys", name), - Data: roleData, + Data: keyData, ErrorOk: expectFail, } } -func testAccStepDeleteRole(t *testing.T, name string) logicaltest.TestStep { +func testAccStepDeleteKey(t *testing.T, name string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.DeleteOperation, Path: path.Join("keys", name), @@ -411,7 +411,7 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, na }) if !valid { - t.Fatalf("Generated code isn't valid.") + t.Fatalf("generated code isn't valid") } return nil @@ -419,7 +419,7 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, na } } -func testAccStepReadRole(t *testing.T, name string, expected map[string]interface{}) logicaltest.TestStep { +func testAccStepReadKey(t *testing.T, name string, expected map[string]interface{}) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.ReadOperation, Path: "keys/" + name, @@ -443,29 +443,29 @@ func testAccStepReadRole(t *testing.T, name string, expected map[string]interfac return err } - var role_algorithm otplib.Algorithm + var keyAlgorithm otplib.Algorithm switch d.Algorithm { case "SHA1": - role_algorithm = otplib.AlgorithmSHA1 + keyAlgorithm = otplib.AlgorithmSHA1 case "SHA256": - role_algorithm = otplib.AlgorithmSHA256 + keyAlgorithm = otplib.AlgorithmSHA256 case "SHA512": - role_algorithm = otplib.AlgorithmSHA512 + keyAlgorithm = otplib.AlgorithmSHA512 } period := expected["period"].(int) switch { case d.Issuer != expected["issuer"]: - return fmt.Errorf("Issuer should equal: %s", expected["issuer"]) + return fmt.Errorf("issuer should equal: %s", expected["issuer"]) case d.AccountName != expected["account_name"]: - return fmt.Errorf("Account_Name should equal: %s", expected["account_name"]) + return fmt.Errorf("ccount_Name should equal: %s", expected["account_name"]) case d.Period != uint(period): - return fmt.Errorf("Period should equal: %i", expected["period"]) - case role_algorithm != expected["algorithm"]: - return fmt.Errorf("Algorithm should equal: %s", expected["algorithm"]) + return fmt.Errorf("period should equal: %i", expected["period"]) + case keyAlgorithm != expected["algorithm"]: + return fmt.Errorf("algorithm should equal: %s", expected["algorithm"]) case d.Digits != expected["digits"]: - return fmt.Errorf("Digits should equal: %i", expected["digits"]) + return fmt.Errorf("digits should equal: %i", expected["digits"]) } return nil diff --git a/builtin/logical/totp/path_role_create.go b/builtin/logical/totp/path_code.go similarity index 70% rename from builtin/logical/totp/path_role_create.go rename to builtin/logical/totp/path_code.go index 45979f5ee90a..ffe8227fac52 100644 --- a/builtin/logical/totp/path_role_create.go +++ b/builtin/logical/totp/path_code.go @@ -9,13 +9,13 @@ import ( totplib "github.com/pquerna/otp/totp" ) -func pathRoleCreate(b *backend) *framework.Path { +func pathCode(b *backend) *framework.Path { return &framework.Path{ Pattern: "code/" + framework.GenericNameRegex("name"), Fields: map[string]*framework.FieldSchema{ "name": &framework.FieldSchema{ Type: framework.TypeString, - Description: "Name of the role.", + Description: "Name of the key.", }, "code": &framework.FieldSchema{ Type: framework.TypeString, @@ -28,8 +28,8 @@ func pathRoleCreate(b *backend) *framework.Path { logical.UpdateOperation: b.pathValidateCode, }, - HelpSynopsis: pathRoleCreateReadHelpSyn, - HelpDescription: pathRoleCreateReadHelpDesc, + HelpSynopsis: pathCodeHelpSyn, + HelpDescription: pathCodeHelpDesc, } } @@ -42,19 +42,19 @@ func (b *backend) pathReadCode( // Get the key b.logger.Trace("totp/pathReadCode: getting key") - role, err := b.Role(req.Storage, name) + key, err := b.Key(req.Storage, name) if err != nil { return nil, err } - if role == nil { + if key == nil { return logical.ErrorResponse(fmt.Sprintf("unknown key: %s", name)), nil } // Generate password using totp library - totpToken, err := totplib.GenerateCodeCustom(role.Key, time.Now(), totplib.ValidateOpts{ - Period: role.Period, - Digits: role.Digits, - Algorithm: role.Algorithm, + totpToken, err := totplib.GenerateCodeCustom(key.Key, time.Now(), totplib.ValidateOpts{ + Period: key.Period, + Digits: key.Digits, + Algorithm: key.Algorithm, }) if err != nil { @@ -80,23 +80,23 @@ func (b *backend) pathValidateCode( // Enforce input value requirements if code == "" { - return logical.ErrorResponse("The code value is required."), nil + return logical.ErrorResponse("the code value is required"), nil } // Get the key's stored values - role, err := b.Role(req.Storage, name) + key, err := b.Key(req.Storage, name) if err != nil { return nil, err } - if role == nil { + if key == nil { return logical.ErrorResponse(fmt.Sprintf("unknown key: %s", name)), nil } - valid, err := totplib.ValidateCustom(code, role.Key, time.Now(), totplib.ValidateOpts{ - Period: role.Period, - Skew: role.Skew, - Digits: role.Digits, - Algorithm: role.Algorithm, + valid, err := totplib.ValidateCustom(code, key.Key, time.Now(), totplib.ValidateOpts{ + Period: key.Period, + Skew: key.Skew, + Digits: key.Digits, + Algorithm: key.Algorithm, }) resp, err := &logical.Response{ @@ -108,10 +108,10 @@ func (b *backend) pathValidateCode( return resp, nil } -const pathRoleCreateReadHelpSyn = ` -Request time-based one-time use password or validate a password for a certain role . +const pathCodeHelpSyn = ` +Request time-based one-time use password or validate a password for a certain key . ` -const pathRoleCreateReadHelpDesc = ` -This path generates and validates time-based one-time use passwords for a certain role. +const pathCodeHelpDesc = ` +This path generates and validates time-based one-time use passwords for a certain key. ` diff --git a/builtin/logical/totp/path_roles.go b/builtin/logical/totp/path_keys.go similarity index 59% rename from builtin/logical/totp/path_roles.go rename to builtin/logical/totp/path_keys.go index 9374d1d089c4..d3deda09141d 100644 --- a/builtin/logical/totp/path_roles.go +++ b/builtin/logical/totp/path_keys.go @@ -13,26 +13,26 @@ import ( totplib "github.com/pquerna/otp/totp" ) -func pathListRoles(b *backend) *framework.Path { +func pathListKeys(b *backend) *framework.Path { return &framework.Path{ Pattern: "keys/?$", Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ListOperation: b.pathRoleList, + logical.ListOperation: b.pathKeyList, }, - HelpSynopsis: pathRoleHelpSyn, - HelpDescription: pathRoleHelpDesc, + HelpSynopsis: pathKeyHelpSyn, + HelpDescription: pathKeyHelpDesc, } } -func pathRoles(b *backend) *framework.Path { +func pathKeys(b *backend) *framework.Path { return &framework.Path{ Pattern: "keys/" + framework.GenericNameRegex("name"), Fields: map[string]*framework.FieldSchema{ "name": { Type: framework.TypeString, - Description: "Name of the role.", + Description: "Name of the key.", }, "generate": { @@ -71,19 +71,19 @@ func pathRoles(b *backend) *framework.Path { "algorithm": { Type: framework.TypeString, Default: "SHA1", - Description: `The hashing algorithm used to generate the TOTP token.`, + Description: `The hashing algorithm used to generate the TOTP token. Options include SHA1, SHA256 and SHA512`, }, "digits": { Type: framework.TypeInt, Default: 6, - Description: `The number of digits in the generated TOTP token.`, + Description: `The number of digits in the generated TOTP token. This value can either be 6 or 8`, }, "skew": { Type: framework.TypeInt, Default: 0, - Description: `The number of delay periods that are allowed when validating a TOTP token.`, + Description: `The number of delay periods that are allowed when validating a TOTP token. This value can either be 0 or 1`, }, "qr_size": { @@ -94,17 +94,17 @@ func pathRoles(b *backend) *framework.Path { }, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.pathRoleRead, - logical.UpdateOperation: b.pathRoleCreate, - logical.DeleteOperation: b.pathRoleDelete, + logical.ReadOperation: b.pathKeyRead, + logical.UpdateOperation: b.pathKeyCreate, + logical.DeleteOperation: b.pathKeyDelete, }, - HelpSynopsis: pathRoleHelpSyn, - HelpDescription: pathRoleHelpDesc, + HelpSynopsis: pathKeyHelpSyn, + HelpDescription: pathKeyHelpDesc, } } -func (b *backend) Role(s logical.Storage, n string) (*roleEntry, error) { +func (b *backend) Key(s logical.Storage, n string) (*keyEntry, error) { entry, err := s.Get("key/" + n) if err != nil { return nil, err @@ -113,7 +113,7 @@ func (b *backend) Role(s logical.Storage, n string) (*roleEntry, error) { return nil, nil } - var result roleEntry + var result keyEntry if err := entry.DecodeJSON(&result); err != nil { return nil, err } @@ -121,7 +121,7 @@ func (b *backend) Role(s logical.Storage, n string) (*roleEntry, error) { return &result, nil } -func (b *backend) pathRoleDelete( +func (b *backend) pathKeyDelete( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { err := req.Storage.Delete("key/" + data.Get("name").(string)) if err != nil { @@ -131,28 +131,28 @@ func (b *backend) pathRoleDelete( return nil, nil } -func (b *backend) pathRoleRead( +func (b *backend) pathKeyRead( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - role, err := b.Role(req.Storage, data.Get("name").(string)) + key, err := b.Key(req.Storage, data.Get("name").(string)) if err != nil { return nil, err } - if role == nil { + if key == nil { return nil, nil } - switch role.Generate { + switch key.Generate { case true: - key_object, err := otplib.NewKeyFromURL(role.URL) + keyObject, err := otplib.NewKeyFromURL(key.URL) if err != nil { - return logical.ErrorResponse("An error occured while generating a Key object."), nil + return logical.ErrorResponse("an error occured while generating a Key object"), nil } - barcode, err := key_object.Image(role.QRSize, role.QRSize) + barcode, err := keyObject.Image(key.QRSize, key.QRSize) if err != nil { - return logical.ErrorResponse("An error occured while generating a QR code image."), nil + return logical.ErrorResponse("an error occured while generating a QR code image"), nil } var buff bytes.Buffer @@ -160,30 +160,30 @@ func (b *backend) pathRoleRead( b64Barcode := base64.StdEncoding.EncodeToString(buff.Bytes()) return &logical.Response{ Data: map[string]interface{}{ - "url": key_object.String(), + "url": keyObject.String(), "barcode": b64Barcode, - "skew": role.Skew, - "qr_size": role.QRSize, + "skew": key.Skew, + "qr_size": key.QRSize, }, }, nil default: // Translate algorithm back to string - algorithm := role.Algorithm.String() + algorithm := key.Algorithm.String() // Return values of key return &logical.Response{ Data: map[string]interface{}{ - "issuer": role.Issuer, - "account_name": role.AccountName, - "period": role.Period, + "issuer": key.Issuer, + "account_name": key.AccountName, + "period": key.Period, "algorithm": algorithm, - "digits": role.Digits, + "digits": key.Digits, }, }, nil } } -func (b *backend) pathRoleList( +func (b *backend) pathKeyList( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { entries, err := req.Storage.List("key/") if err != nil { @@ -193,119 +193,119 @@ func (b *backend) pathRoleList( return logical.ListResponse(entries), nil } -func (b *backend) pathRoleCreate( +func (b *backend) pathKeyCreate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { name := data.Get("name").(string) generate := data.Get("generate").(bool) - key_string := data.Get("key").(string) + keyString := data.Get("key").(string) issuer := data.Get("issuer").(string) - account_name := data.Get("account_name").(string) + accountName := data.Get("account_name").(string) period := data.Get("period").(int) algorithm := data.Get("algorithm").(string) digits := data.Get("digits").(int) skew := data.Get("skew").(int) - qr_size := data.Get("qr_size").(int) - key_size := data.Get("key_size").(int) + qrSize := data.Get("qr_size").(int) + keySize := data.Get("key_size").(int) // Translate digits and algorithm to a format the totp library understands - var role_digits otplib.Digits + var keyDigits otplib.Digits switch digits { case 6: - role_digits = otplib.DigitsSix + keyDigits = otplib.DigitsSix case 8: - role_digits = otplib.DigitsEight + keyDigits = otplib.DigitsEight default: - return logical.ErrorResponse("The digit value can only be 6 or 8."), nil + return logical.ErrorResponse("the digit value can only be 6 or 8"), nil } - var role_algorithm otplib.Algorithm + var keyAlgorithm otplib.Algorithm switch algorithm { case "SHA1": - role_algorithm = otplib.AlgorithmSHA1 + keyAlgorithm = otplib.AlgorithmSHA1 case "SHA256": - role_algorithm = otplib.AlgorithmSHA256 + keyAlgorithm = otplib.AlgorithmSHA256 case "SHA512": - role_algorithm = otplib.AlgorithmSHA512 + keyAlgorithm = otplib.AlgorithmSHA512 default: - return logical.ErrorResponse("The algorithm value is not valid."), nil + return logical.ErrorResponse("the algorithm value is not valid"), nil } // Enforce input value requirements if period <= 0 { - return logical.ErrorResponse("The period value must be greater than zero."), nil + return logical.ErrorResponse("the period value must be greater than zero"), nil } if skew < 0 { - return logical.ErrorResponse("The skew value must be greater than zero."), nil + return logical.ErrorResponse("the skew value must be greater than zero"), nil } - if qr_size <= 0 { - return logical.ErrorResponse("The qr_size value must be greater than zero."), nil + if qrSize <= 0 { + return logical.ErrorResponse("the qr_size value must be greater than zero"), nil } - if key_size <= 0 { - return logical.ErrorResponse("The key_size value must be greater than zero."), nil + if keySize <= 0 { + return logical.ErrorResponse("the key_size value must be greater than zero"), nil } // If the key is generated, Account Name and Issuer are required. if generate { - if account_name == "" { - return logical.ErrorResponse("The account_name value is required for generated keys."), nil + if accountName == "" { + return logical.ErrorResponse("the account_name value is required for generated keys"), nil } if issuer == "" { - return logical.ErrorResponse("The issuer value is required for generated keys."), nil + return logical.ErrorResponse("the issuer value is required for generated keys"), nil } } // Period, Skew and Key Size need to be unsigned ints - uint_period := uint(period) - uint_skew := uint(skew) - uint_key_size := uint(key_size) + uintPeriod := uint(period) + uintSkew := uint(skew) + uintKeySize := uint(keySize) url := "" switch generate { case true: // Generate a new key - key_object, err := totplib.Generate(totplib.GenerateOpts{ + keyObject, err := totplib.Generate(totplib.GenerateOpts{ Issuer: issuer, - AccountName: account_name, - Period: uint_period, - Digits: role_digits, - Algorithm: role_algorithm, - SecretSize: uint_key_size, + AccountName: accountName, + Period: uintPeriod, + Digits: keyDigits, + Algorithm: keyAlgorithm, + SecretSize: uintKeySize, }) if err != nil { - return logical.ErrorResponse("An error occured while generating a key."), nil + return logical.ErrorResponse("an error occured while generating a key"), nil } - url = key_object.String() - key_string = key_object.Secret() + url = keyObject.String() + keyString = keyObject.Secret() case false: - if key_string == "" { - return logical.ErrorResponse("The key value is required."), nil + if keyString == "" { + return logical.ErrorResponse("the key value is required"), nil } - _, err := base32.StdEncoding.DecodeString(key_string) + _, err := base32.StdEncoding.DecodeString(keyString) if err != nil { return logical.ErrorResponse(fmt.Sprintf( - "Invalid key value: %s", err)), nil + "invalid key value: %s", err)), nil } } // Store it - entry, err := logical.StorageEntryJSON("key/"+name, &roleEntry{ - Key: key_string, + entry, err := logical.StorageEntryJSON("key/"+name, &keyEntry{ + Key: keyString, Issuer: issuer, - AccountName: account_name, - Period: uint_period, - Algorithm: role_algorithm, - Digits: role_digits, - Skew: uint_skew, - QRSize: qr_size, + AccountName: accountName, + Period: uintPeriod, + Algorithm: keyAlgorithm, + Digits: keyDigits, + Skew: uintSkew, + QRSize: qrSize, URL: url, Generate: generate, }) @@ -319,7 +319,7 @@ func (b *backend) pathRoleCreate( return nil, nil } -type roleEntry struct { +type keyEntry struct { Key string `json:"key" mapstructure:"key" structs:"key"` Issuer string `json:"issuer" mapstructure:"issuer" structs:"issuer"` AccountName string `json:"account_name" mapstructure:"account_name" structs:"account_name"` @@ -332,11 +332,11 @@ type roleEntry struct { Generate bool `json:"generate" mapstructure:"generate" structs:"generate"` } -const pathRoleHelpSyn = ` -Manage the roles that can be created with this backend. +const pathKeyHelpSyn = ` +Manage the keys that can be created with this backend. ` -const pathRoleHelpDesc = ` -This path lets you manage the roles that can be created with this backend. +const pathKeyHelpDesc = ` +This path lets you manage the keys that can be created with this backend. ` From 2ce1a453e5daf8d15429a15ed3ab8c98fb33a896 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sat, 15 Apr 2017 16:20:38 -0700 Subject: [PATCH 33/43] Moveed barcode and url output to key creation function and did some additional cleanup based on requested changes --- builtin/logical/totp/path_keys.go | 113 +++++++++++++++--------------- 1 file changed, 56 insertions(+), 57 deletions(-) diff --git a/builtin/logical/totp/path_keys.go b/builtin/logical/totp/path_keys.go index d3deda09141d..88258446f093 100644 --- a/builtin/logical/totp/path_keys.go +++ b/builtin/logical/totp/path_keys.go @@ -63,7 +63,7 @@ func pathKeys(b *backend) *framework.Path { }, "period": { - Type: framework.TypeInt, + Type: framework.TypeDurationSecond, Default: 30, Description: `The length of time used to generate a counter for the TOTP token calculation.`, }, @@ -141,46 +141,19 @@ func (b *backend) pathKeyRead( return nil, nil } - switch key.Generate { - case true: - keyObject, err := otplib.NewKeyFromURL(key.URL) - - if err != nil { - return logical.ErrorResponse("an error occured while generating a Key object"), nil - } - - barcode, err := keyObject.Image(key.QRSize, key.QRSize) - - if err != nil { - return logical.ErrorResponse("an error occured while generating a QR code image"), nil - } - - var buff bytes.Buffer - png.Encode(&buff, barcode) - b64Barcode := base64.StdEncoding.EncodeToString(buff.Bytes()) - return &logical.Response{ - Data: map[string]interface{}{ - "url": keyObject.String(), - "barcode": b64Barcode, - "skew": key.Skew, - "qr_size": key.QRSize, - }, - }, nil - default: - // Translate algorithm back to string - algorithm := key.Algorithm.String() - - // Return values of key - return &logical.Response{ - Data: map[string]interface{}{ - "issuer": key.Issuer, - "account_name": key.AccountName, - "period": key.Period, - "algorithm": algorithm, - "digits": key.Digits, - }, - }, nil - } + // Translate algorithm back to string + algorithm := key.Algorithm.String() + + // Return values of key + return &logical.Response{ + Data: map[string]interface{}{ + "issuer": key.Issuer, + "account_name": key.AccountName, + "period": key.Period, + "algorithm": algorithm, + "digits": key.Digits, + }, + }, nil } func (b *backend) pathKeyList( @@ -235,8 +208,11 @@ func (b *backend) pathKeyCreate( return logical.ErrorResponse("the period value must be greater than zero"), nil } - if skew < 0 { - return logical.ErrorResponse("the skew value must be greater than zero"), nil + switch skew { + case 0: + case 1: + default: + return logical.ErrorResponse("the skew value must be 0 or 1"), nil } if qrSize <= 0 { @@ -247,17 +223,6 @@ func (b *backend) pathKeyCreate( return logical.ErrorResponse("the key_size value must be greater than zero"), nil } - // If the key is generated, Account Name and Issuer are required. - if generate { - if accountName == "" { - return logical.ErrorResponse("the account_name value is required for generated keys"), nil - } - - if issuer == "" { - return logical.ErrorResponse("the issuer value is required for generated keys"), nil - } - } - // Period, Skew and Key Size need to be unsigned ints uintPeriod := uint(period) uintSkew := uint(skew) @@ -265,8 +230,19 @@ func (b *backend) pathKeyCreate( url := "" + var response logical.Response + switch generate { case true: + // If the key is generated, Account Name and Issuer are required. + if accountName == "" { + return logical.ErrorResponse("the account_name value is required for generated keys"), nil + } + + if issuer == "" { + return logical.ErrorResponse("the issuer value is required for generated keys"), nil + } + // Generate a new key keyObject, err := totplib.Generate(totplib.GenerateOpts{ Issuer: issuer, @@ -281,9 +257,27 @@ func (b *backend) pathKeyCreate( return logical.ErrorResponse("an error occured while generating a key"), nil } - url = keyObject.String() + // Get key string value keyString = keyObject.Secret() - case false: + + // Prepare the url and barcode + url = keyObject.String() + barcode, err := keyObject.Image(qrSize, qrSize) + + if err != nil { + return logical.ErrorResponse("an error occured while generating a QR code image"), nil + } + + var buff bytes.Buffer + png.Encode(&buff, barcode) + b64Barcode := base64.StdEncoding.EncodeToString(buff.Bytes()) + response = logical.Response{ + Data: map[string]interface{}{ + "url": url, + "barcode": b64Barcode, + }, + } + default: if keyString == "" { return logical.ErrorResponse("the key value is required"), nil } @@ -316,7 +310,12 @@ func (b *backend) pathKeyCreate( return nil, err } - return nil, nil + switch generate { + case true: + return &response, nil + default: + return nil, nil + } } type keyEntry struct { From 8bc9d2cedd09d7fce3f48d8be87b1774fc4e24b9 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sat, 15 Apr 2017 17:50:04 -0700 Subject: [PATCH 34/43] Added ability to pass in TOTP urls --- builtin/logical/totp/path_keys.go | 73 +++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/builtin/logical/totp/path_keys.go b/builtin/logical/totp/path_keys.go index 88258446f093..e161a6abfa47 100644 --- a/builtin/logical/totp/path_keys.go +++ b/builtin/logical/totp/path_keys.go @@ -6,6 +6,9 @@ import ( "encoding/base64" "fmt" "image/png" + "net/url" + "strconv" + "strings" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" @@ -91,6 +94,11 @@ func pathKeys(b *backend) *framework.Path { Default: 200, Description: `The pixel size of the generated square QR code.`, }, + + "url": { + Type: framework.TypeString, + Description: `A TOTP url string containing all of the parameters for key setup.`, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -179,6 +187,63 @@ func (b *backend) pathKeyCreate( skew := data.Get("skew").(int) qrSize := data.Get("qr_size").(int) keySize := data.Get("key_size").(int) + inputURL := data.Get("url").(string) + + // Read parameters from url if given + if inputURL != "" { + //Parse url + urlObject, err := url.Parse(inputURL) + + if err != nil { + return logical.ErrorResponse("an error occured while parsing url string"), nil + } + + //Set up query object + urlQuery := urlObject.Query() + path := strings.TrimPrefix(urlObject.Path, "/") + index := strings.Index(path, ":") + + //Read issuer + urlIssuer := urlQuery.Get("issuer") + if urlIssuer != "" { + issuer = urlIssuer + } else { + if index != -1 { + issuer = path[:index] + } + } + + //Read account name + if index == -1 { + accountName = path + } else { + accountName = path[index+1:] + } + + //Read key string + keyString = urlQuery.Get("secret") + + //Read period + periodQuery, err := strconv.Atoi(urlQuery.Get("period")) + + if err == nil { + period = periodQuery + } + + //Read digits + digitsQuery, err := strconv.Atoi(urlQuery.Get("digits")) + + if err == nil { + digits = digitsQuery + } + + //Read algorithm + algorithmQuery := urlQuery.Get("algorithm") + + if algorithmQuery != "" { + algorithm = algorithmQuery + } + } // Translate digits and algorithm to a format the totp library understands var keyDigits otplib.Digits @@ -228,7 +293,7 @@ func (b *backend) pathKeyCreate( uintSkew := uint(skew) uintKeySize := uint(keySize) - url := "" + urlString := "" var response logical.Response @@ -261,7 +326,7 @@ func (b *backend) pathKeyCreate( keyString = keyObject.Secret() // Prepare the url and barcode - url = keyObject.String() + urlString = keyObject.String() barcode, err := keyObject.Image(qrSize, qrSize) if err != nil { @@ -273,7 +338,7 @@ func (b *backend) pathKeyCreate( b64Barcode := base64.StdEncoding.EncodeToString(buff.Bytes()) response = logical.Response{ Data: map[string]interface{}{ - "url": url, + "url": urlString, "barcode": b64Barcode, }, } @@ -300,7 +365,7 @@ func (b *backend) pathKeyCreate( Digits: keyDigits, Skew: uintSkew, QRSize: qrSize, - URL: url, + URL: urlString, Generate: generate, }) if err != nil { From 2b129fd5ed75ae81af4c467d4f96fba642c6c17b Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sun, 16 Apr 2017 15:10:41 -0700 Subject: [PATCH 35/43] Added additional tests for TOTP server functions --- builtin/logical/totp/backend_test.go | 390 ++++++++++++++++++++++++++- 1 file changed, 386 insertions(+), 4 deletions(-) diff --git a/builtin/logical/totp/backend_test.go b/builtin/logical/totp/backend_test.go index c71a99d3c795..a6923155c121 100644 --- a/builtin/logical/totp/backend_test.go +++ b/builtin/logical/totp/backend_test.go @@ -3,6 +3,7 @@ package totp import ( "fmt" "log" + "net/url" "path" "testing" "time" @@ -25,6 +26,17 @@ func createKey() (string, error) { return key, err } +func generateCode(key string, period uint, digits otplib.Digits, algorithm otplib.Algorithm) (string, error) { + // Generate password using totp library + totpToken, err := totplib.GenerateCodeCustom(key, time.Now(), totplib.ValidateOpts{ + Period: period, + Digits: digits, + Algorithm: algorithm, + }) + + return totpToken, err +} + func TestBackend_readCredentialsDefaultValues(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} @@ -37,7 +49,8 @@ func TestBackend_readCredentialsDefaultValues(t *testing.T) { key, _ := createKey() keyData := map[string]interface{}{ - "key": key, + "key": key, + "generate": false, } expected := map[string]interface{}{ @@ -75,6 +88,7 @@ func TestBackend_readCredentialsEightDigitsThirtySecondPeriod(t *testing.T) { "account_name": "Test", "key": key, "digits": 8, + "generate": false, } expected := map[string]interface{}{ @@ -112,6 +126,7 @@ func TestBackend_readCredentialsSixDigitsNinetySecondPeriod(t *testing.T) { "account_name": "Test", "key": key, "period": 90, + "generate": false, } expected := map[string]interface{}{ @@ -149,6 +164,7 @@ func TestBackend_readCredentialsSHA256(t *testing.T) { "account_name": "Test", "key": key, "algorithm": "SHA256", + "generate": false, } expected := map[string]interface{}{ @@ -186,6 +202,7 @@ func TestBackend_readCredentialsSHA512(t *testing.T) { "account_name": "Test", "key": key, "algorithm": "SHA512", + "generate": false, } expected := map[string]interface{}{ @@ -221,6 +238,7 @@ func TestBackend_keyCrudDefaultValues(t *testing.T) { "issuer": "Vault", "account_name": "Test", "key": key, + "generate": false, } expected := map[string]interface{}{ @@ -232,11 +250,16 @@ func TestBackend_keyCrudDefaultValues(t *testing.T) { "key": key, } + code, _ := generateCode(key, 30, otplib.DigitsSix, otplib.AlgorithmSHA1) + invalidCode := "12345678" + logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ testAccStepCreateKey(t, "test", keyData, false), testAccStepReadKey(t, "test", expected), + testAccStepValidateCode(t, "test", code, true), + testAccStepValidateCode(t, "test", invalidCode, false), testAccStepDeleteKey(t, "test"), testAccStepReadKey(t, "test", nil), }, @@ -254,6 +277,7 @@ func TestBackend_createKeyMissingKeyValue(t *testing.T) { keyData := map[string]interface{}{ "issuer": "Vault", "account_name": "Test", + "generate": false, } logicaltest.Test(t, logicaltest.TestCase{ @@ -277,6 +301,7 @@ func TestBackend_createKeyInvalidKeyValue(t *testing.T) { "issuer": "Vault", "account_name": "Test", "key": "1", + "generate": false, } logicaltest.Test(t, logicaltest.TestCase{ @@ -304,6 +329,7 @@ func TestBackend_createKeyInvalidAlgorithm(t *testing.T) { "account_name": "Test", "key": key, "algorithm": "BADALGORITHM", + "generate": false, } logicaltest.Test(t, logicaltest.TestCase{ @@ -331,6 +357,7 @@ func TestBackend_createKeyInvalidPeriod(t *testing.T) { "account_name": "Test", "key": key, "period": -1, + "generate": false, } logicaltest.Test(t, logicaltest.TestCase{ @@ -358,6 +385,276 @@ func TestBackend_createKeyInvalidDigits(t *testing.T) { "account_name": "Test", "key": key, "digits": 20, + "generate": false, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, true), + testAccStepReadKey(t, "test", nil), + }, + }) +} + +func TestBackend_generatedKeyDefaultValues(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + keyData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "generate": true, + "key_size": 20, + } + + expected := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "digits": otplib.DigitsSix, + "period": 30, + "algorithm": otplib.AlgorithmSHA1, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, false), + testAccStepReadKey(t, "test", expected), + }, + }) +} + +func TestBackend_generatedKeyNonDefaultKeySize(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + keyData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "generate": true, + "key_size": 10, + } + + expected := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "digits": otplib.DigitsSix, + "period": 30, + "algorithm": otplib.AlgorithmSHA1, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, false), + testAccStepReadKey(t, "test", expected), + }, + }) +} + +func TestBackend_urlPassedNonGeneratedKey(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + urlString := "otpauth://totp/Vault:test@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA512&digits=6&period=60" + + keyData := map[string]interface{}{ + "url": urlString, + "generate": false, + } + + expected := map[string]interface{}{ + "issuer": "Vault", + "account_name": "test@email.com", + "digits": otplib.DigitsSix, + "period": 60, + "algorithm": otplib.AlgorithmSHA512, + "key": "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, false), + testAccStepReadKey(t, "test", expected), + testAccStepReadCreds(t, b, config.StorageView, "test", expected), + }, + }) +} + +func TestBackend_urlPassedGeneratedKeyDefaultValues(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + urlString := "otpauth://totp/Vault:test@email.com" + + keyData := map[string]interface{}{ + "url": urlString, + "generate": true, + "key_size": 20, + } + + expected := map[string]interface{}{ + "issuer": "Vault", + "account_name": "test@email.com", + "digits": otplib.DigitsSix, + "period": 30, + "algorithm": otplib.AlgorithmSHA1, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, false), + testAccStepReadKey(t, "test", expected), + }, + }) +} + +func TestBackend_generatedKeyInvalidSkew(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + keyData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "skew": "2", + "generate": true, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, true), + testAccStepReadKey(t, "test", nil), + }, + }) +} + +func TestBackend_generatedKeyInvalidQRSize(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + keyData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "qr_size": "-100", + "generate": true, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, true), + testAccStepReadKey(t, "test", nil), + }, + }) +} + +func TestBackend_generatedKeyInvalidKeySize(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + keyData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "key_size": "-100", + "generate": true, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, true), + testAccStepReadKey(t, "test", nil), + }, + }) +} + +func TestBackend_generatedKeyMissingAccountName(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + keyData := map[string]interface{}{ + "issuer": "Vault", + "generate": true, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, true), + testAccStepReadKey(t, "test", nil), + }, + }) +} + +func TestBackend_generatedKeyMissingIssuer(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + keyData := map[string]interface{}{ + "account_name": "test@email.com", + "generate": true, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, true), + testAccStepReadKey(t, "test", nil), + }, + }) +} + +func TestBackend_invalidURLValue(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + keyData := map[string]interface{}{ + "url": "notaurl", + "generate": false, } logicaltest.Test(t, logicaltest.TestCase{ @@ -375,6 +672,54 @@ func testAccStepCreateKey(t *testing.T, name string, keyData map[string]interfac Path: path.Join("keys", name), Data: keyData, ErrorOk: expectFail, + Check: func(resp *logical.Response) error { + //Skip this if the key is not generated by vault or if the test is expected to fail + if !keyData["generate"].(bool) || expectFail { + return nil + } + + var d struct { + Url string `mapstructure:"url"` + Barcode string `mapstructure:"barcode"` + } + + if err := mapstructure.Decode(resp.Data, &d); err != nil { + return err + } + + //Check to see if barcode and url are returned + if d.Barcode == "" { + t.Fatalf("a barcode was not returned for a generated key") + } + + if d.Url == "" { + t.Fatalf("a url was not returned for a generated key") + } + + //Parse url + urlObject, err := url.Parse(d.Url) + + if err != nil { + t.Fatal("an error occured while parsing url string") + } + + //Set up query object + urlQuery := urlObject.Query() + + //Read secret + urlSecret := urlQuery.Get("secret") + + //Check key length + keySize := keyData["key_size"].(int) + correctSecretStringSize := (keySize / 5) * 8 + actualSecretStringSize := len(urlSecret) + + if actualSecretStringSize != correctSecretStringSize { + t.Fatal("incorrect key string length") + } + + return nil + }, } } @@ -393,9 +738,11 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, s logical.Storage, na var d struct { Code string `mapstructure:"code"` } + if err := mapstructure.Decode(resp.Data, &d); err != nil { return err } + log.Printf("[TRACE] Generated credentials: %v", d) period := validation["period"].(int) @@ -459,15 +806,50 @@ func testAccStepReadKey(t *testing.T, name string, expected map[string]interface case d.Issuer != expected["issuer"]: return fmt.Errorf("issuer should equal: %s", expected["issuer"]) case d.AccountName != expected["account_name"]: - return fmt.Errorf("ccount_Name should equal: %s", expected["account_name"]) + return fmt.Errorf("account_name should equal: %s", expected["account_name"]) case d.Period != uint(period): - return fmt.Errorf("period should equal: %i", expected["period"]) + return fmt.Errorf("period should equal: %d", expected["period"]) case keyAlgorithm != expected["algorithm"]: return fmt.Errorf("algorithm should equal: %s", expected["algorithm"]) case d.Digits != expected["digits"]: - return fmt.Errorf("digits should equal: %i", expected["digits"]) + return fmt.Errorf("digits should equal: %d", expected["digits"]) } + return nil + }, + } +} +func testAccStepValidateCode(t *testing.T, name string, code string, valid bool) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: "code/" + name, + Data: map[string]interface{}{ + "code": code, + }, + Check: func(resp *logical.Response) error { + if resp == nil { + return fmt.Errorf("bad: %#v", resp) + } + + var d struct { + Valid bool `mapstructure:"valid"` + } + + if err := mapstructure.Decode(resp.Data, &d); err != nil { + return err + } + + switch valid { + case true: + if d.Valid != true { + return fmt.Errorf("code was not valid: %s", code) + } + + default: + if d.Valid != false { + return fmt.Errorf("code was incorrectly validated: %s", code) + } + } return nil }, } From aa7be3da27bc7295a27c94d4a0fe46f4c88bd3f9 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Mon, 17 Apr 2017 12:07:28 -0700 Subject: [PATCH 36/43] Removed unused QRSize, URL and Generate members of keyEntry struct --- builtin/logical/totp/path_keys.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/builtin/logical/totp/path_keys.go b/builtin/logical/totp/path_keys.go index e161a6abfa47..976e864ceef6 100644 --- a/builtin/logical/totp/path_keys.go +++ b/builtin/logical/totp/path_keys.go @@ -364,9 +364,6 @@ func (b *backend) pathKeyCreate( Algorithm: keyAlgorithm, Digits: keyDigits, Skew: uintSkew, - QRSize: qrSize, - URL: urlString, - Generate: generate, }) if err != nil { return nil, err @@ -391,9 +388,6 @@ type keyEntry struct { Algorithm otplib.Algorithm `json:"algorithm" mapstructure:"algorithm" structs:"algorithm"` Digits otplib.Digits `json:"digits" mapstructure:"digits" structs:"digits"` Skew uint `json:"skew" mapstructure:"skew" structs:"skew"` - QRSize int `json:"qr_size" mapstructure:"qr_size" structs:"qr_size"` - URL string `json:"url" mapstructure:"url" structs:"url"` - Generate bool `json:"generate" mapstructure:"generate" structs:"generate"` } const pathKeyHelpSyn = ` From f307f8eacf50387c841d7cd8e88b9c4fca2d7b4e Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Tue, 18 Apr 2017 12:25:58 -0700 Subject: [PATCH 37/43] Removed unnecessary urlstring variable from pathKeyCreate --- builtin/logical/totp/path_keys.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/builtin/logical/totp/path_keys.go b/builtin/logical/totp/path_keys.go index 976e864ceef6..453314db9735 100644 --- a/builtin/logical/totp/path_keys.go +++ b/builtin/logical/totp/path_keys.go @@ -293,8 +293,6 @@ func (b *backend) pathKeyCreate( uintSkew := uint(skew) uintKeySize := uint(keySize) - urlString := "" - var response logical.Response switch generate { @@ -326,7 +324,7 @@ func (b *backend) pathKeyCreate( keyString = keyObject.Secret() // Prepare the url and barcode - urlString = keyObject.String() + urlString := keyObject.String() barcode, err := keyObject.Image(qrSize, qrSize) if err != nil { @@ -365,6 +363,7 @@ func (b *backend) pathKeyCreate( Digits: keyDigits, Skew: uintSkew, }) + if err != nil { return nil, err } From 72626e85e80bbcffe0b47bdfb73995f8d0597880 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Sun, 23 Apr 2017 13:50:06 -0700 Subject: [PATCH 38/43] Added website documentation for TOTP secret backend --- website/source/api/secret/totp/index.html.md | 264 ++++++++++++++++++ .../source/docs/secrets/totp/index.html.md | 83 ++++++ website/source/layouts/api.erb | 3 + website/source/layouts/docs.erb | 4 + 4 files changed, 354 insertions(+) create mode 100644 website/source/api/secret/totp/index.html.md create mode 100644 website/source/docs/secrets/totp/index.html.md diff --git a/website/source/api/secret/totp/index.html.md b/website/source/api/secret/totp/index.html.md new file mode 100644 index 000000000000..1ba2b5f58bac --- /dev/null +++ b/website/source/api/secret/totp/index.html.md @@ -0,0 +1,264 @@ +--- +layout: "api" +page_title: "TOTP Secret Backend - HTTP API" +sidebar_current: "docs-http-secret-totp" +description: |- + This is the API documentation for the Vault TOTP secret backend. +--- + +# TOTP Secret Backend HTTP API + +This is the API documentation for the Vault TOTP secret backend. For +general information about the usage and operation of the TOTP backend, +please see the +[Vault TOTP backend documentation](/docs/secrets/totp/index.html). + +This documentation assumes the TOTP backend is mounted at the +`/totp` path in Vault. Since it is possible to mount secret backends at +any location, please update your API calls accordingly. + +## Create Key + +This endpoint creates or updates a key definition. + +| Method | Path | Produces | +| :------- | :--------------------------- | :------------------------------------------------------------------------ | +| `POST` | `/totp/keys/:name` | if generating a key: `200 application/json` else: `204 (empty body)` | + +### Parameters + +- `name` `(string: )` – Specifies the name of the key to create. This is specified as part of the URL. + +- `generate` `(bool: false)` – Specifies if a key is generated by Vault or if a key is being passed from another service. + +- `key_size` `(int: 20)` – Specifies the size in bytes of the Vault generated key. + +- `url` `(string: "")` – Specifies the TOTP key url string that can be used to configure a key. + +- `key` `(string: )` – Specifies the master key used to generate a TOTP code. + +- `issuer` `(string: "" )` – Specifies the name of the key’s issuing organization. + +- `account_name` `(string: "" )` – Specifies the name of the account associated with the key. + +- `period` `(int or duration format string: 30)` – Specifies the length of time in seconds used to generate a counter for the TOTP code calculation. + +- `algorithm` `(string: "SHA1")` – Specifies the hashing algorithm used to generate the TOTP code. Options include "SHA1", "SHA256" and "SHA512". + +- `digits` `(int: 6)` – Specifies the number of digits in the generated TOTP code. This value can be set to 6 or 8. + +- `skew` `(int: 0)` – Specifies the number of delay periods that are allowed when validating a TOTP code. This value can be either 0 or 1. + +- `qr_size` `(int: 200)` – Specifies the pixel size of the square QR code when generating a new key. + +### Sample Payload + +```json +{ + "url": "otpauth://totp/Google:test@gmail.com?secret=Y64VEVMBTSXCYIWRSHRNDZW62MPGVU2G&issuer=Google" +} +``` + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request POST \ + --data @payload.json \ + https://vault.rocks/v1/totp/keys/my-key +``` + +### Sample Payload + +```json +{ + "generate": true, + "issuer": "Google", + "account_name": "test@gmail.com", +} +``` + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request POST \ + --data @payload.json \ + https://vault.rocks/v1/totp/keys/my-key +``` + +### Sample Response + +```json +{ + "data": { + "barcode": "iVBORw0KGgoAAAANSUhEUgAAAMgAAADIEAAAAADYoy0BAAAGXklEQVR4nOyd4Y4iOQyEmRPv/8p7upX6BJm4XbbDbK30fT9GAtJJhpLjdhw3z1+/HmDEP396AvDO878/X1+9i1frWvu5Po/6Xz+P2kft1nFVa1f7z+YdjT/5PrEQMxDEDAQx4/n6orsGr6z9ZP1mviMbP/MBav/R6/U61Ud0vk8sxAwEMQNBzHju3lTvv6P2ajwS9Ve9zz+9pkfjRp+r/SjzwULMQBAzEMSMrQ/pUo0bouun7dW9LXVvrBq/TMBCzEAQMxDEjKM+JFqT17W4mu9Y+49eq/OL3r/GVX3CJ7KtWIgZCGIGgpix9SHTtXGa4476qfoa1adVc+HV/6/yfWIhZiCIGQhixpsP6Z4nulD3lqavV7q+Yvo6G7/zfWIhZiCIGQhixteJ/Rh1Da3e71d9RjRul2ocdeK7xELMQBAzEMSM3z6ku6dTrdOo1l9M6y5O7clVx5n4SCzEDAQxA0HMuN3L+qlavqj9itpePY+VtVdrHqfzeQULMQNBzEAQM97ikAv1vr/brltTeCp/svarcjLe2F1PnbohCGIGgphRqjG8mJ6PmtYMVnP363Vqv6d8qZrzf2AhfiCIGQhixm0c8n+jQ8+7+jZ4cY3PrlfHO/1Ml+45st18sRAzEMQMBDHjdxyixgPqs0lWsvvwqH00zrSO41R80p3XXXssxAwEMQNBzJCeuaieo6pedzGtb1/76fqgLH6ofg+dZ65gIWYgiBkIYsbbs9/V+/EVde1V+62eh1I/r/qIrs+Ixo2uYy/LGAQxA0HMeNvLilDX1OraXc2jVNtPzxJXr6v+HzuwEDMQxAwEMWNbp95d21WmzzBR6066e07dPMq0XoW9LEMQxAwEMUOqUz+1p9ONd07Xz586u6yifp/4EEMQxAwEMUPay7rIcthqTrx6v1/NTX+qZrIbF63v34GFmIEgZiCIGdvfU++e1a3GM2oOPjtvpfbfjS+qeZFJXgcLMQNBzEAQM6Tn9p7OLVdrFqP5TFF9ZXTdqfqTV7AQMxDEDAQx482HdPMPGdN8SjeHr6710zzJidrCB/kQTxDEDAQxY7uXdTGNC9S9pK6vqs6nWzdyej53PhELMQNBzEAQM0o59YtTz/xQfVO3jmOdl0rmE6f5ort5YSFmIIgZCGLGbU69eka3ep+v5sCzcbp5jZXMR0zr+aPPqVM3BkHMQBAzRs/tjejmwj9d05ihzq96nQr5EEMQxAwEMWPrQy6q9/fdevFTcVA0v+n5K7U/tf4lGhcfYgiCmIEgZtw+6+RCXUurvkKlepZ2vS5i+oyTaby0GxcLMQNBzEAQM0r5kKnv6K6xK9X4R13zu+eyJnXpazssxAwEMQNBzNj+fkg3nqjGK9laPz1vleXwq2v+p+vciUMMQRAzEMSM298xrOYDVqrtpmtzt59uHqc6v2zcBxbiB4KYgSBmbOvUV7q577VdOIliXqLr87p7Tere2YnrsRAzEMQMBDFj+zuGar3Gp+rNp3kUtR5lmj/Jxo/GvZsvFmIGgpiBIGbcPi/rW+MPPaeqOs407xL1E1E9lzWpg8FCzEAQMxDEDOk3qC66a7f6fsSn1uz18+o8P+GzsBAzEMQMBDFjm1Ov7L3s3p+2/6lcfoa6ZxaNm50DWyEOMQRBzEAQM7Zne6PX3XilW5M3zbd0c/3ZHpvqY6P+7j7HQsxAEDMQxIxRPqRaT6Kuzemkh7WJ3RrJbJxq7eOuPyzEDAQxA0HMKJ3t/XbxobW/Gmdka/PpPMxPgoWYgSBmIIgZ0m9QrXTP1mb9Ru2y+/hsD2xaM9jN5UfjEIf8RSCIGQhiRus3qLp7ONU6jK4vynxMdn10XdY+m4/SHxZiBoKYgSBm3MYhGdl9/qkzvN18ilpDqF6nxiPVGs3Xz7EQMxDEDAQx4/ZcVoR6fqobZ6h7Vtm81TVejZdWuvHNXXssxAwEMQNBzHju3pyujdO68Ky9Wm+h9qPGJVG/6nyU+WIhZiCIGQhixtaHdFF9hlqLeOrcVPcMQDeOmtTNYyFmIIgZCGLGUR/SPQs73QuL5tGtiVznlc1X/T8iXtthIWYgiBkIYsbWh3T3nNS1dXqe6tReW8S0Hr1b5/LAQvxAEDMQxIw3H9I9nzU9R6XGHdn41dx4d4+rGp9En7OX9ReAIGYgiBlff6IWG2KwEDP+DQAA//+TDHXGhqE4+AAAAABJRU5ErkJggg==", + "url" : "otpauth://totp/Google:test@gmail.com?algorithm=SHA1&digits=6&issuer=Google&period=30&secret=HTXT7KJFVNAJUPYWQRWMNVQE5AF5YZI2", + } +} +``` + +## Read Key + +This endpoint queries the key definition. + +| Method | Path | Produces | +| :------- | :--------------------------- | :--------------------- | +| `GET` | `/totp/keys/:name` | `200 application/json` | + +### Parameters + +- `name` `(string: )` – Specifies the name of the key to read. This is specified as part of the URL. + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + https://vault.rocks/v1/totp/keys/my-key +``` + +### Sample Response + +```json +{ + "data": { + "account_name": "test@gmail.com", + "algorithm" : "SHA1", + "digits" : 6, + "issuer": "Google", + "period" : 30, + } +} +``` + +## List Keys + +This endpoint returns a list of available keys. Only the key names are +returned, not any values. + +| Method | Path | Produces | +| :------- | :--------------------------- | :--------------------- | +| `LIST` | `/totp/keys` | `200 application/json` | + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request LIST \ + https://vault.rocks/v1/totp/keys +``` + +### Sample Response + +```json +{ + "auth": null, + "data": { + "keys": ["my-key"] + }, + "lease_duration": 0, + "lease_id": "", + "renewable": false +} +``` + +## Delete Key + +This endpoint deletes the key definition. + +| Method | Path | Produces | +| :------- | :--------------------------- | :--------------------- | +| `DELETE` | `/totp/keys/:name` | `204 (empty body)` | + +### Parameters + +- `name` `(string: )` – Specifies the name of the key to delete. This + is specified as part of the URL. + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request DELETE \ + https://vault.rocks/v1/totp/keys/my-key +``` + +## Generate Credentials + +This endpoint generates a new time-based one-time use password based on the named +key. + +| Method | Path | Produces | +| :------- | :--------------------------- | :--------------------- | +| `GET` | `/totp/code/:name` | `200 application/json` | + +### Parameters + +- `name` `(string: )` – Specifies the name of the key to create + credentials against. This is specified as part of the URL. + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + https://vault.rocks/v1/totp/code/my-key +``` + +### Sample Response + +```json +{ + "data": { + "code": "810920", + } +} +``` + +## Validate Credentials + +This endpoint validates a time-based one-time use password generated from the named +key. + +| Method | Path | Produces | +| :------- | :--------------------------- | :--------------------- | +| `POST` | `/totp/code/:name` | `200 application/json` | + +### Parameters + +- `name` `(string: )` – Specifies the name of the key used to generate the password. This is specified as part of the URL. + +- `code` `(string: )` – Specifies the password you want to validate. + +### Sample Payload + +```json +{ + "code": "123802" +} +``` + +### Sample Request + +``` +$ curl \ + --header "X-Vault-Token: ..." \ + --request POST \ + --data @payload.json \ + https://vault.rocks/v1/totp/code/my-key +``` + +### Sample Response + +```json +{ + "data": { + "valid": true, + } +} +``` diff --git a/website/source/docs/secrets/totp/index.html.md b/website/source/docs/secrets/totp/index.html.md new file mode 100644 index 000000000000..6fe3f185dbb1 --- /dev/null +++ b/website/source/docs/secrets/totp/index.html.md @@ -0,0 +1,83 @@ +--- +layout: "docs" +page_title: "TOTP Secret Backend" +sidebar_current: "docs-secrets-totp" +description: |- + The TOTP secret backend for Vault generates time-based one-time use passwords. +--- + +# TOTP Secret Backend + +Name: `totp` + +The TOTP secret backend for Vault will allow Vault users to store their multi-factor +authentication keys in Vault and use the API to retrieve time-based one-time use passwords +on demand. The backend can also be used to generate a new key and validate passwords generated by that key. + +This page will show a quick start for this backend. For detailed documentation +on every path, use `vault path-help` after mounting the backend. + +## Quick Start + +The first step to using the TOTP backend is to mount it. +Unlike the `generic` backend, the `totp` backend is not mounted by default. + +```text +$ vault mount totp +Successfully mounted 'totp' at 'totp'! +``` + +The next step is to configure a key. For example, lets create +a "test" key by passing in a TOTP key url: + +```text +$ vault write totp/keys/test \ + url="otpauth://totp/Google:test@gmail.com?secret=Y64VEVMBTSXCYIWRSHRNDZW62MPGVU2G&issuer=Google" +Success! Data written to: totp/keys/test +``` + +By writing to the `keys/test` path we are defining the `test` key. + +To generate a new set of credentials, we simply read from that key using the `code` path: + +```text +$ vault read totp/code/test +Key Value +code 135031 +``` +Vault is now configured to create time-based one-time use passwords! + +By reading from the `code/test` path, Vault has generated a new +time-based one-time use password using the `test` key configuration. + +Using ACLs, it is possible to restrict using the TOTP backend such +that trusted operators can manage the key definitions, and both +users and applications are restricted in the credentials they are +allowed to read. + +The TOTP backend can also be used to generate new keys and validate passwords generated using those keys. + +In order to generate a new key, set the generate flag to true and pass in an issuer and account name. + +```text +$ vault write totp/keys/test \ + generate=true issuer=Google account_name=test@gmail.com +``` +A base64 encoded barcode and url will be returned upon generating a new key. These can be given to client applications that +can generate passwords. You can validate those passwords by writing to the `code/test` path. + +```text +$ vault write totp/code/test \ + code=127388 +Key Value +valid true +``` + +If you get stuck at any time, simply run `vault path-help totp` or with a +subpath for interactive help output. + +## API + +The TOTP secret backend has a full HTTP API. Please see the +[TOTP secret backend API](/api/secret/totp/index.html) for more +details. diff --git a/website/source/layouts/api.erb b/website/source/layouts/api.erb index c209937bc0bb..fa0903901101 100644 --- a/website/source/layouts/api.erb +++ b/website/source/layouts/api.erb @@ -53,6 +53,9 @@ > SSH + > + TOTP + > Transit diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 8f2686e64439..b58b8e7322a1 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -247,6 +247,10 @@ SSH + > + TOTP + + > Transit From 8ea74b9b7ad261e3d51dcc734894e62fc743e49c Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Fri, 28 Apr 2017 12:46:03 -0700 Subject: [PATCH 39/43] Added errors if generate is true and url or key is passed, removed logger from backend, and revised parameter documentation. --- builtin/logical/totp/backend.go | 4 --- builtin/logical/totp/backend_test.go | 53 +++++++++++++++++++++++++--- builtin/logical/totp/path_code.go | 7 ---- builtin/logical/totp/path_keys.go | 35 +++++++++--------- 4 files changed, 66 insertions(+), 33 deletions(-) diff --git a/builtin/logical/totp/backend.go b/builtin/logical/totp/backend.go index 889863652302..4e3554bdbdf3 100644 --- a/builtin/logical/totp/backend.go +++ b/builtin/logical/totp/backend.go @@ -3,8 +3,6 @@ package totp import ( "strings" - log "github.com/mgutz/logxi/v1" - "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -27,13 +25,11 @@ func Backend(conf *logical.BackendConfig) *backend { Secrets: []*framework.Secret{}, } - b.logger = conf.Logger return &b } type backend struct { *framework.Backend - logger log.Logger } const backendHelp = ` diff --git a/builtin/logical/totp/backend_test.go b/builtin/logical/totp/backend_test.go index a6923155c121..79afbb46e0a6 100644 --- a/builtin/logical/totp/backend_test.go +++ b/builtin/logical/totp/backend_test.go @@ -503,12 +503,11 @@ func TestBackend_urlPassedGeneratedKeyDefaultValues(t *testing.T) { t.Fatal(err) } - urlString := "otpauth://totp/Vault:test@email.com" - keyData := map[string]interface{}{ - "url": urlString, - "generate": true, - "key_size": 20, + "issuer": "Vault", + "account_name": "test@email.com", + "generate": true, + "key_size": 20, } expected := map[string]interface{}{ @@ -666,6 +665,50 @@ func TestBackend_invalidURLValue(t *testing.T) { }) } +func TestBackend_urlAndGenerateTrue(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + keyData := map[string]interface{}{ + "url": "otpauth://totp/Vault:test@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA512&digits=6&period=60", + "generate": true, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, true), + testAccStepReadKey(t, "test", nil), + }, + }) +} + +func TestBackend_keyAndGenerateTrue(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + keyData := map[string]interface{}{ + "key": "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", + "generate": true, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, true), + testAccStepReadKey(t, "test", nil), + }, + }) +} + func testAccStepCreateKey(t *testing.T, name string, keyData map[string]interface{}, expectFail bool) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, diff --git a/builtin/logical/totp/path_code.go b/builtin/logical/totp/path_code.go index ffe8227fac52..63fc9c514c57 100644 --- a/builtin/logical/totp/path_code.go +++ b/builtin/logical/totp/path_code.go @@ -35,13 +35,9 @@ func pathCode(b *backend) *framework.Path { func (b *backend) pathReadCode( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - b.logger.Trace("totp/pathReadCode: enter") - defer b.logger.Trace("totp/pathReadCode: exit") - name := data.Get("name").(string) // Get the key - b.logger.Trace("totp/pathReadCode: getting key") key, err := b.Key(req.Storage, name) if err != nil { return nil, err @@ -56,14 +52,11 @@ func (b *backend) pathReadCode( Digits: key.Digits, Algorithm: key.Algorithm, }) - if err != nil { return nil, err } // Return the secret - b.logger.Trace("totp/pathReadCode: generating secret") - resp, err := &logical.Response{ Data: map[string]interface{}{ "code": totpToken, diff --git a/builtin/logical/totp/path_keys.go b/builtin/logical/totp/path_keys.go index 453314db9735..5b81204d7be4 100644 --- a/builtin/logical/totp/path_keys.go +++ b/builtin/logical/totp/path_keys.go @@ -47,22 +47,22 @@ func pathKeys(b *backend) *framework.Path { "key_size": { Type: framework.TypeInt, Default: 20, - Description: "Determines the size in bytes of the generated key.", + Description: "Determines the size in bytes of the generated key. Only used if generate is true.", }, "key": { Type: framework.TypeString, - Description: "The shared master key used to generate a TOTP token.", + Description: "The shared master key used to generate a TOTP token. Only used if generate is false.", }, "issuer": { Type: framework.TypeString, - Description: `The name of the key's issuing organization.`, + Description: `The name of the key's issuing organization. Required if generate is true and url is not passed.`, }, "account_name": { Type: framework.TypeString, - Description: `The name of the account associated with the key.`, + Description: `The name of the account associated with the key. Required if generate is true and url is not passed.`, }, "period": { @@ -74,30 +74,30 @@ func pathKeys(b *backend) *framework.Path { "algorithm": { Type: framework.TypeString, Default: "SHA1", - Description: `The hashing algorithm used to generate the TOTP token. Options include SHA1, SHA256 and SHA512`, + Description: `The hashing algorithm used to generate the TOTP token. Options include SHA1, SHA256 and SHA512.`, }, "digits": { Type: framework.TypeInt, Default: 6, - Description: `The number of digits in the generated TOTP token. This value can either be 6 or 8`, + Description: `The number of digits in the generated TOTP token. This value can either be 6 or 8.`, }, "skew": { Type: framework.TypeInt, Default: 0, - Description: `The number of delay periods that are allowed when validating a TOTP token. This value can either be 0 or 1`, + Description: `The number of delay periods that are allowed when validating a TOTP token. This value can either be 0 or 1. Only used if generate is true.`, }, "qr_size": { Type: framework.TypeInt, Default: 200, - Description: `The pixel size of the generated square QR code.`, + Description: `The pixel size of the generated square QR code. Only used if generate is true.`, }, "url": { Type: framework.TypeString, - Description: `A TOTP url string containing all of the parameters for key setup.`, + Description: `A TOTP url string containing all of the parameters for key setup. Only used if generate is false.`, }, }, @@ -189,11 +189,19 @@ func (b *backend) pathKeyCreate( keySize := data.Get("key_size").(int) inputURL := data.Get("url").(string) + if generate == true { + if keyString != "" { + return logical.ErrorResponse("a key should not be passed if generate is true"), nil + } + if inputURL != "" { + return logical.ErrorResponse("a url should not be passed if generate is true"), nil + } + } + // Read parameters from url if given if inputURL != "" { //Parse url urlObject, err := url.Parse(inputURL) - if err != nil { return logical.ErrorResponse("an error occured while parsing url string"), nil } @@ -225,21 +233,18 @@ func (b *backend) pathKeyCreate( //Read period periodQuery, err := strconv.Atoi(urlQuery.Get("period")) - if err == nil { period = periodQuery } //Read digits digitsQuery, err := strconv.Atoi(urlQuery.Get("digits")) - if err == nil { digits = digitsQuery } //Read algorithm algorithmQuery := urlQuery.Get("algorithm") - if algorithmQuery != "" { algorithm = algorithmQuery } @@ -315,7 +320,6 @@ func (b *backend) pathKeyCreate( Algorithm: keyAlgorithm, SecretSize: uintKeySize, }) - if err != nil { return logical.ErrorResponse("an error occured while generating a key"), nil } @@ -326,7 +330,6 @@ func (b *backend) pathKeyCreate( // Prepare the url and barcode urlString := keyObject.String() barcode, err := keyObject.Image(qrSize, qrSize) - if err != nil { return logical.ErrorResponse("an error occured while generating a QR code image"), nil } @@ -346,7 +349,6 @@ func (b *backend) pathKeyCreate( } _, err := base32.StdEncoding.DecodeString(keyString) - if err != nil { return logical.ErrorResponse(fmt.Sprintf( "invalid key value: %s", err)), nil @@ -363,7 +365,6 @@ func (b *backend) pathKeyCreate( Digits: keyDigits, Skew: uintSkew, }) - if err != nil { return nil, err } From ec4922977270a812fb4e8b80146137bfaa29753d Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Fri, 28 Apr 2017 12:59:52 -0700 Subject: [PATCH 40/43] Updated website documentation and added QR example --- website/source/api/secret/totp/index.html.md | 24 ++++++++++++------- .../source/docs/secrets/totp/index.html.md | 4 ++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/website/source/api/secret/totp/index.html.md b/website/source/api/secret/totp/index.html.md index 1ba2b5f58bac..595819e4c7b6 100644 --- a/website/source/api/secret/totp/index.html.md +++ b/website/source/api/secret/totp/index.html.md @@ -31,15 +31,15 @@ This endpoint creates or updates a key definition. - `generate` `(bool: false)` – Specifies if a key is generated by Vault or if a key is being passed from another service. -- `key_size` `(int: 20)` – Specifies the size in bytes of the Vault generated key. +- `key_size` `(int: 20)` – Specifies the size in bytes of the Vault generated key. Only used if generate is true. -- `url` `(string: "")` – Specifies the TOTP key url string that can be used to configure a key. +- `url` `(string: "")` – Specifies the TOTP key url string that can be used to configure a key. Only used if generate is false. -- `key` `(string: )` – Specifies the master key used to generate a TOTP code. +- `key` `(string: )` – Specifies the master key used to generate a TOTP code. Only used if generate is false. - `issuer` `(string: "" )` – Specifies the name of the key’s issuing organization. -- `account_name` `(string: "" )` – Specifies the name of the account associated with the key. +- `account_name` `(string: "" )` – Specifies the name of the account associated with the key. - `period` `(int or duration format string: 30)` – Specifies the length of time in seconds used to generate a counter for the TOTP code calculation. @@ -47,9 +47,9 @@ This endpoint creates or updates a key definition. - `digits` `(int: 6)` – Specifies the number of digits in the generated TOTP code. This value can be set to 6 or 8. -- `skew` `(int: 0)` – Specifies the number of delay periods that are allowed when validating a TOTP code. This value can be either 0 or 1. +- `skew` `(int: 0)` – Specifies the number of delay periods that are allowed when validating a TOTP code. This value can be either 0 or 1. Only used if generate is true. -- `qr_size` `(int: 200)` – Specifies the pixel size of the square QR code when generating a new key. +- `qr_size` `(int: 200)` – Specifies the pixel size of the square QR code when generating a new key. Only used if generate is true. ### Sample Payload @@ -100,6 +100,14 @@ $ curl \ } ``` +### Embedding Barcodes in HTML + +To embed the generated barcode in a web page, include the base64 string in an `img` tag with the prefix `data:image/png;base64` + +``` + +``` + ## Read Key This endpoint queries the key definition. @@ -188,7 +196,7 @@ $ curl \ https://vault.rocks/v1/totp/keys/my-key ``` -## Generate Credentials +## Generate Code This endpoint generates a new time-based one-time use password based on the named key. @@ -220,7 +228,7 @@ $ curl \ } ``` -## Validate Credentials +## Validate Code This endpoint validates a time-based one-time use password generated from the named key. diff --git a/website/source/docs/secrets/totp/index.html.md b/website/source/docs/secrets/totp/index.html.md index 6fe3f185dbb1..97cebceb8720 100644 --- a/website/source/docs/secrets/totp/index.html.md +++ b/website/source/docs/secrets/totp/index.html.md @@ -32,7 +32,7 @@ a "test" key by passing in a TOTP key url: ```text $ vault write totp/keys/test \ - url="otpauth://totp/Google:test@gmail.com?secret=Y64VEVMBTSXCYIWRSHRNDZW62MPGVU2G&issuer=Google" + url="otpauth://totp/Vault:test@gmail.com?secret=Y64VEVMBTSXCYIWRSHRNDZW62MPGVU2G&issuer=Vault" Success! Data written to: totp/keys/test ``` @@ -61,7 +61,7 @@ In order to generate a new key, set the generate flag to true and pass in an iss ```text $ vault write totp/keys/test \ - generate=true issuer=Google account_name=test@gmail.com + generate=true issuer=Vault account_name=test@gmail.com ``` A base64 encoded barcode and url will be returned upon generating a new key. These can be given to client applications that can generate passwords. You can validate those passwords by writing to the `code/test` path. From fbabadd82fff33a9f26bc9d12d413afc67bb276e Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Tue, 2 May 2017 20:11:46 -0700 Subject: [PATCH 41/43] Added exported variable and ability to disable QR generation, cleaned up error reporting, changed default skew value, updated documentation and added additional tests --- builtin/logical/totp/backend_test.go | 243 ++++++++++++++++++++++++++- builtin/logical/totp/path_keys.go | 100 ++++++----- 2 files changed, 298 insertions(+), 45 deletions(-) diff --git a/builtin/logical/totp/backend_test.go b/builtin/logical/totp/backend_test.go index 79afbb46e0a6..2a18a056f6e8 100644 --- a/builtin/logical/totp/backend_test.go +++ b/builtin/logical/totp/backend_test.go @@ -410,6 +410,8 @@ func TestBackend_generatedKeyDefaultValues(t *testing.T) { "account_name": "Test", "generate": true, "key_size": 20, + "exported": true, + "qr_size": 200, } expected := map[string]interface{}{ @@ -429,6 +431,31 @@ func TestBackend_generatedKeyDefaultValues(t *testing.T) { }) } +func TestBackend_generatedKeyDefaultValuesNoQR(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + keyData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "Test", + "generate": true, + "key_size": 20, + "exported": true, + "qr_size": 0, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, false), + }, + }) +} + func TestBackend_generatedKeyNonDefaultKeySize(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} @@ -442,6 +469,8 @@ func TestBackend_generatedKeyNonDefaultKeySize(t *testing.T) { "account_name": "Test", "generate": true, "key_size": 10, + "exported": true, + "qr_size": 200, } expected := map[string]interface{}{ @@ -461,7 +490,55 @@ func TestBackend_generatedKeyNonDefaultKeySize(t *testing.T) { }) } -func TestBackend_urlPassedNonGeneratedKey(t *testing.T) { +func TestBackend_urlPassedNonGeneratedKeyInvalidPeriod(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + urlString := "otpauth://totp/Vault:test@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA512&digits=6&period=AZ" + + keyData := map[string]interface{}{ + "url": urlString, + "generate": false, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, true), + testAccStepReadKey(t, "test", nil), + }, + }) +} + +func TestBackend_urlPassedNonGeneratedKeyInvalidDigits(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + urlString := "otpauth://totp/Vault:test@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA512&digits=Q&period=60" + + keyData := map[string]interface{}{ + "url": urlString, + "generate": false, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, true), + testAccStepReadKey(t, "test", nil), + }, + }) +} + +func TestBackend_urlPassedNonGeneratedKeyIssuerInFirstPosition(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(config) @@ -495,7 +572,7 @@ func TestBackend_urlPassedNonGeneratedKey(t *testing.T) { }) } -func TestBackend_urlPassedGeneratedKeyDefaultValues(t *testing.T) { +func TestBackend_urlPassedNonGeneratedKeyIssuerInQueryString(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(config) @@ -503,19 +580,54 @@ func TestBackend_urlPassedGeneratedKeyDefaultValues(t *testing.T) { t.Fatal(err) } + urlString := "otpauth://totp/test@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA512&digits=6&period=60&issuer=Vault" + keyData := map[string]interface{}{ + "url": urlString, + "generate": false, + } + + expected := map[string]interface{}{ "issuer": "Vault", "account_name": "test@email.com", - "generate": true, - "key_size": 20, + "digits": otplib.DigitsSix, + "period": 60, + "algorithm": otplib.AlgorithmSHA512, + "key": "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, false), + testAccStepReadKey(t, "test", expected), + testAccStepReadCreds(t, b, config.StorageView, "test", expected), + }, + }) +} + +func TestBackend_urlPassedNonGeneratedKeyMissingIssuer(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + urlString := "otpauth://totp/test@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA512&digits=6&period=60" + + keyData := map[string]interface{}{ + "url": urlString, + "generate": false, } expected := map[string]interface{}{ - "issuer": "Vault", + "issuer": "", "account_name": "test@email.com", "digits": otplib.DigitsSix, - "period": 30, - "algorithm": otplib.AlgorithmSHA1, + "period": 60, + "algorithm": otplib.AlgorithmSHA512, + "key": "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", } logicaltest.Test(t, logicaltest.TestCase{ @@ -523,6 +635,75 @@ func TestBackend_urlPassedGeneratedKeyDefaultValues(t *testing.T) { Steps: []logicaltest.TestStep{ testAccStepCreateKey(t, "test", keyData, false), testAccStepReadKey(t, "test", expected), + testAccStepReadCreds(t, b, config.StorageView, "test", expected), + }, + }) +} + +func TestBackend_urlPassedNonGeneratedKeyMissingAccountName(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + urlString := "otpauth://totp/Vault:?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA512&digits=6&period=60" + + keyData := map[string]interface{}{ + "url": urlString, + "generate": false, + } + + expected := map[string]interface{}{ + "issuer": "Vault", + "account_name": "", + "digits": otplib.DigitsSix, + "period": 60, + "algorithm": otplib.AlgorithmSHA512, + "key": "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, false), + testAccStepReadKey(t, "test", expected), + testAccStepReadCreds(t, b, config.StorageView, "test", expected), + }, + }) +} + +func TestBackend_urlPassedNonGeneratedKeyMissingAccountNameandIssuer(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + urlString := "otpauth://totp/?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&algorithm=SHA512&digits=6&period=60" + + keyData := map[string]interface{}{ + "url": urlString, + "generate": false, + } + + expected := map[string]interface{}{ + "issuer": "", + "account_name": "", + "digits": otplib.DigitsSix, + "period": 60, + "algorithm": otplib.AlgorithmSHA512, + "key": "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, false), + testAccStepReadKey(t, "test", expected), + testAccStepReadCreds(t, b, config.StorageView, "test", expected), }, }) } @@ -709,6 +890,38 @@ func TestBackend_keyAndGenerateTrue(t *testing.T) { }) } +func TestBackend_generatedKeyExportedFalse(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + keyData := map[string]interface{}{ + "issuer": "Vault", + "account_name": "test@email.com", + "generate": true, + "exported": false, + } + + expected := map[string]interface{}{ + "issuer": "Vault", + "account_name": "test@email.com", + "digits": otplib.DigitsSix, + "period": 30, + "algorithm": otplib.AlgorithmSHA1, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepCreateKey(t, "test", keyData, false), + testAccStepReadKey(t, "test", expected), + }, + }) +} + func testAccStepCreateKey(t *testing.T, name string, keyData map[string]interface{}, expectFail bool) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, @@ -721,6 +934,22 @@ func testAccStepCreateKey(t *testing.T, name string, keyData map[string]interfac return nil } + // Check to see if barcode and url were returned if exported is false + if !keyData["exported"].(bool) { + if resp != nil { + t.Fatalf("data was returned when exported was set to false") + } + return nil + } + + // Check to see if a barcode was returned when qr_size is zero + if keyData["qr_size"].(int) == 0 { + if _, exists := resp.Data["barcode"]; exists { + t.Fatalf("a barcode was returned when qr_size was set to zero") + } + return nil + } + var d struct { Url string `mapstructure:"url"` Barcode string `mapstructure:"barcode"` diff --git a/builtin/logical/totp/path_keys.go b/builtin/logical/totp/path_keys.go index 5b81204d7be4..58c5fa2f5d16 100644 --- a/builtin/logical/totp/path_keys.go +++ b/builtin/logical/totp/path_keys.go @@ -44,6 +44,12 @@ func pathKeys(b *backend) *framework.Path { Description: "Determines if a key is generated by vault or another service.", }, + "exported": { + Type: framework.TypeBool, + Default: true, + Description: "Determines if a QR code and url are returned upon generating a key. Only used if generate is true.", + }, + "key_size": { Type: framework.TypeInt, Default: 20, @@ -57,12 +63,12 @@ func pathKeys(b *backend) *framework.Path { "issuer": { Type: framework.TypeString, - Description: `The name of the key's issuing organization. Required if generate is true and url is not passed.`, + Description: `The name of the key's issuing organization. Required if generate is true.`, }, "account_name": { Type: framework.TypeString, - Description: `The name of the account associated with the key. Required if generate is true and url is not passed.`, + Description: `The name of the account associated with the key. Required if generate is true.`, }, "period": { @@ -85,14 +91,14 @@ func pathKeys(b *backend) *framework.Path { "skew": { Type: framework.TypeInt, - Default: 0, + Default: 1, Description: `The number of delay periods that are allowed when validating a TOTP token. This value can either be 0 or 1. Only used if generate is true.`, }, "qr_size": { Type: framework.TypeInt, Default: 200, - Description: `The pixel size of the generated square QR code. Only used if generate is true.`, + Description: `The pixel size of the generated square QR code. Only used if generate is true and exported is true. If this value is 0, a QR code will not be returned.`, }, "url": { @@ -178,6 +184,7 @@ func (b *backend) pathKeyCreate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { name := data.Get("name").(string) generate := data.Get("generate").(bool) + exported := data.Get("exported").(bool) keyString := data.Get("key").(string) issuer := data.Get("issuer").(string) accountName := data.Get("account_name").(string) @@ -189,7 +196,7 @@ func (b *backend) pathKeyCreate( keySize := data.Get("key_size").(int) inputURL := data.Get("url").(string) - if generate == true { + if generate { if keyString != "" { return logical.ErrorResponse("a key should not be passed if generate is true"), nil } @@ -203,7 +210,7 @@ func (b *backend) pathKeyCreate( //Parse url urlObject, err := url.Parse(inputURL) if err != nil { - return logical.ErrorResponse("an error occured while parsing url string"), nil + return logical.ErrorResponse("an error occured while parsing url string"), err } //Set up query object @@ -232,15 +239,23 @@ func (b *backend) pathKeyCreate( keyString = urlQuery.Get("secret") //Read period - periodQuery, err := strconv.Atoi(urlQuery.Get("period")) - if err == nil { - period = periodQuery + periodQuery := urlQuery.Get("period") + if periodQuery != "" { + periodInt, err := strconv.Atoi(periodQuery) + if err != nil { + return logical.ErrorResponse("an error occured while parsing period value in url"), err + } + period = periodInt } //Read digits - digitsQuery, err := strconv.Atoi(urlQuery.Get("digits")) - if err == nil { - digits = digitsQuery + digitsQuery := urlQuery.Get("digits") + if digitsQuery != "" { + digitsInt, err := strconv.Atoi(digitsQuery) + if err != nil { + return logical.ErrorResponse("an error occured while parsing digits value in url"), err + } + digits = digitsInt } //Read algorithm @@ -258,7 +273,7 @@ func (b *backend) pathKeyCreate( case 8: keyDigits = otplib.DigitsEight default: - return logical.ErrorResponse("the digit value can only be 6 or 8"), nil + return logical.ErrorResponse("the digits value can only be 6 or 8"), nil } var keyAlgorithm otplib.Algorithm @@ -285,8 +300,9 @@ func (b *backend) pathKeyCreate( return logical.ErrorResponse("the skew value must be 0 or 1"), nil } - if qrSize <= 0 { - return logical.ErrorResponse("the qr_size value must be greater than zero"), nil + // QR size can be zero but it shouldn't be negative + if qrSize < 0 { + return logical.ErrorResponse("the qr_size value must be greater than or equal to zero"), nil } if keySize <= 0 { @@ -298,7 +314,7 @@ func (b *backend) pathKeyCreate( uintSkew := uint(skew) uintKeySize := uint(keySize) - var response logical.Response + var response *logical.Response switch generate { case true: @@ -321,27 +337,40 @@ func (b *backend) pathKeyCreate( SecretSize: uintKeySize, }) if err != nil { - return logical.ErrorResponse("an error occured while generating a key"), nil + return logical.ErrorResponse("an error occured while generating a key"), err } // Get key string value keyString = keyObject.Secret() - // Prepare the url and barcode - urlString := keyObject.String() - barcode, err := keyObject.Image(qrSize, qrSize) - if err != nil { - return logical.ErrorResponse("an error occured while generating a QR code image"), nil - } - - var buff bytes.Buffer - png.Encode(&buff, barcode) - b64Barcode := base64.StdEncoding.EncodeToString(buff.Bytes()) - response = logical.Response{ - Data: map[string]interface{}{ - "url": urlString, - "barcode": b64Barcode, - }, + // Skip returning the QR code and url if exported is set to false + if exported { + // Prepare the url and barcode + urlString := keyObject.String() + + // Don't include QR code is size is set to zero + if qrSize == 0 { + response = &logical.Response{ + Data: map[string]interface{}{ + "url": urlString, + }, + } + } else { + barcode, err := keyObject.Image(qrSize, qrSize) + if err != nil { + return logical.ErrorResponse("an error occured while generating a QR code image"), err + } + + var buff bytes.Buffer + png.Encode(&buff, barcode) + b64Barcode := base64.StdEncoding.EncodeToString(buff.Bytes()) + response = &logical.Response{ + Data: map[string]interface{}{ + "url": urlString, + "barcode": b64Barcode, + }, + } + } } default: if keyString == "" { @@ -372,12 +401,7 @@ func (b *backend) pathKeyCreate( return nil, err } - switch generate { - case true: - return &response, nil - default: - return nil, nil - } + return response, nil } type keyEntry struct { From 0023345c4e83fa4cc7ebef9f4b90eaac1c9fc69e Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Tue, 2 May 2017 20:27:01 -0700 Subject: [PATCH 42/43] Updated API documentation to inlude to exported variable and qr size option --- website/source/api/secret/totp/index.html.md | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/website/source/api/secret/totp/index.html.md b/website/source/api/secret/totp/index.html.md index 595819e4c7b6..3cc6e5b06e73 100644 --- a/website/source/api/secret/totp/index.html.md +++ b/website/source/api/secret/totp/index.html.md @@ -21,15 +21,17 @@ any location, please update your API calls accordingly. This endpoint creates or updates a key definition. -| Method | Path | Produces | -| :------- | :--------------------------- | :------------------------------------------------------------------------ | -| `POST` | `/totp/keys/:name` | if generating a key: `200 application/json` else: `204 (empty body)` | +| Method | Path | Produces | +| :------- | :--------------------------- | :--------------------------------------------------------------------------------------------- | +| `POST` | `/totp/keys/:name` | if generating a key and exported is true: `200 application/json` else: `204 (empty body)` | ### Parameters - `name` `(string: )` – Specifies the name of the key to create. This is specified as part of the URL. -- `generate` `(bool: false)` – Specifies if a key is generated by Vault or if a key is being passed from another service. +- `generate` `(bool: false)` – Specifies if a key should be generated by Vault or if a key is being passed from another service. + +- `exported` `(bool: true)` – Specifies if a QR code and url are returned upon generating a key. Only used if generate is true. - `key_size` `(int: 20)` – Specifies the size in bytes of the Vault generated key. Only used if generate is true. @@ -37,9 +39,9 @@ This endpoint creates or updates a key definition. - `key` `(string: )` – Specifies the master key used to generate a TOTP code. Only used if generate is false. -- `issuer` `(string: "" )` – Specifies the name of the key’s issuing organization. +- `issuer` `(string: "" )` – Specifies the name of the key’s issuing organization. -- `account_name` `(string: "" )` – Specifies the name of the account associated with the key. +- `account_name` `(string: "" )` – Specifies the name of the account associated with the key. - `period` `(int or duration format string: 30)` – Specifies the length of time in seconds used to generate a counter for the TOTP code calculation. @@ -47,9 +49,9 @@ This endpoint creates or updates a key definition. - `digits` `(int: 6)` – Specifies the number of digits in the generated TOTP code. This value can be set to 6 or 8. -- `skew` `(int: 0)` – Specifies the number of delay periods that are allowed when validating a TOTP code. This value can be either 0 or 1. Only used if generate is true. +- `skew` `(int: 1)` – Specifies the number of delay periods that are allowed when validating a TOTP code. This value can be either 0 or 1. Only used if generate is true. -- `qr_size` `(int: 200)` – Specifies the pixel size of the square QR code when generating a new key. Only used if generate is true. +- `qr_size` `(int: 200)` – Specifies the pixel size of the square QR code when generating a new key. Only used if generate is true and exported is true. If this value is 0, a QR code will not be returned. ### Sample Payload @@ -100,9 +102,7 @@ $ curl \ } ``` -### Embedding Barcodes in HTML - -To embed the generated barcode in a web page, include the base64 string in an `img` tag with the prefix `data:image/png;base64` +If a QR code is returned, it consists of base64-formatted PNG bytes. You can embed it in a web page by including the base64 string in an `img` tag with the prefix `data:image/png;base64` ``` From ecff38e801f4d5d079c0602bc94700a4d9f7a1c4 Mon Sep 17 00:00:00 2001 From: mymercurialsky Date: Wed, 3 May 2017 18:00:45 -0700 Subject: [PATCH 43/43] Cleaned up return statements in path_code, added error handling while validating codes and clarified documentation for generate parameters in path_keys --- builtin/logical/totp/path_code.go | 12 ++++++------ builtin/logical/totp/path_keys.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/builtin/logical/totp/path_code.go b/builtin/logical/totp/path_code.go index 63fc9c514c57..0481db145f1a 100644 --- a/builtin/logical/totp/path_code.go +++ b/builtin/logical/totp/path_code.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" + otplib "github.com/pquerna/otp" totplib "github.com/pquerna/otp/totp" ) @@ -57,13 +58,11 @@ func (b *backend) pathReadCode( } // Return the secret - resp, err := &logical.Response{ + return &logical.Response{ Data: map[string]interface{}{ "code": totpToken, }, }, nil - - return resp, nil } func (b *backend) pathValidateCode( @@ -91,14 +90,15 @@ func (b *backend) pathValidateCode( Digits: key.Digits, Algorithm: key.Algorithm, }) + if err != nil && err != otplib.ErrValidateInputInvalidLength { + return logical.ErrorResponse("an error occured while validating the code"), err + } - resp, err := &logical.Response{ + return &logical.Response{ Data: map[string]interface{}{ "valid": valid, }, }, nil - - return resp, nil } const pathCodeHelpSyn = ` diff --git a/builtin/logical/totp/path_keys.go b/builtin/logical/totp/path_keys.go index 58c5fa2f5d16..3f36aef0f503 100644 --- a/builtin/logical/totp/path_keys.go +++ b/builtin/logical/totp/path_keys.go @@ -41,7 +41,7 @@ func pathKeys(b *backend) *framework.Path { "generate": { Type: framework.TypeBool, Default: false, - Description: "Determines if a key is generated by vault or another service.", + Description: "Determines if a key should be generated by Vault or if a key is being passed from another service.", }, "exported": {