CalAnonSync/src/calanonsync/settings.go

221 lines
5.1 KiB
Go

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")
}