Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 4 additions & 15 deletions cipher/aes.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,20 @@ import (
)

type (
AesKeyFunc func(*[AesKeySize]byte) error
AesKey [32]byte

AesGcm struct {
key [AesKeySize]byte
key AesKey
}
)

const (
AesKeySize = 32
)

var (
ErrCipher = errors.New("cipher failure")
)

// Non-nil returned error wraps [ErrCipher].
func NewAesGcm(fn AesKeyFunc) (*AesGcm, error) {
aes := AesGcm{}

err := fn(&aes.key)
if err != nil {
return nil, fmt.Errorf("%w: failed to get encryption key: %s", ErrCipher, err.Error())
}

return &aes, nil
func NewAesGcm(key AesKey) *AesGcm {
return &AesGcm{key: key}
}

// Non-nil returned error wraps [ErrCipher].
Expand Down
65 changes: 45 additions & 20 deletions cmd/toolkit-assume-role/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ package main

import (
"context"
"errors"
"flag"
"fmt"
"os"

"github.com/aws/aws-sdk-go-v2/config"

"github.com/kxue43/cli-toolkit/creds"
"github.com/kxue43/cli-toolkit/key"
"github.com/kxue43/cli-toolkit/terminal"
)

var (
cmd = creds.AssumeRoleCmd{}
input = creds.ProcessInput{}

helpMsg = `Usage: %s -mfa-serial=STRING -profile=STRING [flags] <RoleArn>

Expand All @@ -27,11 +29,11 @@ Flags:
)

func registerFlagsAndHelp() {
flag.StringVar(&cmd.MFASerial, "mfa-serial", "", "ARN of the virtual MFA to use when assuming the role.")
flag.StringVar(&cmd.Profile, "profile", "", "Source profile used for assuming the role.")
flag.StringVar(&cmd.Region, "region", "us-east-1", "The regional STS service endpoint to call.")
flag.StringVar(&cmd.RoleSessionName, "role-session-name", "ToolkitCLI", "Role session name.")
flag.Int64Var(&cmd.DurationSeconds, "duration-seconds", 3600, "Role session duration seconds.")
flag.StringVar(&input.MFASerial, "mfa-serial", "", "ARN of the virtual MFA to use when assuming the role.")
flag.StringVar(&input.Profile, "profile", "", "Source profile used for assuming the role.")
flag.StringVar(&input.Region, "region", "us-east-1", "The regional STS service endpoint to call.")
flag.StringVar(&input.RoleSessionName, "role-session-name", "ToolkitCLI", "Role session name.")
flag.Int64Var(&input.DurationSeconds, "duration-seconds", 3600, "Role session duration seconds.")

flag.Usage = func() {
_, _ = fmt.Fprintf(flag.CommandLine.Output(), helpMsg, os.Args[0])
Expand All @@ -40,15 +42,31 @@ func registerFlagsAndHelp() {
}
}

func validateInput(input creds.ProcessInput) error {
if input.MFASerial == "" {
return errors.New("-mfa-serial is required")
}

if input.Profile == "" {
return errors.New("-profile is required")
}

if input.DurationSeconds > 14400 {
return errors.New("-duration-seconds cannot exceed 14400, i.e. 4 hours")
}

if input.RoleArn == "" {
return errors.New("the <RoleArn> argument is required")
}

return nil
}

func main() {
exitCode := 0

defer func() { os.Exit(exitCode) }()

registerFlagsAndHelp()

flag.Parse()

device, err := os.OpenFile("/dev/tty", os.O_RDWR|os.O_SYNC, 0600)
if err != nil {
exitCode = 1
Expand All @@ -65,18 +83,26 @@ func main() {
}
}()

ctx := context.Background()
registerFlagsAndHelp()

flag.Parse()

if args := flag.Args(); len(args) > 0 {
input.RoleArn = args[0]
}

err = cmd.ValidateInputs(flag.Args())
err = validateInput(input)
if err != nil {
tty.Print(err.Error())
tty.Println(err.Error())

exitCode = 1

return
}

cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(cmd.Profile), config.WithRegion(cmd.Region))
ctx := context.Background()

cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(input.Profile), config.WithRegion(input.Region))
if err != nil {
tty.Printf("failed to load AWS SDK configuration: %s\n", err)

Expand All @@ -85,14 +111,13 @@ func main() {
return
}

err = cmd.Init(tty, cfg)
if err != nil {
tty.Print(err.Error())
}
kp := key.NewKeyringProvider("kxue43.toolkit.assume-role", "cache-encryption-key")

processor := creds.NewProcessor(input, tty, cfg, kp)

err = cmd.Run(ctx, os.Stdout)
err = processor.Run(ctx, os.Stdout)
if err != nil {
tty.Print(err.Error())
tty.Println(err.Error())

exitCode = 1

Expand Down
28 changes: 12 additions & 16 deletions creds/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,6 @@ import (
)

type (
CredentialProcessOutput struct {
AccessKeyId string `json:"AccessKeyId"`
SecretAccessKey string `json:"SecretAccessKey"`
SessionToken string `json:"SessionToken"`
Expiration string `json:"Expiration"`
Version int `json:"Version"`
}

cacher struct {
logger logger
cipher *cipher.AesGcm
Expand Down Expand Up @@ -86,17 +78,21 @@ func decodeFromFileName(roleArn, fileName string) (ts time.Time, err error) {
}

// Non-nil returned error wraps [ErrCacheInit].
func newCacher(logger logger, fn cipher.AesKeyFunc) (*cacher, error) {
aes, err := cipher.NewAesGcm(fn)
func newCacher(logger logger, kp KeyProvider) (*cacher, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrCacheInit, err.Error())
return nil, fmt.Errorf("%w: could not locate user home directory", ErrCacheInit)
}

home, err := os.UserHomeDir()
var key cipher.AesKey

err = kp.Write(key[:])
if err != nil {
return nil, fmt.Errorf("%w: could not locate user home directory", ErrCacheInit)
return nil, fmt.Errorf("%w: failed to obtain encryption key for cache file: %s", ErrCacheInit, err.Error())
}

aes := cipher.NewAesGcm(key)

cacheDir := filepath.Join(home, ".aws", "toolkit-cache")

info, err := os.Stat(cacheDir)
Expand All @@ -119,7 +115,7 @@ func newCacher(logger logger, fn cipher.AesKeyFunc) (*cacher, error) {

// Non-nil returned error wraps [ErrInvalidCredential] or [ErrCacheSave].
// contents is valid for use as long as it's not nil.
func (c *cacher) Save(roleArn string, output *CredentialProcessOutput) (contents []byte, err error) {
func (c *cacher) save(roleArn string, output *ProcessOutput) (contents []byte, err error) {
ts, err := time.Parse(time.RFC3339, output.Expiration)
if err != nil {
return nil, fmt.Errorf("%w: expiration %q is not of the right format: %s", ErrInvalidCredential, output.Expiration, err.Error())
Expand All @@ -144,9 +140,9 @@ func (c *cacher) Save(roleArn string, output *CredentialProcessOutput) (contents
return contents, nil
}

// Retrieve tries to retrieve AWS credentials from cache files.
// retrieve tries to retrieve AWS credentials from cache files.
// It succeeded if and only if the returned byte slice is not nil.
func (c *cacher) Retrieve(roleArn string) (contents []byte) {
func (c *cacher) retrieve(roleArn string) (contents []byte) {
max := time.Now().Add(time.Minute * 10)
actives := make(cacheFileSlice, 0)
pattern := filepath.Join(c.cacheDir, fmt.Sprintf(`%s-*`, getPrefix(roleArn)))
Expand Down
Loading