vault/builtin/logical/totp/path_keys.go
hashicorp-copywrite[bot] 0b12cdcfd1
[COMPLIANCE] License changes (#22290)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License.

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* add missing license headers

* Update copyright file headers to BUS-1.1

* Fix test that expected exact offset on hcl file

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Sarah Thompson <sthompson@hashicorp.com>
Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
2023-08-10 18:14:03 -07:00

456 lines
12 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package totp
import (
"bytes"
"context"
"encoding/base32"
"encoding/base64"
"fmt"
"image/png"
"net/url"
"strconv"
"strings"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
otplib "github.com/pquerna/otp"
totplib "github.com/pquerna/otp/totp"
)
func pathListKeys(b *backend) *framework.Path {
return &framework.Path{
Pattern: "keys/?$",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixTOTP,
OperationSuffix: "keys",
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ListOperation: b.pathKeyList,
},
HelpSynopsis: pathKeyHelpSyn,
HelpDescription: pathKeyHelpDesc,
}
}
func pathKeys(b *backend) *framework.Path {
return &framework.Path{
Pattern: "keys/" + framework.GenericNameWithAtRegex("name"),
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixTOTP,
OperationSuffix: "key",
},
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: "Name of the key.",
},
"generate": {
Type: framework.TypeBool,
Default: false,
Description: "Determines if a key should be generated by Vault or if a key is being passed from 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,
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. Only used if generate is false.",
},
"issuer": {
Type: framework.TypeString,
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.`,
},
"period": {
Type: framework.TypeDurationSecond,
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. 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.`,
},
"skew": {
Type: framework.TypeInt,
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 and exported is true. If this value is 0, a QR code will not be returned.`,
},
"url": {
Type: framework.TypeString,
Description: `A TOTP url string containing all of the parameters for key setup. Only used if generate is false.`,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathKeyRead,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "read",
},
},
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathKeyCreate,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "create",
},
},
logical.DeleteOperation: &framework.PathOperation{
Callback: b.pathKeyDelete,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "delete",
},
},
},
HelpSynopsis: pathKeyHelpSyn,
HelpDescription: pathKeyHelpDesc,
}
}
func (b *backend) Key(ctx context.Context, s logical.Storage, n string) (*keyEntry, error) {
entry, err := s.Get(ctx, "key/"+n)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result keyEntry
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (b *backend) pathKeyDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
err := req.Storage.Delete(ctx, "key/"+data.Get("name").(string))
if err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathKeyRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
key, err := b.Key(ctx, req.Storage, data.Get("name").(string))
if err != nil {
return nil, err
}
if key == nil {
return nil, 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(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
entries, err := req.Storage.List(ctx, "key/")
if err != nil {
return nil, err
}
return logical.ListResponse(entries), nil
}
func (b *backend) pathKeyCreate(ctx context.Context, 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)
period := data.Get("period").(int)
algorithm := data.Get("algorithm").(string)
digits := data.Get("digits").(int)
skew := data.Get("skew").(int)
qrSize := data.Get("qr_size").(int)
keySize := data.Get("key_size").(int)
inputURL := data.Get("url").(string)
if generate {
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 occurred while parsing url string"), err
}
// 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 := urlQuery.Get("period")
if periodQuery != "" {
periodInt, err := strconv.Atoi(periodQuery)
if err != nil {
return logical.ErrorResponse("an error occurred while parsing period value in url"), err
}
period = periodInt
}
// Read digits
digitsQuery := urlQuery.Get("digits")
if digitsQuery != "" {
digitsInt, err := strconv.Atoi(digitsQuery)
if err != nil {
return logical.ErrorResponse("an error occurred while parsing digits value in url"), err
}
digits = digitsInt
}
// 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
switch digits {
case 6:
keyDigits = otplib.DigitsSix
case 8:
keyDigits = otplib.DigitsEight
default:
return logical.ErrorResponse("the digits value can only be 6 or 8"), nil
}
var keyAlgorithm otplib.Algorithm
switch algorithm {
case "SHA1":
keyAlgorithm = otplib.AlgorithmSHA1
case "SHA256":
keyAlgorithm = otplib.AlgorithmSHA256
case "SHA512":
keyAlgorithm = otplib.AlgorithmSHA512
default:
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
}
switch skew {
case 0:
case 1:
default:
return logical.ErrorResponse("the skew value must be 0 or 1"), 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 {
return logical.ErrorResponse("the key_size value must be greater than zero"), nil
}
// Period, Skew and Key Size need to be unsigned ints
uintPeriod := uint(period)
uintSkew := uint(skew)
uintKeySize := uint(keySize)
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,
AccountName: accountName,
Period: uintPeriod,
Digits: keyDigits,
Algorithm: keyAlgorithm,
SecretSize: uintKeySize,
Rand: b.GetRandomReader(),
})
if err != nil {
return logical.ErrorResponse("an error occurred while generating a key"), err
}
// Get key string value
keyString = keyObject.Secret()
// 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 if 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 nil, fmt.Errorf("failed to generate QR code image: %w", 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 == "" {
return logical.ErrorResponse("the key value is required"), nil
}
if i := len(keyString) % 8; i != 0 {
keyString += strings.Repeat("=", 8-i)
}
_, err := base32.StdEncoding.DecodeString(strings.ToUpper(keyString))
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"invalid key value: %s", err)), nil
}
}
// Store it
entry, err := logical.StorageEntryJSON("key/"+name, &keyEntry{
Key: keyString,
Issuer: issuer,
AccountName: accountName,
Period: uintPeriod,
Algorithm: keyAlgorithm,
Digits: keyDigits,
Skew: uintSkew,
})
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
}
return response, nil
}
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"`
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"`
}
const pathKeyHelpSyn = `
Manage the keys that can be created with this backend.
`
const pathKeyHelpDesc = `
This path lets you manage the keys that can be created with this backend.
`