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 }