Skip to content

Commit

Permalink
Merge pull request #2184 from Infisical/daniel/keyring-cli-improvements
Browse files Browse the repository at this point in the history
feat(cli): persistant `file` vault passphrase
  • Loading branch information
maidul98 authored Aug 5, 2024
2 parents 0bca24b + c8b93e4 commit e684882
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 51 deletions.
140 changes: 105 additions & 35 deletions cli/packages/cmd/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Copyright (c) 2023 Infisical Inc.
package cmd

import (
"encoding/base64"
"fmt"
"strings"

Expand All @@ -13,53 +14,56 @@ import (
"github.com/spf13/cobra"
)

var AvailableVaultsAndDescriptions = []string{"auto (automatically select native vault on system)", "file (encrypted file vault)"}
var AvailableVaults = []string{"auto", "file"}
type VaultBackendType struct {
Name string
Description string
}

var AvailableVaults = []VaultBackendType{
{
Name: "auto",
Description: "automatically select the system keyring",
},
{
Name: "file",
Description: "encrypted file vault",
},
}

var vaultSetCmd = &cobra.Command{
Example: `infisical vault set pass`,
Use: "set [vault-name]",
Short: "Used to set the vault backend to store your login details securely at rest",
Example: `infisical vault set file --passphrase <your-passphrase>`,
Use: "set [file|auto] [flags]",
Short: "Used to configure the vault backends",
DisableFlagsInUseLine: true,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
wantedVaultTypeName := args[0]
currentVaultBackend, err := util.GetCurrentVaultBackend()

vaultType := args[0]

passphrase, err := cmd.Flags().GetString("passphrase")
if err != nil {
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
return
util.HandleError(err, "Unable to get passphrase flag")
}

if wantedVaultTypeName == string(currentVaultBackend) {
log.Error().Msgf("You are already on vault backend [%s]", currentVaultBackend)
if vaultType == util.VAULT_BACKEND_FILE_MODE && passphrase != "" {
setFileVaultPassphrase(passphrase)
return
}

if wantedVaultTypeName == "auto" || wantedVaultTypeName == "file" {
configFile, err := util.GetConfigFile()
if err != nil {
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
return
}

configFile.VaultBackendType = wantedVaultTypeName // save selected vault
configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login

err = util.WriteConfigFile(&configFile)
if err != nil {
log.Error().Msgf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err)
return
}

fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]\n", currentVaultBackend, wantedVaultTypeName)

Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION))
} else {
log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(AvailableVaults, ", "))
}
util.PrintWarning("This command has been deprecated. Please use 'infisical vault use [file|auto]' to select which vault to use.\n")
selectVaultTypeCmd(cmd, args)
},
}

var vaultUseCmd = &cobra.Command{
Example: `infisical vault use [file|auto]`,
Use: "use [file|auto]",
Short: "Used to select the the type of vault backend to store sensitive data securely at rest",
DisableFlagsInUseLine: true,
Args: cobra.MinimumNArgs(1),
Run: selectVaultTypeCmd,
}

// runCmd represents the run command
var vaultCmd = &cobra.Command{
Use: "vault",
Expand All @@ -71,10 +75,30 @@ var vaultCmd = &cobra.Command{
},
}

func setFileVaultPassphrase(passphrase string) {
configFile, err := util.GetConfigFile()
if err != nil {
log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err)
return
}

// encode with base64
encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(passphrase))
configFile.VaultBackendPassphrase = encodedPassphrase

err = util.WriteConfigFile(&configFile)
if err != nil {
log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err)
return
}

util.PrintSuccessMessage("\nSuccessfully, set passphrase for file vault.\n")
}

func printAvailableVaultBackends() {
fmt.Printf("Vaults are used to securely store your login details locally. Available vaults:")
for _, backend := range AvailableVaultsAndDescriptions {
fmt.Printf("\n- %s", backend)
for _, vaultType := range AvailableVaults {
fmt.Printf("\n- %s (%s)", vaultType.Name, vaultType.Description)
}

currentVaultBackend, err := util.GetCurrentVaultBackend()
Expand All @@ -87,7 +111,53 @@ func printAvailableVaultBackends() {
fmt.Printf("\n\nYou are currently using [%s] vault to store your login credentials\n", string(currentVaultBackend))
}

func selectVaultTypeCmd(cmd *cobra.Command, args []string) {
wantedVaultTypeName := args[0]
currentVaultBackend, err := util.GetCurrentVaultBackend()
if err != nil {
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
return
}

if wantedVaultTypeName == string(currentVaultBackend) {
log.Error().Msgf("You are already on vault backend [%s]", currentVaultBackend)
return
}

if wantedVaultTypeName == util.VAULT_BACKEND_AUTO_MODE || wantedVaultTypeName == util.VAULT_BACKEND_FILE_MODE {
configFile, err := util.GetConfigFile()
if err != nil {
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
return
}

configFile.VaultBackendType = wantedVaultTypeName // save selected vault
configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login

err = util.WriteConfigFile(&configFile)
if err != nil {
log.Error().Msgf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err)
return
}

fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]\n", currentVaultBackend, wantedVaultTypeName)

Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION))
} else {
var availableVaultsNames []string
for _, vault := range AvailableVaults {
availableVaultsNames = append(availableVaultsNames, vault.Name)
}
log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(availableVaultsNames, ", "))
}
}

