package main import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "encoding/json" "github.com/spf13/cobra" "golang.org/x/crypto/ssh/terminal" "io" "io/ioutil" "log" "os" "strings" "syscall" ) type ServerSettings struct { URL string Username string Password string } type Settings struct { EWS ServerSettings CalDAV ServerSettings Anonymize struct { Title string } } const settingsName = "calanonsync.json" const keyName = ".calanonsync.key" func LoadSettings() Settings { f, err := os.Open(settingsName) if err != nil { panic(err) } settings := Settings{} err = json.NewDecoder(f).Decode(&settings) if err != nil { panic(err) } if settings.CalDAV.URL != "" && !strings.HasSuffix(settings.CalDAV.URL, "/") { settings.CalDAV.URL += "/" } // Load a key if possible. var key []byte = nil keyString, err := ioutil.ReadFile(keyName) if err == nil { key, err = base64.StdEncoding.DecodeString(string(keyString)) if err != nil { log.Fatalf("Could not load encryption key: %s\n", err) } } else if !os.IsNotExist(err) { log.Fatalf("Could not load encryption key: %s\n", err) } ensurePassword := func(password *string, name string) { if *password == "" { print(name + " password: ") b, err := terminal.ReadPassword(int(syscall.Stdin)) println() if err != nil { panic(err) } *password = string(b) } else if key != nil { // Password already set. Since we have an encryption key, try to // decrypt the password. pwbytes, err := base64.StdEncoding.DecodeString(*password) if err != nil { log.Fatalf("Could not decode password: %s\n", err) } block, err := aes.NewCipher(key) if err != nil { log.Fatalf("Could not create cipher: %s\n", err) } if len(pwbytes) < block.BlockSize() { log.Fatalln("Could not decrypt password. Encrypted stream is too short.") } iv := pwbytes[:aes.BlockSize] result := pwbytes[aes.BlockSize:] stream := cipher.NewCFBDecrypter(block, iv) stream.XORKeyStream(result, result) *password = string(result) } } ensurePassword(&settings.EWS.Password, "EWS") ensurePassword(&settings.CalDAV.Password, "CalDAV") return settings } func InitSettingsCmd() *cobra.Command { settingsCmd := &cobra.Command{ Use: "settings", Short: "Manage settings.", } encryptCmd := &cobra.Command{ Use: "encrypt", Short: "Encrypt the passwords in the settings file.", Long: `This will encrypt the passwords in the settings file that are not empty. It will generate a new "master" password and store that alongside the settings file. This is NOT secure, it just helps to prevent over-the-shoulder "attacks".`, Run: runSettingsEncryption, } decryptCmd := &cobra.Command{ Use: "decrypt", Short: "Decrypt a previously encrypted settings file.", Run: runSettingsDecryption, } settingsCmd.AddCommand(encryptCmd, decryptCmd) return settingsCmd } func runSettingsEncryption(cmd *cobra.Command, args []string) { s := LoadSettings() if _, err := os.Stat(keyName); err == nil || os.IsExist(err) { log.Fatalln("Cannot encrypt an (apparently) already encrypted settings file. If this is an error, please remove .calanonsync.key and try again.") } // Generate a secure 256 bit key. key := make([]byte, 32) if n, err := rand.Read(key); n != 32 || err != nil { log.Fatalf("Could not get random 256 bit key: %s (%d)\n", err, n) } block, err := aes.NewCipher(key) if err != nil { log.Fatalf("Could not create cipher: %s\n", err) } doEncrypt := func(pwd *string) { if *pwd != "" { result := make([]byte, aes.BlockSize+len(*pwd)) // Prepare the initialization vector iv := result[:aes.BlockSize] if _, err := io.ReadFull(rand.Reader, iv); err != nil { panic(err) } stream := cipher.NewCFBEncrypter(block, iv) stream.XORKeyStream(result[aes.BlockSize:], []byte(*pwd)) *pwd = base64.StdEncoding.EncodeToString(result) } } doEncrypt(&s.EWS.Password) doEncrypt(&s.CalDAV.Password) if s.EWS.Password == "" && s.CalDAV.Password == "" { log.Fatalf("No passwords found. Nothing to encrypt.") } // Rewrite the settings file. f, err := os.OpenFile(settingsName, os.O_WRONLY|os.O_TRUNC, 0600) if err != nil { log.Fatalf("Could not rewrite settings: %s\n", err) } defer f.Close() e := json.NewEncoder(f) e.SetIndent("", " ") err = e.Encode(&s) if err != nil { panic(err) } f, err = os.OpenFile(keyName, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) if err != nil { log.Fatalf("Could not write keyfile: %s\n", err) } defer f.Close() ks := base64.StdEncoding.EncodeToString(key) _, err = f.WriteString(ks) if err != nil { panic(err) } log.Println("Settings encrypted") } func runSettingsDecryption(cmd *cobra.Command, args []string) { s := LoadSettings() // Rewrite the settings file. f, err := os.OpenFile(settingsName, os.O_WRONLY|os.O_TRUNC, 0600) if err != nil { log.Fatalf("Could not rewrite settings: %s\n", err) } defer f.Close() e := json.NewEncoder(f) e.SetIndent("", " ") err = e.Encode(&s) if err != nil { panic(err) } err = os.Remove(keyName) if err != nil { log.Fatalf("Could not remove key file: %s\n", err) } log.Println("Settings decrypted") }