github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/cmd/config.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path"
    11  
    12  	"github.com/cozy/cozy-stack/client/request"
    13  	"github.com/cozy/cozy-stack/model/account"
    14  	"github.com/cozy/cozy-stack/pkg/config/config"
    15  	"github.com/cozy/cozy-stack/pkg/crypto"
    16  	"github.com/cozy/cozy-stack/pkg/keyring"
    17  	"github.com/cozy/cozy-stack/pkg/utils"
    18  	"github.com/spf13/cobra"
    19  	"golang.org/x/term"
    20  )
    21  
    22  var configCmdGroup = &cobra.Command{
    23  	Use:   "config <command>",
    24  	Short: "Show and manage configuration elements",
    25  	Long:  `cozy-stack config allows to print and generate some parts of the configuration`,
    26  }
    27  
    28  var adminPasswdCmd = &cobra.Command{
    29  	Use:     "passwd <filepath>",
    30  	Aliases: []string{"password", "passphrase", "pass"},
    31  	Short:   "Generate an admin passphrase",
    32  	Long: `
    33  cozy-stack config passwd generates a passphrase hash and save it to the
    34  specified file. If no file is specified, it is directly printed in standard
    35  output. This passphrase is the one used to authenticate accesses to the
    36  administration API.
    37  
    38  The environment variable 'COZY_ADMIN_PASSPHRASE' can be used to pass the
    39  passphrase if needed.
    40  `,
    41  	Example: "$ cozy-stack config passwd ~/.cozy/cozy-admin-passphrase",
    42  	RunE: func(cmd *cobra.Command, args []string) error {
    43  		if len(args) > 1 {
    44  			return cmd.Usage()
    45  		}
    46  		var filename string
    47  		if len(args) == 1 {
    48  			filename = path.Clean(utils.AbsPath(args[0]))
    49  			ok, err := utils.DirExists(filename)
    50  			if err == nil && ok {
    51  				filename = path.Join(filename, config.GetConfig().AdminSecretFileName)
    52  			}
    53  		}
    54  
    55  		if filename != "" {
    56  			errPrintfln("Hashed passphrase will be written in %s", filename)
    57  		}
    58  
    59  		passphrase := []byte(os.Getenv("COZY_ADMIN_PASSPHRASE"))
    60  		if len(passphrase) == 0 {
    61  			errPrintf("Passphrase: ")
    62  			pass1, err := term.ReadPassword(int(os.Stdin.Fd()))
    63  			errPrintfln("")
    64  			if err != nil {
    65  				return err
    66  			}
    67  
    68  			errPrintf("Confirmation: ")
    69  			pass2, err := term.ReadPassword(int(os.Stdin.Fd()))
    70  			errPrintfln("")
    71  			if err != nil {
    72  				return err
    73  			}
    74  			if !bytes.Equal(pass1, pass2) {
    75  				return fmt.Errorf("Passphrase missmatch")
    76  			}
    77  			if len(pass1) == 0 {
    78  				return fmt.Errorf("Empty password is forbidden")
    79  			}
    80  
    81  			passphrase = pass1
    82  		}
    83  
    84  		b, err := crypto.GenerateFromPassphrase(passphrase)
    85  		if err != nil {
    86  			return err
    87  		}
    88  
    89  		var out io.Writer
    90  		if filename != "" {
    91  			var f *os.File
    92  			f, err = os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0440)
    93  			if err != nil {
    94  				return err
    95  			}
    96  			defer f.Close()
    97  
    98  			if err = os.Chmod(filename, 0440); err != nil {
    99  				return err
   100  			}
   101  
   102  			out = f
   103  		} else {
   104  			out = os.Stdout
   105  		}
   106  
   107  		_, err = fmt.Fprintln(out, string(b))
   108  		return err
   109  	},
   110  }
   111  
   112  var genKeysCmd = &cobra.Command{
   113  	Use:   "gen-keys <filepath>",
   114  	Short: "Generate an key pair for encryption and decryption of credentials",
   115  	Long: `
   116  cozy-stack config gen-keys generate a key-pair and save them in the specified path.
   117  
   118  The decryptor key filename is given the ".dec" extension suffix.
   119  The encryptor key filename is given the ".enc" extension suffix.
   120  
   121  The files permissions are 0400.`,
   122  
   123  	Example: `$ cozy-stack config gen-keys ~/credentials-key
   124  keyfiles written in:
   125  	~/credentials-key.enc
   126  	~/credentials-key.dec
   127  `,
   128  	RunE: func(cmd *cobra.Command, args []string) error {
   129  		if len(args) != 1 {
   130  			return cmd.Usage()
   131  		}
   132  
   133  		filename := path.Clean(utils.AbsPath(args[0]))
   134  		encryptorFilename := filename + ".enc"
   135  		decryptorFilename := filename + ".dec"
   136  
   137  		marshaledEncryptorKey, marshaledDecryptorKey, err := keyring.GenerateEncodedNACLKeyPair()
   138  		if err != nil {
   139  			return nil
   140  		}
   141  
   142  		if err = writeFile(encryptorFilename, marshaledEncryptorKey, 0400); err != nil {
   143  			return err
   144  		}
   145  		if err = writeFile(decryptorFilename, marshaledDecryptorKey, 0400); err != nil {
   146  			return err
   147  		}
   148  		errPrintfln("keyfiles written in:\n  %s\n  %s", encryptorFilename, decryptorFilename)
   149  		return nil
   150  	},
   151  }
   152  
   153  var encryptCredentialsDataCmd = &cobra.Command{
   154  	Use:   "encrypt-data <encoding keyfile> <text>",
   155  	Short: "Encrypt data with the specified encryption keyfile.",
   156  	Long:  `cozy-stack config encrypt-data encrypts any valid JSON data`,
   157  	Example: `
   158  $ ./cozy-stack config encrypt-data ~/.cozy/key.enc "{\"foo\": \"bar\"}"
   159  $ bmFjbNFjY+XZkS26YtVPUIKKm/JdnAGwG30n6A4ypS1p1dHev8hOtaRbW+lGneoO7PS9JCW8U5GSXhASu+c3UkaZ
   160  `,
   161  	RunE: func(cmd *cobra.Command, args []string) error {
   162  		if len(args) != 2 {
   163  			return cmd.Usage()
   164  		}
   165  
   166  		// Check if we have good-formatted JSON
   167  		var result map[string]interface{}
   168  		err := json.Unmarshal([]byte(args[1]), &result)
   169  		if err != nil {
   170  			return err
   171  		}
   172  
   173  		encKeyStruct, err := readKeyFromFile(args[0])
   174  		if err != nil {
   175  			return err
   176  		}
   177  		dataEncrypted, err := account.EncryptBufferWithKey(encKeyStruct, []byte(args[1]))
   178  		if err != nil {
   179  			return err
   180  		}
   181  		data := base64.StdEncoding.EncodeToString(dataEncrypted)
   182  		fmt.Fprintf(os.Stdout, "%s\n", data)
   183  
   184  		return nil
   185  	},
   186  }
   187  
   188  var decryptCredentialsDataCmd = &cobra.Command{
   189  	Use:   "decrypt-data <decoding keyfile> <ciphertext>",
   190  	Short: "Decrypt data with the specified decryption keyfile.",
   191  	RunE: func(cmd *cobra.Command, args []string) error {
   192  		if len(args) != 2 {
   193  			return cmd.Usage()
   194  		}
   195  
   196  		decKeyStruct, err := readKeyFromFile(args[0])
   197  		if err != nil {
   198  			return err
   199  		}
   200  
   201  		dataEncrypted, err := base64.StdEncoding.DecodeString(args[1])
   202  		if err != nil {
   203  			return err
   204  		}
   205  		decrypted, err := account.DecryptBufferWithKey(decKeyStruct, dataEncrypted)
   206  		if err != nil {
   207  			return err
   208  		}
   209  
   210  		fmt.Fprintf(os.Stdout, "%s\n", decrypted)
   211  
   212  		return nil
   213  	},
   214  }
   215  
   216  var encryptCredentialsCmd = &cobra.Command{
   217  	Use:     "encrypt-creds <keyfile> <login> <password>",
   218  	Aliases: []string{"encrypt-credentials"},
   219  	Short:   "Encrypt the given credentials with the specified decryption keyfile.",
   220  	RunE: func(cmd *cobra.Command, args []string) error {
   221  		if len(args) != 3 {
   222  			return cmd.Usage()
   223  		}
   224  
   225  		credsEncryptor, err := readKeyFromFile(args[0])
   226  		if err != nil {
   227  			return err
   228  		}
   229  
   230  		encryptedCreds, err := account.EncryptCredentialsWithKey(credsEncryptor, args[1], args[2])
   231  		if err != nil {
   232  			return err
   233  		}
   234  		fmt.Fprintf(os.Stdout, "Encrypted credentials: %s\n", encryptedCreds)
   235  		return nil
   236  	},
   237  }
   238  
   239  var decryptCredentialsCmd = &cobra.Command{
   240  	Use:     "decrypt-creds <keyfile> <ciphertext>",
   241  	Aliases: []string{"decrypt-credentials"},
   242  	Short:   "Decrypt the given credentials cipher text with the specified decryption keyfile.",
   243  	RunE: func(cmd *cobra.Command, args []string) error {
   244  		if len(args) != 2 {
   245  			return cmd.Usage()
   246  		}
   247  
   248  		credsDecryptor, err := readKeyFromFile(args[0])
   249  		if err != nil {
   250  			return err
   251  		}
   252  
   253  		credentialsEncrypted, err := base64.StdEncoding.DecodeString(args[1])
   254  		if err != nil {
   255  			return fmt.Errorf("Cipher text is not properly base64 encoded: %s", err)
   256  		}
   257  
   258  		login, password, err := account.DecryptCredentialsWithKey(credsDecryptor, credentialsEncrypted)
   259  		if err != nil {
   260  			return fmt.Errorf("Could not decrypt cipher text: %s", err)
   261  		}
   262  
   263  		fmt.Fprintf(os.Stdout, `Decrypted credentials:
   264  login:    %q
   265  password: %q
   266  `, login, password)
   267  
   268  		return nil
   269  	},
   270  }
   271  
   272  func writeFile(filename string, data []byte, perm os.FileMode) error {
   273  	f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm)
   274  	if err != nil {
   275  		return err
   276  	}
   277  	n, err := f.Write(data)
   278  	if err == nil && n < len(data) {
   279  		err = io.ErrShortWrite
   280  	}
   281  	if err1 := f.Close(); err == nil {
   282  		err = err1
   283  	}
   284  	return err
   285  }
   286  
   287  func readKeyFromFile(filepath string) (*keyring.NACLKey, error) {
   288  	keyBytes, err := os.ReadFile(filepath)
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  
   293  	return keyring.UnmarshalNACLKey(keyBytes)
   294  }
   295  
   296  var insertAssetCmd = &cobra.Command{
   297  	Use:   "insert-asset --url <url> --name <name> --shasum <shasum> --context <context>",
   298  	Short: "Inserts an asset",
   299  	Long: `Inserts a custom asset in a specific context
   300  
   301  Deprecated: please use the command cozy-stack assets add.
   302  `,
   303  	RunE: func(cmd *cobra.Command, args []string) error {
   304  		errPrintfln("Please use cozy-stack assets add, this command has been deprecated")
   305  		return addAsset(cmd, args)
   306  	},
   307  }
   308  
   309  var removeAssetCmd = &cobra.Command{
   310  	Use:   "rm-asset [context] [name]",
   311  	Short: "Removes an asset",
   312  	Long: `Removes a custom asset in a specific context
   313  
   314  Deprecated: please use the command cozy-stack assets rm.
   315  `,
   316  	RunE: func(cmd *cobra.Command, args []string) error {
   317  		errPrintfln("Please use cozy-stack assets rm, this command has been deprecated")
   318  		return rmAsset(cmd, args)
   319  	},
   320  }
   321  
   322  var listAssetCmd = &cobra.Command{
   323  	Use:   "ls-assets",
   324  	Short: "List assets",
   325  	Long: `List assets currently served by the stack
   326  
   327  Deprecated: please use the command cozy-stack assets ls.
   328  `,
   329  	RunE: func(cmd *cobra.Command, args []string) error {
   330  		errPrintfln("Please use cozy-stack assets ls, this command has been deprecated")
   331  		return lsAssets(cmd, args)
   332  	},
   333  }
   334  
   335  var showContextCmd = &cobra.Command{
   336  	Use:     "show-context",
   337  	Short:   "Show a context",
   338  	Example: "$ cozy-stack config show-context cozy_demo",
   339  	RunE: func(cmd *cobra.Command, args []string) error {
   340  		if len(args) < 1 {
   341  			return cmd.Usage()
   342  		}
   343  		ac := newAdminClient()
   344  		req := &request.Options{
   345  			Method: "GET",
   346  			Path:   "instances/contexts/" + args[0],
   347  		}
   348  		res, err := ac.Req(req)
   349  		if err != nil {
   350  			return err
   351  		}
   352  		defer res.Body.Close()
   353  
   354  		var v interface{}
   355  
   356  		err = json.NewDecoder(res.Body).Decode(&v)
   357  		if err != nil {
   358  			return err
   359  		}
   360  
   361  		json, err := json.MarshalIndent(v, "", "  ")
   362  		if err != nil {
   363  			return err
   364  		}
   365  
   366  		fmt.Println(string(json))
   367  		return nil
   368  	},
   369  }
   370  
   371  var listContextsCmd = &cobra.Command{
   372  	Use:     "ls-contexts",
   373  	Aliases: []string{"list-contexts"},
   374  	Short:   "List contexts",
   375  	Long:    "List contexts currently used by the stack",
   376  	Example: "$ cozy-stack config ls-contexts",
   377  	RunE: func(cmd *cobra.Command, args []string) error {
   378  		ac := newAdminClient()
   379  		req := &request.Options{
   380  			Method: "GET",
   381  			Path:   "instances/contexts",
   382  		}
   383  		res, err := ac.Req(req)
   384  		if err != nil {
   385  			return err
   386  		}
   387  		defer res.Body.Close()
   388  
   389  		var v interface{}
   390  
   391  		err = json.NewDecoder(res.Body).Decode(&v)
   392  		if err != nil {
   393  			return err
   394  		}
   395  
   396  		json, err := json.MarshalIndent(v, "", "  ")
   397  		if err != nil {
   398  			return err
   399  		}
   400  
   401  		fmt.Println(string(json))
   402  		return nil
   403  	},
   404  }
   405  
   406  func init() {
   407  	configCmdGroup.AddCommand(adminPasswdCmd)
   408  	configCmdGroup.AddCommand(genKeysCmd)
   409  	configCmdGroup.AddCommand(encryptCredentialsDataCmd)
   410  	configCmdGroup.AddCommand(decryptCredentialsDataCmd)
   411  	configCmdGroup.AddCommand(encryptCredentialsCmd)
   412  	configCmdGroup.AddCommand(decryptCredentialsCmd)
   413  	configCmdGroup.AddCommand(insertAssetCmd)
   414  	configCmdGroup.AddCommand(listAssetCmd)
   415  	configCmdGroup.AddCommand(removeAssetCmd)
   416  	configCmdGroup.AddCommand(showContextCmd)
   417  	configCmdGroup.AddCommand(listContextsCmd)
   418  	RootCmd.AddCommand(configCmdGroup)
   419  	insertAssetCmd.Flags().StringVar(&flagURL, "url", "", "The URL of the asset")
   420  	insertAssetCmd.Flags().StringVar(&flagName, "name", "", "The name of the asset")
   421  	insertAssetCmd.Flags().StringVar(&flagShasum, "shasum", "", "The shasum of the asset")
   422  	insertAssetCmd.Flags().StringVar(&flagContext, "context", "", "The context of the asset")
   423  }