func init() {

vaultSetCmd.Flags().StringP("passphrase", "p", "", "Set the passphrase for the file vault")

vaultCmd.AddCommand(vaultSetCmd)
vaultCmd.AddCommand(vaultUseCmd)

rootCmd.AddCommand(vaultCmd)
}
9 changes: 5 additions & 4 deletions cli/packages/models/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ type UserCredentials struct {

// The file struct for Infisical config file
type ConfigFile struct {
LoggedInUserEmail string `json:"loggedInUserEmail"`
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
VaultBackendType string `json:"vaultBackendType,omitempty"`
LoggedInUserEmail string `json:"loggedInUserEmail"`
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
VaultBackendType string `json:"vaultBackendType,omitempty"`
VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"`
}

type LoggedInUser struct {
Expand Down
18 changes: 14 additions & 4 deletions cli/packages/util/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package util

import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -50,10 +51,11 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error {
}

configFile := models.ConfigFile{
LoggedInUserEmail: userCredentials.Email,
LoggedInUserDomain: config.INFISICAL_URL,
LoggedInUsers: existingConfigFile.LoggedInUsers,
VaultBackendType: existingConfigFile.VaultBackendType,
LoggedInUserEmail: userCredentials.Email,
LoggedInUserDomain: config.INFISICAL_URL,
LoggedInUsers: existingConfigFile.LoggedInUsers,
VaultBackendType: existingConfigFile.VaultBackendType,
VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase,
}

configFileMarshalled, err := json.Marshal(configFile)
Expand Down Expand Up @@ -215,6 +217,14 @@ func GetConfigFile() (models.ConfigFile, error) {
return models.ConfigFile{}, err
}

if configFile.VaultBackendPassphrase != "" {
decodedPassphrase, err := base64.StdEncoding.DecodeString(configFile.VaultBackendPassphrase)
if err != nil {
return models.ConfigFile{}, fmt.Errorf("GetConfigFile: Unable to decode base64 passphrase [err=%s]", err)
}
os.Setenv("INFISICAL_VAULT_FILE_PASSPHRASE", string(decodedPassphrase))
}

return configFile, nil
}

Expand Down
4 changes: 4 additions & 0 deletions cli/packages/util/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ const (
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN"
INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase.

VAULT_BACKEND_AUTO_MODE = "auto"
VAULT_BACKEND_FILE_MODE = "file"

// Universal Auth
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID"
Expand Down
52 changes: 48 additions & 4 deletions cli/packages/util/keyringwrapper.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package util

import (
"encoding/base64"

"github.com/manifoldco/promptui"
"github.com/zalando/go-keyring"
)

Expand All @@ -20,16 +23,51 @@ func SetValueInKeyring(key, value string) error {
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again")
}

return keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value)
err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value)

if err != nil {
configFile, _ := GetConfigFile()

if configFile.VaultBackendPassphrase == "" {
PrintWarning("System keyring could not be used, falling back to `file` vault for sensitive data storage.")
passphrasePrompt := promptui.Prompt{
Label: "Enter the passphrase to use for keyring encryption",
}
passphrase, err := passphrasePrompt.Run()
if err != nil {
return err
}

encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(passphrase))
configFile.VaultBackendPassphrase = encodedPassphrase
err = WriteConfigFile(&configFile)
if err != nil {
return err
}

// We call this function at last to trigger the environment variable to be set
GetConfigFile()
}

err = keyring.Set(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key, value)
}

return err
}

func GetValueInKeyring(key string) (string, error) {
currentVaultBackend, err := GetCurrentVaultBackend()
if err != nil {
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again")
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical reset] then try again")
}

value, err := keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key)

if err != nil {
value, err = keyring.Get(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key)
}
return value, err

return keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
}

func DeleteValueInKeyring(key string) error {
Expand All @@ -38,5 +76,11 @@ func DeleteValueInKeyring(key string) error {
return err
}

return keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
err = keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key)

if err != nil {
err = keyring.Delete(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key)
}

return err
}
6 changes: 3 additions & 3 deletions cli/packages/util/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ func GetCurrentVaultBackend() (string, error) {
}

if configFile.VaultBackendType == "" {
return "auto", nil
return VAULT_BACKEND_AUTO_MODE, nil
}

if configFile.VaultBackendType != "auto" && configFile.VaultBackendType != "file" {
return "auto", nil
if configFile.VaultBackendType != VAULT_BACKEND_AUTO_MODE && configFile.VaultBackendType != VAULT_BACKEND_FILE_MODE {
return VAULT_BACKEND_AUTO_MODE, nil
}

return configFile.VaultBackendType, nil
Expand Down
2 changes: 1 addition & 1 deletion docs/cli/commands/vault.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ description: "Change the vault type in Infisical"
To safeguard your login details when using the CLI, Infisical places them in a system vault or an encrypted text file, protected by a passphrase that only the user knows.
<Tip>To avoid constantly entering your passphrase when using the `file` vault type, set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable with your password in your shell</Tip>
<Tip>To avoid constantly entering your passphrase when using the `file` vault type, use the `infisical vault set file --passphrase <your-passphrase>` CLI command to specify your password once.</Tip>

0 comments on commit e684882

Please sign in to comment.