diff --git a/.gitignore b/.gitignore index 359f9e9fd..568a22982 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ /aws-vault /aws-vault-* +/aws-vault.app /SHA256SUMS +mac/aws-vault.app/Contents/*.provisionprofile +mac/entitlements.plist \ No newline at end of file diff --git a/Makefile b/Makefile index 976f4bd13..8b603396a 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,9 @@ VERSION=$(shell git describe --tags --candidates=1 --dirty) BUILD_FLAGS=-ldflags="-X main.Version=$(VERSION)" -trimpath CERT_ID ?= Developer ID Application: 99designs Inc (NRM9HVJ62Z) SRC=$(shell find . -name '*.go') go.mod +ARCH=$(shell uname -m) INSTALL_DIR ?= ~/bin +APP_PATH ?= ./aws-vault.app .PHONY: binaries clean release install ifeq ($(shell uname), Darwin) @@ -24,6 +26,11 @@ dmgs: aws-vault-darwin-amd64.dmg aws-vault-darwin-arm64.dmg clean: rm -f ./aws-vault ./aws-vault-*-* ./SHA256SUMS + rm -rf $(APP_PATH) + +app: clean aws-vault-darwin-$(ARCH) + ./bin/bundle-app aws-vault-darwin-$(ARCH) $(APP_PATH) + echo "Run at $(APP_PATH)/Contents/MacOS/aws-vault" release: binaries dmgs SHA256SUMS diff --git a/README.md b/README.md index 7066a6f16..ec3f2c5ab 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,9 @@ Config, usage, tips and tricks are available in the [USAGE.md](./USAGE.md) file. The supported vaulting backends are: + * [macOS Keychain](https://support.apple.com/en-au/guide/keychain-access/welcome/mac) +* (Beta) [macOS Data Protection Keychain](https://developer.apple.com/documentation/technotes/tn3137-on-mac-keychains#API-differences:~:text=File%2Dbased%20keychain-,Data%20protection%20keychain,-The%20file%2Dbased) (Touch ID, Apple Watch, etc) * [Windows Credential Manager](https://support.microsoft.com/en-au/help/4026814/windows-accessing-credential-manager) * Secret Service ([Gnome Keyring](https://wiki.gnome.org/Projects/GnomeKeyring), [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5)) * [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5) @@ -161,6 +163,12 @@ $ go build . $ codesign --sign ./aws-vault ``` +### Data Protection Keychain (macOS) + +If you wish to develop/test using the Data Protection Keychain (`dp-keychain`) (i.e. Touch ID) you will need to bundle and sign the app (each time you make changes). You cannot use this directly, even from a signed binary. It must exist in the app bundle as it needs the `embedded.provisionprofile` to access the keychain. + +For more information about setting up a test environment for the Data Protection Keychain [Read more here](./USAGE.md#developing-with-the-data-protection-keychain-ie-touch-id). + ## References and Inspiration * https://github.com/pda/aws-keychain diff --git a/USAGE.md b/USAGE.md index 298cf0cc6..ab180a0b8 100644 --- a/USAGE.md +++ b/USAGE.md @@ -16,11 +16,20 @@ - [Environment variables](#environment-variables) - [Backends](#backends) - [Keychain](#keychain) + - [(Beta) Data Protection Keychain](#beta-data-protection-keychain) + - [Using the Data Protection Keychain](#using-the-data-protection-keychain) + - [Keychain Access Control](#keychain-access-control) + - [Combining Access Controls](#combining-access-controls) + - [Updating Access Control](#updating-access-control) + - [Keychain Access Constraint](#keychain-access-constraint) + - [Access Constraints](#access-constraints) + - [Limitations of the Data Protection Keychain](#limitations-of-the-data-protection-keychain) - [Managing credentials](#managing-credentials) - [Using multiple profiles](#using-multiple-profiles) - [Listing profiles and credentials](#listing-profiles-and-credentials) - [Removing credentials](#removing-credentials) - [Rotating credentials](#rotating-credentials) + - [Copying credentials](#copying-credentials) - [Managing Sessions](#managing-sessions) - [Executing a command](#executing-a-command) - [Logging into AWS console](#logging-into-aws-console) @@ -45,7 +54,9 @@ - [Shell completion](#shell-completion) - [Desktop apps](#desktop-apps) - [Docker](#docker) - + - [Development](#development) + - [Developing with the Data Protection Keychain (i.e Touch ID)](#developing-with-the-data-protection-keychain-ie-touch-id) + - [Build Flow](#build-flow) ## Getting Help @@ -308,6 +319,10 @@ To override or set the source identity (used in `exec` and `login`): You can choose among different pluggable secret storage backends. You can set the backend using the `--backend` flag or the `AWS_VAULT_BACKEND` environment variable. Run `aws-vault --help` to see what your `--backend` flag supports. +There are two types of keychains in macOS: +- File-based keychain (`keychain`) - default for `aws-vault` +- Data Protection keychain (`dp-keychain`) + ### Keychain If you're looking to configure the amount of time between having to enter your Keychain password for each usage of a particular profile, you can do so through Keychain: @@ -320,6 +335,110 @@ If you're looking to configure the amount of time between having to enter your K ![keychain-image](https://imgur.com/ARkr5Ba.png) +### (Beta) Data Protection Keychain + +Apple have stated the Data Protection keychain [is more secure](https://developer.apple.com/documentation/technotes/tn3137-on-mac-keychains#API-differences) than the file-based keychain. And that whilst not offiical, the file-based keychain is [considered to be deprecated](https://developer.apple.com/documentation/technotes/tn3137-on-mac-keychains#API-differences:~:text=The%20file%2Dbased%20keychain%20is%20on%20the%20road%20to%20deprecation). + +The Data Protection keychain is encrypted with a key that is protected by the Secure Enclave, this is a hardware security feature of Apple devices. It means the keychain is only accessible when the device is unlocked and the user is authenticated. + +#### Using the Data Protection Keychain + +To keep backwards compatibility, the Data Protection Keychain is currently not the default backend. Instead a new backend `dp-keychain` has been created. + +To use the Data Protection Keychain, you can set the `AWS_VAULT_BACKEND` in your shell profile. For example, in your `~/.bash_profile` or `~/.zshrc`: + +```shell +$ vim ~/.bash_profile or ~/.zshrc +export AWS_VAULT_BACKEND=dp-keychain +``` + +It can also be used inline with the `--backend` flag: + +```shell +$ aws-vault --backend=dp-keychain exec my-profile +``` + +#### Keychain Access Control + +The level of protection can be configured for the Data Protection Keychain. These values are based on the underlying [Security Control Flags](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags) + +The protection level can be set to one of the following: + +| Access Control (`AWS_VAULT_ACCESS_CONTROL`) | Authentication Method | Description | +| --- | --- | --- | +| `BiometryAny` | ✅ Touch ID
❌ User Password | The user can authenticate with currently enrolled fingerprints or facial data, plus any newly enrolled biometric data. | +| `BiometryCurrentSet` | ✅ Touch ID
❌ User Password | The user can authenticate with currently enrolled fingerprints or facial data only. **New biometric data cannot be enrolled.** This is similar to how banking apps work on iOS. | +| `UserPresence` | ✅ Touch ID
✅ User Password | The user can authenticate with fingerprint or facial data, or by entering the device passcode. New biometric data can be enrolled. | +| `DevicePasscode` | ❌ Touch ID
✅ User Password | The user must authenticate with the device passcode. **Note: this is not recommended currently as you will need to enter your password to often** | +| `Watch` | ❌ Touch ID
❌ User Password
✅ Watch | The user must authenticate with the Apple Watch (running Watch OS 6 or later). | +| `ApplicationPassword` | ❌ Touch ID
❌ User Password
✅ Application Password | This is a specific password that is set for the application. Similar to how `keychain` works on `aws-vault`. | + +The default protection level is `UserPresence`. + +To set the protection level it is suggested you set the `AWS_VAULT_ACCESS_CONTROL` environment variable in your shell profile. For example, in your `~/.bash_profile` or `~/.zshrc`: + +```shell +$ vim ~/.bash_profile or ~/.zshrc +export AWS_VAULT_ACCESS_CONTROL=BiometryAny +``` + +##### Combining Access Controls + +Access controls can be combined using `And` or `Or` to create more complex access controls. + +**Require both Touch ID and a User Password:** + +```shell +BiometryCurrentSetAndDevicePasscode +``` + +**Require User Password, Current Biometric data and Watch:** + +```shell +BiometryCurrentSetAndWatchAndDevicePasscode +``` + +**To require either Touch ID or a Watch:** + +```shell +BiometryCurrentSetOrWatch +``` + +##### Updating Access Control + +It's not currently possible to retrospectively change access control settings for existing items in a keychain. Therefore if you change the `AWS_VAULT_ACCESS_CONTROL` environment value, keep in mind only new items will use the new access control settings. + +The only option around this would be to [delete the profile](#removing-credentials) and re-add it. + +#### Keychain Access Constraint + +To set the keychain access constraint, you can set the `AWS_VAULT_ACCESS_CONSTRAINT` environment value in your shell profile. + +```shell +export AWS_VAULT_ACCESS_CONSTRAINT=AccessibleWhenUnlocked +``` + +The default for this value is empty, which means the keychain access constraint is set to `AccessibleWhenUnlocked` as per the [Apple documentation](https://developer.apple.com/documentation/security/ksecattraccessiblewhenunlocked#:~:text=This%20is%20the%20default%20value%20for%20keychain%20items%20added%20without%20explicitly%20setting%20an%20accessibility%20constant.). + +Note: constraints are placed along with the access control settings. For example, `AccessibleWhenUnlocked` does not mean credentials can be accessed without authentication. It means the credentials can be retrieved via authentication when the device is unlocked. + +##### Access Constraints + +The keychain access constraint can be set to one of the following: + +| Access Constraint (`AWS_VAULT_ACCESS_CONSTRAINT`) | Description | +| --- | --- | +| `AccessibleWhenUnlocked` | Items with this attribute migrate to a new device when using encrypted backups. This is the `default value` for keychain items added without explicitly setting an accessibility constant. | +| `AccessibleAfterFirstUnlock` | After the first unlock, the data remains accessible until the next restart. This is recommended for items that need to be accessed by background applications. Items with this attribute migrate to a new device when using encrypted backups. | +| `AccessibleAfterFirstUnlockThisDeviceOnly` | After the first unlock, the data remains accessible until the next restart. This is recommended for items that need to be accessed by background applications. Items with this attribute do not migrate to a new device. Thus, after restoring from a backup of a different device, these items will not be present. | +| `AccessibleWhenPasscodeSetThisDeviceOnly` | This is recommended for items that only need to be accessible while the application is in the foreground. Items with this attribute never migrate to a new device. After a backup is restored to a new device, these items are missing. No items can be stored in this class on devices without a passcode. Disabling the device passcode causes all items in this class to be deleted. | +| `AccessibleWhenUnlockedThisDeviceOnly` | This is recommended for items that need to be accessible only while the application is in the foreground. Items with this attribute _do not_ migrate to a new device. Thus, after restoring from a backup of a different device, these items will not be present. | +| `AccessibleAlways` | @deprecated use `AccessibleWhenUnlockedThisDeviceOnly` instead | +| `AccessibleAccessibleAlwaysThisDeviceOnly` | @deprecated use `AccessibleWhenUnlockedThisDeviceOnly` instead | + +#### Limitations of the Data Protection Keychain + +Currently the Data Protection Keychain will prompt for biometrics every time you use `aws-vault`. There is no way to avoid this limitation. In saying that, Touch ID is incredibly fast so it outweighs the inconvenience of having to enter your password (every 5-10 mins when using the file-based keychain). ## Managing credentials @@ -416,6 +535,16 @@ The minimal IAM policy required to rotate your own credentials is: } ``` +### Copying credentials + +The `aws-vault copy` command can be used to copy all credentials (and sessions) from one keychain to another. + +```shell +# Copy AWS credentials/sessions from the "keychain" keychain to the "dp-keychain" keychain +$ aws-vault copy keychain dp-keychain +Copying credentials from keychain to dp-keychain +Copied 1 credentials, 0 OIDC tokens, and 1 sessions. +``` ## Managing Sessions @@ -743,3 +872,62 @@ To test it out: $ docker-compose run testapp testapp $ aws sts get-caller-identity ``` + +## Development + +### Developing with the Data Protection Keychain (i.e Touch ID) + +To use the data protection keychain (`dp-keychain`) and touch ID the app bundled and signed each time, otherwise you will receive -34018 keychain error. + +- **Create a certificate** + - [Generate a CSR](https://developer.apple.com/help/account/create-certificates/create-a-certificate-signing-request) + - [Create a new certificate](https://developer.apple.com/account/resources/certificates/add) + - Select "Developer ID Application" + +- **Setup an App ID** + - **[Register a new identifier](https://developer.apple.com/account/resources/identifiers/add/bundleId)** + - Select "App IDs" + - Select "App" + - Description "`aws-vault`" + - Bundle ID "`com.99designs.aws-vault`" (explicit) + +- **Setup a Provisioning Profile** + - **[Create a Distribution Profile](https://developer.apple.com/account/resources/profiles/add)** + - Distribution "Developer ID" + - Select App ID: "`aws-vault`" + - Select Certificate + - Provisioning Profile Name: "`aws-vault`" + - Generate + - Save to aws-vault repo in `mac/aws-vault.app/Contents/embedded.provisionprofile` + +- **Setup environment** + - `APPLE_DEVELOPER_ID`: + - You can find your Developer ID from [here](https://developer.apple.com/account#MembershipDetailsCard)) + - Set it in `~/.zshrc` (or equivalent) i.e. `export APPLE_DEVELOPER_ID=NRM9HVJ62Z` + - Or just set it in shell `APPLE_DEVELOPER_ID=NRM9HVJ62Z make app` + +### Build Flow + +To build the app, run `make app` you can then use the binary located in `./aws-vault.app/Contents/MacOS/aws-vault`. + +```shell +$ make app +(build details omitted) + +$ ./aws-vault.app/Contents/MacOS/aws-vault --backend=dp-keychain ls +Profile Credentials Sessions +======= =========== ======== +default - - +``` + +These two functions could become cumbersome whilst developing so you could merge them together like so: + +```shell +$ make app && ./aws-vault.app/Contents/MacOS/aws-vault --backend=dp-keychain ls + +(build details omitted) + +Profile Credentials Sessions +======= =========== ======== +default - - +``` \ No newline at end of file diff --git a/bin/bundle-app b/bin/bundle-app new file mode 100755 index 000000000..b4da17912 --- /dev/null +++ b/bin/bundle-app @@ -0,0 +1,30 @@ +#!/bin/bash + +set -euo pipefail + +BIN_PATH="$1" +SRC_PATH="$2" +APP_PATH=${APP_PATH:-"mac/aws-vault.app"} +APPLE_DEVELOPER_ID="${APPLE_DEVELOPER_ID:-"NRM9HVJ62Z"}" ## 99designs Inc as default +CERT_ID="${CERT_ID:-$APPLE_DEVELOPER_ID}" +CERT_ID="${CERT_ID:-"Developer ID Application: 99designs Inc (NRM9HVJ62Z)"}" +BUNDLE_ID="${BUNDLE_ID:-"com.99designs.aws-vault"}" + +if [ ! -f "$APP_PATH/Contents/embedded.provisionprofile" ]; then + echo "*.provisionprofile not found, please download profile from Apple Developer Portal and place it in $APP_PATH/Contents/embedded.provisionprofile" + exit 1 +fi + +if [[ ! -f "mac/entitlements.plist" ]] ; then + sed "s/APPLE_DEVELOPER_ID/$APPLE_DEVELOPER_ID/g" mac/entitlements.template.plist > mac/entitlements.plist +fi + +echo "Building app bundle" +cp -a $APP_PATH $SRC_PATH +mkdir -p $SRC_PATH/Contents/MacOS +cp -a $BIN_PATH $SRC_PATH/Contents/MacOS/aws-vault + +echo "App bundle built at $SRC_PATH" + +echo "Signing app" +codesign --options runtime --deep --entitlements "mac/entitlements.plist" --timestamp --sign "$CERT_ID" "$SRC_PATH" \ No newline at end of file diff --git a/bin/create-dmg b/bin/create-dmg index 96c2c2e57..983a34528 100755 --- a/bin/create-dmg +++ b/bin/create-dmg @@ -16,6 +16,7 @@ set -euo pipefail BIN_PATH="$1" DMG_PATH="${2:-$1.dmg}" +CERT_ID="${CERT_ID:-$APPLE_DEVELOPER_ID}" # Apple accepts developer ID also for --sign CERT_ID="${CERT_ID:-"Developer ID Application: 99designs Inc (NRM9HVJ62Z)"}" KEYCHAIN_PROFILE="${KEYCHAIN_PROFILE:-AC_PASSWORD}" @@ -27,11 +28,9 @@ fi tmpdir="$(mktemp -d)" trap "rm -rf $tmpdir" EXIT -cp -a $BIN_PATH $tmpdir/aws-vault -src_path="$tmpdir/aws-vault" +src_path="$tmpdir/aws-vault.app" -echo "Signing binary" -codesign --options runtime --timestamp --sign "$CERT_ID" "$src_path" +./bin/bundle-app "$BIN_PATH" "$src_path" echo "Creating dmg" hdiutil create -quiet -srcfolder "$src_path" "$DMG_PATH" @@ -43,4 +42,4 @@ echo "Submitting notorization request" xcrun notarytool submit $DMG_PATH --keychain-profile "$KEYCHAIN_PROFILE" --wait echo "Stapling" -xcrun stapler staple -q $DMG_PATH +xcrun stapler staple -q $DMG_PATH \ No newline at end of file diff --git a/cli/copy.go b/cli/copy.go new file mode 100644 index 000000000..e70a1a03a --- /dev/null +++ b/cli/copy.go @@ -0,0 +1,125 @@ +package cli + +import ( + "fmt" + "log" + + "github.com/99designs/aws-vault/v7/vault" + "github.com/99designs/keyring" + "github.com/alecthomas/kingpin/v2" +) + +type CopyCommandInput struct { + SourceBackend string + DestinationBackend string +} + +func ConfigureCopyCommand(app *kingpin.Application, a *AwsVault) { + input := CopyCommandInput{} + + cmd := app.Command("copy", "Copy credentials from one backend to another") + + cmd.Arg("src", "Name of the backend to move credentials from"). + Required(). + EnumVar(&input.SourceBackend, a.AvailableBackends()...) + + cmd.Arg("destination", "Name of the backend to move credentials to"). + Required(). + EnumVar(&input.DestinationBackend, a.AvailableBackends()...) + + cmd.Action(func(c *kingpin.ParseContext) (err error) { + src := &AwsVault{KeyringBackend: input.SourceBackend, KeyringConfig: a.KeyringConfig, Debug: a.Debug} + dest := &AwsVault{KeyringBackend: input.DestinationBackend, KeyringConfig: a.KeyringConfig, Debug: a.Debug} + + srcKeyring, err := src.Keyring() + if err != nil { + return err + } + + destKeyring, err := dest.Keyring() + if err != nil { + return err + } + + fmt.Printf("Copying credentials from %s to %s\n", input.SourceBackend, input.DestinationBackend) + + err = CopyCommand(input, srcKeyring, destKeyring) + app.FatalIfError(err, "Copy") + return nil + }) +} + +func CopyCommand(input CopyCommandInput, srcKeyring keyring.Keyring, destKeyring keyring.Keyring) error { + srcCredentialKeyring := &vault.CredentialKeyring{Keyring: srcKeyring} + srcOidcTokenKeyring := &vault.OIDCTokenKeyring{Keyring: srcCredentialKeyring.Keyring} + srcSessionKeyring := &vault.SessionKeyring{Keyring: srcCredentialKeyring.Keyring} + destCredentialKeyring := &vault.CredentialKeyring{Keyring: destKeyring} + destOidcTokenKeyring := &vault.OIDCTokenKeyring{Keyring: destCredentialKeyring.Keyring} + destSessionKeyring := &vault.SessionKeyring{Keyring: destCredentialKeyring.Keyring} + + srcCredentialNames, err := srcCredentialKeyring.Keys() + if err != nil { + return err + } + + srcOidcTokenNames, err := srcOidcTokenKeyring.Keys() + if err != nil { + return err + } + + srcSessionNames, err := srcSessionKeyring.Keys() + if err != nil { + return err + } + + log.Printf("Found %d credentials to copy", len(srcCredentialNames)) + log.Printf("Found %d OIDC tokens to copy", len(srcOidcTokenNames)) + log.Printf("Found %d sessions to copy", len(srcSessionNames)) + + for _, credentialName := range srcCredentialNames { + creds, err := srcCredentialKeyring.Get(credentialName) + if err != nil { + return err + } + + log.Printf("Copying %s", credentialName) + + err = destCredentialKeyring.Set(credentialName, creds) + if err != nil { + return err + } + } + + for _, oidcTokenName := range srcOidcTokenNames { + oidcToken, err := srcOidcTokenKeyring.Get(oidcTokenName) + if err != nil { + return err + } + + log.Printf("Copying %s", oidcTokenName) + + err = destOidcTokenKeyring.Set(oidcTokenName, oidcToken) + if err != nil { + log.Printf("Error copying %s: %s", oidcTokenName, err) + return err + } + } + + for _, sessionName := range srcSessionNames { + session, err := srcSessionKeyring.Get(sessionName) + if err != nil { + return err + } + + log.Printf("Copying %s", sessionName) + + err = destSessionKeyring.Set(sessionName, session) + if err != nil { + return err + } + } + + fmt.Printf("Copied %d credentials, %d OIDC tokens, and %d sessions.\n", len(srcCredentialNames), len(srcOidcTokenNames), len(srcSessionNames)) + + return nil +} diff --git a/cli/global.go b/cli/global.go index 10561f82d..5dd208361 100644 --- a/cli/global.go +++ b/cli/global.go @@ -5,6 +5,7 @@ import ( "io" "log" "os" + "regexp" "strings" "github.com/99designs/aws-vault/v7/prompt" @@ -30,11 +31,15 @@ type AwsVault struct { KeyringConfig keyring.Config KeyringBackend string promptDriver string + accessControl string keyringImpl keyring.Keyring awsConfigFile *vault.ConfigFile } +var accessControlOptions = []string{"UserPresence", "BiometryCurrentSet", "BiometryAnySet", "DevicePasscode", "Watch", "ApplicationPassword"} +var accessConstraintOptions = []string{"", "AccessibleWhenUnlocked", "AccessibleAfterFirstUnlock", "AccessibleAfterFirstUnlockThisDeviceOnly", "AccessibleWhenPasscodeSetThisDeviceOnly", "AccessibleWhenUnlockedThisDeviceOnly"} + func isATerminal() bool { fd := os.Stdout.Fd() return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd) @@ -94,16 +99,21 @@ func (a *AwsVault) MustGetProfileNames() []string { return config.ProfileNames() } -func ConfigureGlobals(app *kingpin.Application) *AwsVault { - a := &AwsVault{ - KeyringConfig: keyringConfigDefaults, - } - +// Get available backends +func (a *AwsVault) AvailableBackends() []string { backendsAvailable := []string{} for _, backendType := range keyring.AvailableBackends() { backendsAvailable = append(backendsAvailable, string(backendType)) } + return backendsAvailable +} + +func ConfigureGlobals(app *kingpin.Application) *AwsVault { + a := &AwsVault{ + KeyringConfig: keyringConfigDefaults, + } + backendsAvailable := a.AvailableBackends() promptsAvailable := prompt.Available() app.Flag("debug", "Show debugging output"). @@ -162,6 +172,43 @@ func ConfigureGlobals(app *kingpin.Application) *AwsVault { Envar("AWS_VAULT_FILE_DIR"). StringVar(&a.KeyringConfig.FileDir) + app.Flag("access-control", "Access Control Settings for the Data Protection Keychain \"dp-keychain\" backend"). + Default("UserPresence"). + Envar("AWS_VAULT_ACCESS_CONTROL"). + StringVar(&a.accessControl) + + app.Flag("access-constraint", "Access Control Settings for the Data Protection Keychain \"dp-keychain\" backend"). + Default(""). + Envar("AWS_VAULT_ACCESS_CONSTRAINT"). + EnumVar(&a.KeyringConfig.KeychainAccessConstraint, accessConstraintOptions...) + + app.Validate(func(app *kingpin.Application) error { + // Ensure that current keyring backend is supported + if a.KeyringBackend != "dp-keychain" && a.KeyringConfig.KeychainAccessConstraint != "" { + return fmt.Errorf("--access-control is not supported with the backend '%s', only 'dp-keychain' is supported", a.KeyringBackend) + } + + if a.KeyringBackend != "dp-keychain" && a.accessControl != "UserPresence" { + return fmt.Errorf("--access-control is not supported with the backend '%s', only 'dp-keychain' is supported", a.KeyringBackend) + } + + log.Printf("Using keyring backend: %s", a.KeyringBackend) + log.Printf("Using access control: %s", a.accessControl) + + if a.KeyringConfig.KeychainAccessConstraint != "" { + log.Printf("Using access constraint: %s", a.KeyringConfig.KeychainAccessConstraint) + } + + terms, err := validateAccessControls(a) + if err != nil { + return err + } + + a.KeyringConfig.KeychainAccessControl = terms + + return nil + }) + app.PreAction(func(c *kingpin.ParseContext) error { if !a.Debug { log.SetOutput(io.Discard) @@ -174,6 +221,44 @@ func ConfigureGlobals(app *kingpin.Application) *AwsVault { return a } +func validateAccessControls(a *AwsVault) ([]string, error) { + validTerms := accessControlOptions + validTermsPattern := strings.Join(validTerms, "|") + + // Regex for checking structure + pattern := fmt.Sprintf(`^(%s)(?:\s*(And|Or)\s*(%s))*$`, validTermsPattern, validTermsPattern) + regex := regexp.MustCompile(pattern) + + if !regex.MatchString(a.accessControl) { + return nil, fmt.Errorf("invalid access control setting: '%s'", a.accessControl) + } + + // Split the string by 'And' or 'Or' to check for repeats + splitRegex := regexp.MustCompile(`\s*(And|Or)\s*`) + terms := splitRegex.Split(a.accessControl, -1) + + // Map to track occurrences of terms + seen := make(map[string]bool) + for _, term := range terms { + normalizedTerm := strings.TrimSpace(term) + if seen[normalizedTerm] { + return nil, fmt.Errorf("repeated access control term: '%s'", normalizedTerm) + } + seen[normalizedTerm] = true + } + + return terms, nil +} + +func StringInSlice(str string, list []string) bool { + for _, v := range list { + if v == str { + return true + } + } + return false +} + func fileKeyringPassphrasePrompt(prompt string) (string, error) { if password, ok := os.LookupEnv("AWS_VAULT_FILE_PASSPHRASE"); ok { return password, nil diff --git a/cli/global_test.go b/cli/global_test.go new file mode 100644 index 000000000..65ebfbcbf --- /dev/null +++ b/cli/global_test.go @@ -0,0 +1,44 @@ +package cli + +import ( + "github.com/alecthomas/kingpin/v2" + "testing" +) + +// Assuming ConfigureGlobals is a function that configures global flags and returns some struct +// containing global variables or configuration settings. +func setupCLI() *kingpin.Application { + app := kingpin.New("aws-vault", "A tool for securely managing AWS keys") + // Here you would set up your flags, commands, and any validation hooks + ConfigureGlobals(app) // Assuming this function sets up your flags and validation + return app +} + +func TestCheckAccessControlValidation(t *testing.T) { + cases := []struct { + name string + args []string + wantErr bool + }{ + {"ValidInput", []string{"--backend", "dp-keychain", "--access-control", "UserPresenceAndBiometryAnySet"}, false}, + {"InvalidKeyringBackend", []string{"--backend", "keychain", "--access-control", "UserPresence"}, false}, + {"InvalidAccessControl", []string{"--backend", "dp-keychain", "--access-control", "UserPresenceAndInvalid"}, true}, + {"ConjunctionAtStart", []string{"--backend", "dp-keychain", "--access-control", "AndUserPresence"}, true}, + {"ConjunctionAtEnd", []string{"--backend", "dp-keychain", "--access-control", "AndUserPresence"}, true}, + {"InvalidCasing", []string{"--backend", "dp-keychain", "--access-control", "userpresence"}, true}, + {"InvalidConjunctions", []string{"--backend", "dp-keychain", "--access-control", "UserPresence,Watch"}, true}, + {"RepeatTerms", []string{"--backend", "dp-keychain", "--access-control", "UserPresenceAndUserPresence"}, true}, + {"RepeatConjunctions", []string{"--backend", "dp-keychain", "--access-control", "UserPresenceAndAndWatch"}, true}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + app := kingpin.New("aws-vault", "A tool for securely managing AWS keys") + ConfigureGlobals(app) + _, err := app.Parse(tc.args) + if (tc.wantErr && err == nil) || (!tc.wantErr && err != nil) { + t.Errorf("CheckAccessControlValidation() for %s: unexpected error status: %v", tc.name, err) + } + }) + } +} diff --git a/go.mod b/go.mod index 70522e8f0..974fe4f38 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,12 @@ require ( github.com/dvsekhvalnov/jose2go v1.5.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect golang.org/x/sys v0.6.0 // indirect ) + +replace github.com/99designs/keyring => github.com/alexw23/keyring v0.0.0-20240507145515-95e6dc96a98e + +replace github.com/keybase/go-keychain => github.com/alexw23/go-keychain v0.0.0-20240507145345-41efe171240e diff --git a/go.sum b/go.sum index 605baf9a8..f04de6fc8 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,13 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= -github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= -github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU= github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/alexw23/go-keychain v0.0.0-20240507145345-41efe171240e h1:tr4NMs+H918AUrqOpYkjfeZDg7554ufuQXS/Ego/JRU= +github.com/alexw23/go-keychain v0.0.0-20240507145345-41efe171240e/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= +github.com/alexw23/keyring v0.0.0-20240507145515-95e6dc96a98e h1:48fwulC9rkaq78lFJISTWPVk/QQPiE8pxAGeVD7eza8= +github.com/alexw23/keyring v0.0.0-20240507145515-95e6dc96a98e/go.mod h1:HjTFTjKPDlsb9gorNND5dq3eR0f3lfGVutBDeH/m8is= github.com/aws/aws-sdk-go-v2 v1.17.7 h1:CLSjnhJSTSogvqUGhIC6LqFKATMRexcxLZ0i/Nzk9Eg= github.com/aws/aws-sdk-go-v2 v1.17.7/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= github.com/aws/aws-sdk-go-v2/config v1.18.19 h1:AqFK6zFNtq4i1EYu+eC7lcKHYnZagMn6SW171la0bGw= @@ -66,11 +68,11 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/mac/aws-vault.app/Contents/Info.plist b/mac/aws-vault.app/Contents/Info.plist new file mode 100644 index 000000000..3a47f02c6 --- /dev/null +++ b/mac/aws-vault.app/Contents/Info.plist @@ -0,0 +1,14 @@ + + + + + CFBundleExecutable + aws-vault + CFBundleIdentifier + com.99designs.aws-vault + CFBundleVersion + 1.0 + CFBundleShortVersionString + 1.0.0 + + \ No newline at end of file diff --git a/mac/entitlements.template.plist b/mac/entitlements.template.plist new file mode 100644 index 000000000..205f02b8e --- /dev/null +++ b/mac/entitlements.template.plist @@ -0,0 +1,12 @@ + + + + + keychain-access-groups + + APPLE_DEVELOPER_ID.BUNDLE_ID + + com.apple.security.generic-keychain-access + + + \ No newline at end of file diff --git a/main.go b/main.go index fc6b1f708..ef4072430 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,7 @@ func main() { cli.ConfigureClearCommand(app, a) cli.ConfigureLoginCommand(app, a) cli.ConfigureProxyCommand(app) + cli.ConfigureCopyCommand(app, a) kingpin.MustParse(app.Parse(os.Args[1:])) }