token

package module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 16, 2025 License: Apache-2.0 Imports: 7 Imported by: 0

README

Convert DynamoDB last evaluated key to opaque token; create and validate CSRF tokens

Go Reference

This library was born out of my need to encrypt the map[string]AttributeValue last evaluated key from my DynamoDB Query or Scan operations before passing it as the pagination token to the caller, though the library has grown to support any []byte token. ChaCha20-Poly1305 (preferred) and AES with GCM encryption are available, and you can either provide a key statically, or from AWS Secrets Manager to get rotation support for free.

Usage

Get with:

go get github.com/nguyengg/go-aws-commons/opaque-token
Fixed key with ChaCha20-Poly1305 or AES encryption

Binary secret of valid ChaCha20-Poly1305 key size (256-bit) or AES key sizes (128-bit, 192-bit, or 256-bit) must be given at construction time. Use this version if you're just testing out or aren't worried about having some impact when rotating the secret (i.e. you can take some downtime, or it's a personal project where traffic is low or impact is not business critical).

package main

import (
	"context"
	"crypto/rand"
	"io"

	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
	"github.com/nguyengg/go-aws-commons/opaque-token/ddb"
)

func main() {
	ctx := context.Background()
	cfg, _ := config.LoadDefaultConfig(ctx)
	client := dynamodb.NewFromConfig(cfg)
	queryOutputItem, _ := client.Query(ctx, &dynamodb.QueryInput{})

	key := make([]byte, 32)
	_, _ = io.ReadFull(rand.Reader, key)
	c, _ := ddb.New(ddb.WithChaCha20Poly1305(key))

	// continuationToken is an opaque token that can be returned to user without leaking details about the table.
	continuationToken, _ := c.EncodeKey(ctx, queryOutputItem.LastEvaluatedKey)

	// to decrypt the opaque token and use it as exclusive start key in Query or Scan.
	exclusiveStartKey, _ := c.DecodeToken(ctx, continuationToken)
	_, _ = client.Query(ctx, &dynamodb.QueryInput{ExclusiveStartKey: exclusiveStartKey})
}

Key from AWS Secrets Manager

AES key is retrieved from AWS Secrets Manager instead. Because each secret in AWS Secrets Manager has a version Id, this pair of encoder/decoder will prefix the version Id to the opaque token (since the secret name and AWS account and region are not leaked, this should be OK). Be mindful of the cost of calling AWS Secrets Manager for every invocation. If running in AWS Lambda functions, you can make use of Dynamic key with AWS Secrets Manager in Lambda functions.

package main

import (
	"context"
	"crypto/rand"
	"io"

	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
	"github.com/nguyengg/go-aws-commons/opaque-token"
)

func main() {
	ctx := context.Background()
	cfg, _ := config.LoadDefaultConfig(ctx)
	client := dynamodb.NewFromConfig(cfg)
	queryOutputItem, _ := client.Query(ctx, &dynamodb.QueryInput{})

	key := make([]byte, 32)
	_, _ = io.ReadFull(rand.Reader, key)
	c, _ := token.NewDynamoDBKeyConverter(token.WithChaCha20Poly1305(key))

	// continuationToken is an opaque token that can be returned to user without leaking details about the table.
	continuationToken, _ := c.EncodeKey(ctx, queryOutputItem.LastEvaluatedKey)

	// to decrypt the opaque token and use it as exclusive start key in Query or Scan.
	exclusiveStartKey, _ := c.DecodeToken(ctx, continuationToken)
	_, _ = client.Query(ctx, &dynamodb.QueryInput{ExclusiveStartKey: exclusiveStartKey})
}

Key from AWS Parameters and Secrets Lambda Extension

If running in AWS Lambda, this pair of encoder/decoder can make use of the AWS Parameters and Secrets Lambda Extension (https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html) instead of directly using Secrets Manager SDK.

package main

import (
	"context"

	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
	"github.com/nguyengg/go-aws-commons/opaque-token"
)

func main() {
	ctx := context.Background()
	cfg, _ := config.LoadDefaultConfig(ctx)
	client := dynamodb.NewFromConfig(cfg)
	queryOutputItem, _ := client.Query(ctx, &dynamodb.QueryInput{})

	c, _ := token.NewDynamoDBKeyConverter(token.WithKeyFromLambdaExtensionSecrets("my-secret-id"))

	// continuationToken is an opaque token that can be returned to user without leaking details about the table.
	// the token includes the plaintext version id so that DecodeToken knows which key to use.
	continuationToken, _ := c.EncodeKey(ctx, queryOutputItem.LastEvaluatedKey)
	exclusiveStartKey, _ := c.DecodeToken(ctx, continuationToken)
	_, _ = client.Query(ctx, &dynamodb.QueryInput{ExclusiveStartKey: exclusiveStartKey})
}

HMAC and CSRF token generation and verification

The module also provides way to sign and verify payload. To make the signature a suitable CSRF token, be sure to pass a non-zero nonce size for anti-collision purposes, while also including the session id or any other session-dependent value according to https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#pseudo-code-for-implementing-hmac-csrf-tokens.

package main

import (
	"context"

	"github.com/nguyengg/go-aws-commons/opaque-token/hmac"
)

func main() {
	ctx := context.Background()

	// you can use `hasher.WithKey` or `hasher.WithKeyFromSecretsManager` as well.
	signer := hmac.New(hmac.WithKeyFromLambdaExtensionSecrets("my-secret-id"))

	// to get a stable hash (same input produces same output), pass 0 for nonce size.
	payload := []byte("hello, world")
	signature, _ := signer.Sign(ctx, payload, 0)
	ok, _ := signer.Verify(ctx, signature, payload)
	if !ok {
		panic("signature verification fails")
	}

	// to use the signature as CSRF token, include session-dependent value according to
	// https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#pseudo-code-for-implementing-hmac-csrf-tokens.
	// don't add a random value in the payload; by passing non-zero nonce size, the generated token will already
	// include a nonce for anti-collision purposes.
	payload = []byte("84266fdbd31d4c2c6d0665f7e8380fa3")
	signature, _ = signer.Sign(ctx, payload, 16)
	ok, _ = signer.Verify(ctx, signature, payload)
	if !ok {
		panic("CSRF verification fails")
	}
}

Key Rotation

To create a new 32-byte binary secret
file=$(mktemp)
openssl rand 32 > "${file}"
aws secretsmanager create-secret --name my-secret-name --secret-binary "fileb://${file}"
rm "${file}"

To update an existing binary secret
file=$(mktemp)
openssl rand 32 > "${file}"
aws secretsmanager put-secret-value --name my-secret-name --secret-binary "fileb://${file}"
rm "${file}"

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type DynamoDBKeyConverter

type DynamoDBKeyConverter struct {
	// Endec controls how the tokens are encrypted/decrypted.
	//
	// By default, there is no encryption. Prefer NewDynamoDBKeyConverter instead.
	Endec endec.Endec

	// EncodeToString controls how the decrypted binary token is encoded to string.
	//
	// If Endec is not nil, [base64.RawURLEncoding.EncodeToString] will be used as the default EncodeToString.
	// If Endec is nil, EncodeToString is used only if EncodeToString is non-nil.
	EncodeToString func([]byte) string

	// DecodeString controls how the encrypted string token is decoded.
	//
	// If Endec is not nil, [base64.RawURLEncoding.DecodeString] will be used as the default DecodeString.
	// If Endec is nil, DecodeString is used only if DecodeString is non-nil.
	DecodeString func(string) ([]byte, error)
}

DynamoDBKeyConverter converts from DynamoDB's last evaluated key to pagination token and vice versa, intended to be used for query and scan operations.

Per specifications, only three data types (S, N, or B) can be partition key or sort key. The pagination token will be the DynamoDB JSON blob of the evaluated key, which should have no more than 2 entries.

The zero value struct is ready for use which will encode/decode keys without any encryption. Prefer NewDynamoDBKeyConverter instead which provides ways to encrypt/decrypt the token, making it the token opaque.

func NewDynamoDBKeyConverter

func NewDynamoDBKeyConverter(opt EncryptionOption, optFns ...func(*DynamoDBKeyConverter)) (*DynamoDBKeyConverter, error)

NewDynamoDBKeyConverter returns a new DynamoDBKeyConverter that uses encryption/decryption to produce opaque tokens.

If you have static key, pass WithAES or WithChaCha20Poly1305. If you want to retrieve secret binary from AWS Secrets Hasher, pass WithKeyFromSecretsManager. If you are running in AWS Lambda with AWS Parameters and Secrets Lambda Extension (https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html) enabled, pass WithKeyFromLambdaExtensionSecrets.

func (DynamoDBKeyConverter) DecodeToken

func (c DynamoDBKeyConverter) DecodeToken(ctx context.Context, token string) (key map[string]types.AttributeValue, err error)

DecodeToken decodes the given opaque token to an exclusive start key.

func (DynamoDBKeyConverter) EncodeKey

EncodeKey encodes the given last evaluated key to an opaque token.

type EncryptionOption

type EncryptionOption func(*options) error

EncryptionOption makes it easy to specify both the secret key and the encryption algorithm in a user-friendly manner.

func WithAES

func WithAES(key []byte) EncryptionOption

WithAES makes the DynamoDBKeyConverter uses WithAES encryption with the given key.

func WithChaCha20Poly1305

func WithChaCha20Poly1305(key []byte) EncryptionOption

WithChaCha20Poly1305 makes the DynamoDBKeyConverter uses ChaCha20-Poly1305 encryption with the given key.

func WithKeyFromLambdaExtensionSecrets

func WithKeyFromLambdaExtensionSecrets(secretId string, optFns ...func(*endec.SecretsManagerEndecOptions)) EncryptionOption

WithKeyFromLambdaExtensionSecrets makes the DynamoDBKeyConverter uses key from AWS Parameters and Secrets Lambda Extension (https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html) using the default client lambda.DefaultParameterSecretsExtensionClient.

If you want to change the encryption suite or customises the endec.SecretsManagerEndec further, see endec.SecretsManagerEndecOptions.

func WithKeyFromSecretsManager

func WithKeyFromSecretsManager(client endec.GetSecretValueAPIClient, secretId string, optFns ...func(*endec.SecretsManagerEndecOptions)) EncryptionOption

WithKeyFromSecretsManager makes the DynamoDBKeyConverter uses key from AWS Secrets Manager.

If you want to change the encryption suite or customises the endec.SecretsManagerEndec further, see endec.SecretsManagerEndecOptions.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL