diff --git a/src/calanonsync/settings.go b/src/calanonsync/settings.go index 4e05816..4e11ffd 100644 --- a/src/calanonsync/settings.go +++ b/src/calanonsync/settings.go @@ -1,10 +1,16 @@ package main import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" "encoding/json" "fmt" "github.com/spf13/cobra" "golang.org/x/crypto/ssh/terminal" + "io" + "log" "os" "strings" "syscall" @@ -24,6 +30,9 @@ type Settings struct { } } +const settingsName = "calanonsync.json" +const keyName = ".calanonsync.key" + func ensurePassword(password *string, name string) { if *password != "" { // Nothing to do. Password already set. @@ -41,7 +50,7 @@ func ensurePassword(password *string, name string) { } func LoadSettings() Settings { - f, err := os.Open("calanonsync.json") + f, err := os.Open(settingsName) if err != nil { panic(err) } @@ -74,19 +83,83 @@ func InitSettingsCmd() *cobra.Command { 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: func(cmd *cobra.Command, args []string) { - fmt.Println("Encrypt") - }, + Run: runSettingsEncryption, } decryptCmd := &cobra.Command{ Use: "decrypt", Short: "Decrypt a previously encrypted settings file.", - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Decrypt") - }, + 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) + } +} + +func runSettingsDecryption(cmd *cobra.Command, args []string) { + fmt.Println("Decrypt") +}