From 45c62839c99a6a29b7f919d5659393aa2814e4ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sun, 9 Jul 2023 13:55:30 +0200 Subject: [PATCH 01/17] [ADD] Pkg cipher --- pkg/cipher/cipher.go | 48 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 pkg/cipher/cipher.go diff --git a/pkg/cipher/cipher.go b/pkg/cipher/cipher.go new file mode 100644 index 0000000..eb758a4 --- /dev/null +++ b/pkg/cipher/cipher.go @@ -0,0 +1,48 @@ +package cipher + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "errors" + "io" +) + +func Encrypt(key, plaintext []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + ciphertext := make([]byte, aes.BlockSize+len(plaintext)) + iv := ciphertext[:aes.BlockSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, err + } + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext) + return ciphertext, nil +} + +func Decrypt(key, ciphertext []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + if len(ciphertext) < aes.BlockSize { + return nil, errors.New("ciphertext too short") + } + iv := ciphertext[:aes.BlockSize] + ciphertext = ciphertext[aes.BlockSize:] + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(ciphertext, ciphertext) + return ciphertext, nil +} + +func GenerateRandomKey() ([]byte, error) { + key := make([]byte, 32) + _, err := rand.Read(key) + if err != nil { + return nil, err + } + return key, nil +} From 7f84eb5de11fef179acd0c10bc4ca14e1315ef52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sat, 2 Sep 2023 17:05:36 +0200 Subject: [PATCH 02/17] [ADD] Security funcs --- pkg/security/security.go | 58 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 pkg/security/security.go diff --git a/pkg/security/security.go b/pkg/security/security.go new file mode 100644 index 0000000..70f1ded --- /dev/null +++ b/pkg/security/security.go @@ -0,0 +1,58 @@ +package security + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "io" +) + +func Encrypt(key []byte, plaintext string) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + // 96-bit nonce + nonce := make([]byte, 12) + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + return "", err + } + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + return hex.EncodeToString(append(nonce, aesgcm.Seal(nil, nonce, []byte(plaintext), nil)...)), nil +} + +func Decrypt(key []byte, ciphertext string) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + // Extract nonce from the first 24 bytes + nonce, err := hex.DecodeString(ciphertext[:24]) + if err != nil { + return "", err + } + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + decrypted, err := aesgcm.Open(nil, nonce, []byte(ciphertext[24:]), nil) + if err != nil { + return "", err + } + return string(decrypted), nil +} + +func GenerateRandomKey(length int) (string, error) { + randomBytes := make([]byte, length) + _, err := rand.Read(randomBytes) + if err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(randomBytes), nil +} From 31cb468ee10b1d4ea6b98e57a3d41e03de43ef62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sat, 2 Sep 2023 17:09:51 +0200 Subject: [PATCH 03/17] [ADD] Symmetric key --- pkg/config/config.go | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 5aef045..92c7d2a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -9,14 +9,16 @@ import ( "sync" log "github.com/plaenkler/ddns-updater/pkg/logging" + "github.com/plaenkler/ddns-updater/pkg/security" "gopkg.in/yaml.v3" ) type Config struct { - Interval uint64 `yaml:"Interval"` - UseTOTP bool `yaml:"TOTP"` - Port uint64 `yaml:"Port"` - Resolver string `yaml:"Resolver"` + Interval uint64 `yaml:"Interval"` + UseTOTP bool `yaml:"TOTP"` + Port uint64 `yaml:"Port"` + Resolver string `yaml:"Resolver"` + Encryptor string `yaml:"Encryptor"` } const ( @@ -64,13 +66,18 @@ func load() error { } func create() error { + encryptor, err := security.GenerateRandomKey(8) + if err != nil { + return err + } config := Config{ - Interval: 600, - UseTOTP: false, - Port: 80, - Resolver: "", + Interval: 600, + UseTOTP: false, + Port: 80, + Resolver: "", + Encryptor: encryptor, } - err := os.MkdirAll(filepath.Dir(pathToConfig), dirPerm) + err = os.MkdirAll(filepath.Dir(pathToConfig), dirPerm) if err != nil { return err } @@ -115,6 +122,10 @@ func loadFromEnv() error { if resolver != "" { config.Resolver = resolver } + encryptor, ok := os.LookupEnv("DDNS_ENCRYPTOR") + if ok && encryptor != "" { + config.Encryptor = encryptor + } return nil } From 2e6fb7e4e3fc5dd3e41916fd1b52a795c67e2c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sun, 3 Sep 2023 01:40:21 +0200 Subject: [PATCH 04/17] [UPD] Use AES-GCM --- pkg/cipher/cipher.go | 65 ++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/pkg/cipher/cipher.go b/pkg/cipher/cipher.go index eb758a4..1fc8f5a 100644 --- a/pkg/cipher/cipher.go +++ b/pkg/cipher/cipher.go @@ -4,45 +4,58 @@ import ( "crypto/aes" "crypto/cipher" "crypto/rand" - "errors" + "encoding/base64" + "fmt" "io" ) -func Encrypt(key, plaintext []byte) ([]byte, error) { - block, err := aes.NewCipher(key) +func Encrypt(key string, plaintext string) (string, error) { + encrypter, err := aes.NewCipher([]byte(key)) if err != nil { - return nil, err + return "", fmt.Errorf("[cipher-Encrypt-1] encryption failed: %s", err) } - ciphertext := make([]byte, aes.BlockSize+len(plaintext)) - iv := ciphertext[:aes.BlockSize] - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return nil, err + gcm, err := cipher.NewGCM(encrypter) + if err != nil { + return "", fmt.Errorf("[cipher-Encrypt-2] encryption failed: %s", err) + } + nonce := make([]byte, gcm.NonceSize()) + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + return "", fmt.Errorf("[cipher-Encrypt-3] encryption failed: %s", err) } - mode := cipher.NewCBCEncrypter(block, iv) - mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext) - return ciphertext, nil + return base64.StdEncoding.EncodeToString(gcm.Seal(nonce, nonce, []byte(plaintext), nil)), nil } -func Decrypt(key, ciphertext []byte) ([]byte, error) { - block, err := aes.NewCipher(key) +func Decrypt(key string, ciphertext string) (string, error) { + c, err := aes.NewCipher([]byte(key)) + if err != nil { + return "", fmt.Errorf("[cipher-Decrypt-1] decryption failed: %s", err) + } + gcm, err := cipher.NewGCM(c) if err != nil { - return nil, err + return "", fmt.Errorf("[cipher-Decrypt-2] decryption failed: %s", err) } - if len(ciphertext) < aes.BlockSize { - return nil, errors.New("ciphertext too short") + cipherBytes, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return "", fmt.Errorf("[cipher-Decrypt-3] decryption failed: %s", err) + } + nonceSize := gcm.NonceSize() + if len(cipherBytes) < nonceSize { + return "", fmt.Errorf("[cipher-Decrypt-4] decryption failed: %s", err) + } + nonce, cipherBytes := cipherBytes[:nonceSize], cipherBytes[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, cipherBytes, nil) + if err != nil { + return "", fmt.Errorf("[cipher-Decrypt-5] decryption failed: %s", err) } - iv := ciphertext[:aes.BlockSize] - ciphertext = ciphertext[aes.BlockSize:] - mode := cipher.NewCBCDecrypter(block, iv) - mode.CryptBlocks(ciphertext, ciphertext) - return ciphertext, nil + return string(plaintext), nil } -func GenerateRandomKey() ([]byte, error) { - key := make([]byte, 32) - _, err := rand.Read(key) +func GenerateRandomKey(length int) (string, error) { + randomBytes := make([]byte, length) + _, err := rand.Read(randomBytes) if err != nil { - return nil, err + return "", fmt.Errorf("[cipher-GenerateRandomKey-1] generating random key failed: %s", err) } - return key, nil + return base64.URLEncoding.EncodeToString(randomBytes), nil } From 6d80fc3b12d64a4081c36e84c8a76ce19a436cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sun, 3 Sep 2023 01:40:50 +0200 Subject: [PATCH 05/17] [UPD] Encryptor -> Cryptor --- pkg/config/config.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 92c7d2a..980c0df 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -8,17 +8,17 @@ import ( "strconv" "sync" + "github.com/plaenkler/ddns-updater/pkg/cipher" log "github.com/plaenkler/ddns-updater/pkg/logging" - "github.com/plaenkler/ddns-updater/pkg/security" "gopkg.in/yaml.v3" ) type Config struct { - Interval uint64 `yaml:"Interval"` - UseTOTP bool `yaml:"TOTP"` - Port uint64 `yaml:"Port"` - Resolver string `yaml:"Resolver"` - Encryptor string `yaml:"Encryptor"` + Interval uint64 `yaml:"Interval"` + UseTOTP bool `yaml:"TOTP"` + Port uint64 `yaml:"Port"` + Resolver string `yaml:"Resolver"` + Cryptor string `yaml:"Cryptor"` } const ( @@ -66,16 +66,16 @@ func load() error { } func create() error { - encryptor, err := security.GenerateRandomKey(8) + Cryptor, err := cipher.GenerateRandomKey(16) if err != nil { return err } config := Config{ - Interval: 600, - UseTOTP: false, - Port: 80, - Resolver: "", - Encryptor: encryptor, + Interval: 600, + UseTOTP: false, + Port: 80, + Resolver: "", + Cryptor: Cryptor, } err = os.MkdirAll(filepath.Dir(pathToConfig), dirPerm) if err != nil { @@ -122,9 +122,9 @@ func loadFromEnv() error { if resolver != "" { config.Resolver = resolver } - encryptor, ok := os.LookupEnv("DDNS_ENCRYPTOR") - if ok && encryptor != "" { - config.Encryptor = encryptor + Cryptor, ok := os.LookupEnv("DDNS_Cryptor") + if ok && Cryptor != "" { + config.Cryptor = Cryptor } return nil } From 804269dd1f0953c263eac17df89c7e220dafc7c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sun, 3 Sep 2023 01:41:04 +0200 Subject: [PATCH 06/17] [REM] Security pkg --- pkg/security/security.go | 58 ---------------------------------------- 1 file changed, 58 deletions(-) delete mode 100644 pkg/security/security.go diff --git a/pkg/security/security.go b/pkg/security/security.go deleted file mode 100644 index 70f1ded..0000000 --- a/pkg/security/security.go +++ /dev/null @@ -1,58 +0,0 @@ -package security - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "encoding/base64" - "encoding/hex" - "io" -) - -func Encrypt(key []byte, plaintext string) (string, error) { - block, err := aes.NewCipher(key) - if err != nil { - return "", err - } - // 96-bit nonce - nonce := make([]byte, 12) - _, err = io.ReadFull(rand.Reader, nonce) - if err != nil { - return "", err - } - aesgcm, err := cipher.NewGCM(block) - if err != nil { - return "", err - } - return hex.EncodeToString(append(nonce, aesgcm.Seal(nil, nonce, []byte(plaintext), nil)...)), nil -} - -func Decrypt(key []byte, ciphertext string) (string, error) { - block, err := aes.NewCipher(key) - if err != nil { - return "", err - } - // Extract nonce from the first 24 bytes - nonce, err := hex.DecodeString(ciphertext[:24]) - if err != nil { - return "", err - } - aesgcm, err := cipher.NewGCM(block) - if err != nil { - return "", err - } - decrypted, err := aesgcm.Open(nil, nonce, []byte(ciphertext[24:]), nil) - if err != nil { - return "", err - } - return string(decrypted), nil -} - -func GenerateRandomKey(length int) (string, error) { - randomBytes := make([]byte, length) - _, err := rand.Read(randomBytes) - if err != nil { - return "", err - } - return base64.URLEncoding.EncodeToString(randomBytes), nil -} From 3be47388242169ba12e41252ec66b45b8d9df484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sun, 3 Sep 2023 01:42:32 +0200 Subject: [PATCH 07/17] [FIX] Go 1.20 Github workflows do not support 1.21 yet --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 57fe9a7..8ca4d30 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/plaenkler/ddns-updater -go 1.21 +go 1.20 require ( github.com/aliyun/alibaba-cloud-sdk-go v1.62.514 From 4e2cef4f987e7eda1cb198c1f6fe3447324df4c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sun, 3 Sep 2023 02:24:26 +0200 Subject: [PATCH 08/17] [UPD] Encrypt job params --- pkg/server/routes/api/job.go | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/pkg/server/routes/api/job.go b/pkg/server/routes/api/job.go index 99ffc1f..06f3aae 100644 --- a/pkg/server/routes/api/job.go +++ b/pkg/server/routes/api/job.go @@ -5,6 +5,8 @@ import ( "net/http" "strconv" + "github.com/plaenkler/ddns-updater/pkg/cipher" + "github.com/plaenkler/ddns-updater/pkg/config" "github.com/plaenkler/ddns-updater/pkg/database" "github.com/plaenkler/ddns-updater/pkg/database/model" "github.com/plaenkler/ddns-updater/pkg/ddns" @@ -34,19 +36,25 @@ func CreateJob(w http.ResponseWriter, r *http.Request) { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } + encParams, err := cipher.Encrypt(config.Get().Cryptor, params) + if err != nil { + log.Errorf("[api-CreateJob-4] could not encrypt params: %s", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } job := model.SyncJob{ Provider: provider, - Params: params, + Params: encParams, } db := database.GetDatabase() if db == nil { - log.Errorf("[api-CreateJob-4] could not get database connection") + log.Errorf("[api-CreateJob-5] could not get database connection") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err = db.Create(&job).Error if err != nil { - log.Errorf("[api-CreateJob-5] could not create job: %s", err) + log.Errorf("[api-CreateJob-6] could not create job: %s", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -82,27 +90,33 @@ func UpdateJob(w http.ResponseWriter, r *http.Request) { log.Errorf("[api-UpdateJob-4] could not unmarshal params: %s", err) return } + encParams, err := cipher.Encrypt(config.Get().Cryptor, params) + if err != nil { + http.Error(w, "Could not encrypt params", http.StatusInternalServerError) + log.Errorf("[api-UpdateJob-5] could not encrypt params: %s", err) + return + } job := model.SyncJob{ Model: gorm.Model{ ID: uint(id), }, Provider: provider, - Params: params, + Params: encParams, } db := database.GetDatabase() if db == nil { http.Error(w, "Could not get database connection", http.StatusInternalServerError) - log.Errorf("[api-UpdateJob-5] could not get database connection") + log.Errorf("[api-UpdateJob-6] could not get database connection") return } err = db.Save(&job).Error if err != nil { http.Error(w, "Could not update job", http.StatusInternalServerError) - log.Errorf("[api-UpdateJob-6] could not update job: %s", err) + log.Errorf("[api-UpdateJob-7] could not update job: %s", err) return } http.Redirect(w, r, r.Header.Get("Referer"), http.StatusSeeOther) - log.Infof("[api-UpdateJob-7] updated job with ID %d", job.ID) + log.Infof("[api-UpdateJob-8] updated job with ID %d", job.ID) } func DeleteJob(w http.ResponseWriter, r *http.Request) { From 2be6aee023f43f4535cd8f6b826a5bbbc7bf5d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sun, 3 Sep 2023 03:21:31 +0200 Subject: [PATCH 09/17] [UPD] Parameters processing --- pkg/server/routes/web/index.go | 78 +++++++++++++------ .../routes/web/static/html/pages/index.html | 2 +- pkg/server/routes/web/static/js/index.js | 2 +- 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/pkg/server/routes/web/index.go b/pkg/server/routes/web/index.go index 6f1d9c5..a0f18c8 100644 --- a/pkg/server/routes/web/index.go +++ b/pkg/server/routes/web/index.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" + "github.com/plaenkler/ddns-updater/pkg/cipher" "github.com/plaenkler/ddns-updater/pkg/config" "github.com/plaenkler/ddns-updater/pkg/database" "github.com/plaenkler/ddns-updater/pkg/database/model" @@ -33,27 +34,24 @@ func ProvideIndex(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusSeeOther) return } - tpl, err := template.New("index").ParseFS(static, - "static/html/pages/index.html", - "static/html/partials/include.html", - "static/html/partials/navbar.html", - "static/html/partials/modals.html", - ) + addr, err := ddns.GetPublicIP() if err != nil { w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "[web-ProvideIndex-1] could not provide template: %s", err) + fmt.Fprintf(w, "[web-ProvideIndex-2] could not get public IP address: %s", err) return } - data := indexPageData{ - Config: config.Get(), - Providers: ddns.GetProviders(), - } - data.IPAddress, err = ddns.GetPublicIP() + img, err := totps.GetKeyAsQR() if err != nil { w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "[web-ProvideIndex-2] could not get public IP address: %s", err) + fmt.Fprintf(w, "[web-ProvideIndex-6] could not generate TOTP QR code: %s", err) return } + data := indexPageData{ + Config: config.Get(), + Providers: ddns.GetProviders(), + IPAddress: addr, + TOTPQR: template.URL(img), + } db := database.GetDatabase() if db == nil { w.WriteHeader(http.StatusInternalServerError) @@ -66,19 +64,25 @@ func ProvideIndex(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "[web-ProvideIndex-4] could not find jobs: %s", err) return } - err = formatParams(data.Jobs) + err = sanatizeParams(data.Jobs) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "[web-ProvideIndex-5] formatting params failed: %s", err) return } - img, err := totps.GetKeyAsQR() + tpl, err := template.New("index").Funcs(template.FuncMap{ + "formatParams": formatParams, + }).ParseFS(static, + "static/html/pages/index.html", + "static/html/partials/include.html", + "static/html/partials/navbar.html", + "static/html/partials/modals.html", + ) if err != nil { w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "[web-ProvideIndex-6] could not generate TOTP QR code: %s", err) + fmt.Fprintf(w, "[web-ProvideIndex-1] could not provide template: %s", err) return } - data.TOTPQR = template.URL(img) w.Header().Add("Content-Type", "text/html") err = tpl.Execute(w, data) if err != nil { @@ -87,21 +91,45 @@ func ProvideIndex(w http.ResponseWriter, r *http.Request) { } } -func formatParams(jobs []model.SyncJob) error { +func sanatizeParams(jobs []model.SyncJob) error { for j := range jobs { + decParams, err := cipher.Decrypt(config.Get().Cryptor, jobs[j].Params) + if err != nil { + return err + } params := make(map[string]string) - err := json.Unmarshal([]byte(jobs[j].Params), ¶ms) + err = json.Unmarshal([]byte(decParams), ¶ms) if err != nil { return err } - var paramList []string - for k, v := range params { - if strings.ToLower(k) == "password" { - continue + for k := range params { + kl := strings.ToLower(k) + if strings.Contains(kl, "password") { + params[k] = "***" + } + if strings.Contains(kl, "token") { + params[k] = "***" } - paramList = append(paramList, fmt.Sprintf("%s: %s", k, v)) } - jobs[j].Params = strings.Join(paramList, ", ") + encParams, err := json.Marshal(params) + if err != nil { + return err + } + jobs[j].Params = string(encParams) } return nil } + +func formatParams(paramsData string) (string, error) { + params := make(map[string]string) + err := json.Unmarshal([]byte(paramsData), ¶ms) + if err != nil { + return "", err + } + var formatted string + for key, value := range params { + formatted += fmt.Sprintf("%s: %v, ", key, value) + } + formatted = formatted[:len(formatted)-2] + return formatted, nil +} diff --git a/pkg/server/routes/web/static/html/pages/index.html b/pkg/server/routes/web/static/html/pages/index.html index 2e4c616..06c459f 100644 --- a/pkg/server/routes/web/static/html/pages/index.html +++ b/pkg/server/routes/web/static/html/pages/index.html @@ -28,7 +28,7 @@ {{.ID}} {{.Provider}} {{.UpdatedAt.Format "02.01.2006 15:04"}} - {{.Params}} + {{formatParams .Params}} {{end}} diff --git a/pkg/server/routes/web/static/js/index.js b/pkg/server/routes/web/static/js/index.js index 050aebd..1f679b3 100644 --- a/pkg/server/routes/web/static/js/index.js +++ b/pkg/server/routes/web/static/js/index.js @@ -24,7 +24,7 @@ document.querySelector('table tbody').addEventListener('click', function(event) document.getElementById('edit-id').value = row.querySelector('td:nth-child(1)').textContent; document.getElementById('delete-button').href = `/api/job/delete?ID=${row.querySelector('td:nth-child(1)').textContent}`; document.getElementById('edit-provider-select').value = row.querySelector('td:nth-child(2)').textContent; - const params = JSON.parse(row.querySelector('td:nth-child(4)').textContent); + const params = JSON.parse(document.querySelector('td:nth-child(4)').getAttribute('json')); let html = ''; for (const key in params) { html += ` From e932f760b0659aa5c02f463454b1d18c6b7001c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sun, 3 Sep 2023 03:21:48 +0200 Subject: [PATCH 10/17] [UPD] README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3070253..b4e43d3 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ The user-friendly interface allows for straightforward secure setup and manageme - [x] Scheduled update service - [x] Supports multiple IP resolvers - [ ] Deploy as Windows Service +- [x] Secure encryption of sensitive data ## 🏷️ Supported providers From ee4a0682ca4d1ba4c8ea84184bb6d1a92e779c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sun, 3 Sep 2023 03:24:48 +0200 Subject: [PATCH 11/17] [FIX] Log idents --- pkg/server/routes/web/index.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/server/routes/web/index.go b/pkg/server/routes/web/index.go index a0f18c8..0597a5e 100644 --- a/pkg/server/routes/web/index.go +++ b/pkg/server/routes/web/index.go @@ -37,13 +37,13 @@ func ProvideIndex(w http.ResponseWriter, r *http.Request) { addr, err := ddns.GetPublicIP() if err != nil { w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "[web-ProvideIndex-2] could not get public IP address: %s", err) + fmt.Fprintf(w, "[web-ProvideIndex-1] could not get public IP address: %s", err) return } img, err := totps.GetKeyAsQR() if err != nil { w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "[web-ProvideIndex-6] could not generate TOTP QR code: %s", err) + fmt.Fprintf(w, "[web-ProvideIndex-2] could not generate TOTP QR code: %s", err) return } data := indexPageData{ @@ -80,7 +80,7 @@ func ProvideIndex(w http.ResponseWriter, r *http.Request) { ) if err != nil { w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "[web-ProvideIndex-1] could not provide template: %s", err) + fmt.Fprintf(w, "[web-ProvideIndex-6] could not provide template: %s", err) return } w.Header().Add("Content-Type", "text/html") From dc19fe2bba1cbb5b2844d9dd8851d7100ea36538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sun, 3 Sep 2023 14:03:15 +0200 Subject: [PATCH 12/17] [UPD] README.md --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b4e43d3..e3010a6 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,11 @@ The user-friendly interface allows for straightforward secure setup and manageme - [x] Simple & User friendly UI - [x] Secure authentication with TOTP -- [x] Available as Docker Container +- [x] Encryption of sensitive data - [x] Scheduled update service - [x] Supports multiple IP resolvers - [ ] Deploy as Windows Service -- [x] Secure encryption of sensitive data +- [x] Available as Docker Container ## 🏷️ Supported providers @@ -127,10 +127,16 @@ Changes to the interval take effect immediately. The program must be restarted f A config.yaml file is provided to store all settings. In the absence of this file, the program generates one. Users have the option to directly modify settings within this file. It is important to note that changes made here will only take effect upon restarting the program. Default settings within the file are as follows: ```yaml +# How often the IP address is checked in seconds Interval: 600 +# Enable TOTP authentication TOTP: false +# Port for the web interface Port: 80 +# Custom IP resolver returns IPv4 address in plain text Resolver: "" +# Random key for symmetrical encryption +Cryptor: 29atdqljyqUVXNAuXltyng== ``` **3. Environment Variables** @@ -142,4 +148,5 @@ DDNS_INTERVAL=600 DDNS_TOTP=false DDNS_PORT=80 DDNS_RESOLVER=ipv4.example.com +DDNS_Cryptor=29atdqljyqUVXNAuXltyng== ``` \ No newline at end of file From b0c883f512fc5dec9b04f66f8001d4f76851be91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sun, 3 Sep 2023 14:09:48 +0200 Subject: [PATCH 13/17] [FIX] Decrypt job params --- pkg/ddns/service.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pkg/ddns/service.go b/pkg/ddns/service.go index 133a10e..39eeb43 100644 --- a/pkg/ddns/service.go +++ b/pkg/ddns/service.go @@ -5,6 +5,7 @@ import ( "sync" "time" + "github.com/plaenkler/ddns-updater/pkg/cipher" "github.com/plaenkler/ddns-updater/pkg/config" "github.com/plaenkler/ddns-updater/pkg/database" "github.com/plaenkler/ddns-updater/pkg/database/model" @@ -82,22 +83,27 @@ func updateDDNSEntries(db *gorm.DB, jobs []model.SyncJob, a model.IPAddress) { log.Errorf("[ddns-updateDDNSEntries-1] no updater found for job %v", job.ID) continue } + params, err := cipher.Decrypt(config.Get().Cryptor, job.Params) + if err != nil { + log.Errorf("[ddns-updateDDNSEntries-2] failed to decrypt job params for job %v: %s", job.ID, err) + continue + } request := updater.Request - err := json.Unmarshal([]byte(job.Params), request) + err = json.Unmarshal([]byte(params), request) if err != nil { - log.Errorf("[ddns-updateDDNSEntries-2] failed to unmarshal job params for job %v: %s", job.ID, err) + log.Errorf("[ddns-updateDDNSEntries-3] failed to unmarshal job params for job %v: %s", job.ID, err) continue } err = updater.Updater(request, a.Address) if err != nil { - log.Errorf("[ddns-updateDDNSEntries-3] failed to update DDNS entry for job %v: %s", job.ID, err) + log.Errorf("[ddns-updateDDNSEntries-4] failed to update DDNS entry for job %v: %s", job.ID, err) continue } err = db.Model(&job).Update("ip_address_id", a.ID).Error if err != nil { - log.Errorf("[ddns-updateDDNSEntries-4] failed to update IP address for job %v: %s", job.ID, err) + log.Errorf("[ddns-updateDDNSEntries-5] failed to update IP address for job %v: %s", job.ID, err) } - log.Infof("[ddns-updateDDNSEntries-5] updated DDNS entry for ID: %v Provider: %s Params: %+v", job.ID, job.Provider, job.Params) + log.Infof("[ddns-updateDDNSEntries-6] updated DDNS entry for ID: %v Provider: %s Params: %+v", job.ID, job.Provider, job.Params) } } From 493b404f29280b98743e689c358ec47c3b9f3623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sun, 3 Sep 2023 15:23:11 +0200 Subject: [PATCH 14/17] [FIX] DDNS_Cryptor -> DDNS_CRYPTOR --- README.md | 2 +- pkg/config/config.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e3010a6..f686928 100644 --- a/README.md +++ b/README.md @@ -148,5 +148,5 @@ DDNS_INTERVAL=600 DDNS_TOTP=false DDNS_PORT=80 DDNS_RESOLVER=ipv4.example.com -DDNS_Cryptor=29atdqljyqUVXNAuXltyng== +DDNS_CRYPTOR=29atdqljyqUVXNAuXltyng== ``` \ No newline at end of file diff --git a/pkg/config/config.go b/pkg/config/config.go index 980c0df..e47c903 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -122,7 +122,7 @@ func loadFromEnv() error { if resolver != "" { config.Resolver = resolver } - Cryptor, ok := os.LookupEnv("DDNS_Cryptor") + Cryptor, ok := os.LookupEnv("DDNS_CRYPTOR") if ok && Cryptor != "" { config.Cryptor = Cryptor } From 30a754b13da83aef196131fe392cd73eeaedb00b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sun, 3 Sep 2023 15:44:12 +0200 Subject: [PATCH 15/17] [FIX] Cryptor -> cryptor --- pkg/config/config.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index e47c903..c605d02 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -66,7 +66,7 @@ func load() error { } func create() error { - Cryptor, err := cipher.GenerateRandomKey(16) + cryptor, err := cipher.GenerateRandomKey(16) if err != nil { return err } @@ -75,7 +75,7 @@ func create() error { UseTOTP: false, Port: 80, Resolver: "", - Cryptor: Cryptor, + Cryptor: cryptor, } err = os.MkdirAll(filepath.Dir(pathToConfig), dirPerm) if err != nil { @@ -122,9 +122,9 @@ func loadFromEnv() error { if resolver != "" { config.Resolver = resolver } - Cryptor, ok := os.LookupEnv("DDNS_CRYPTOR") - if ok && Cryptor != "" { - config.Cryptor = Cryptor + cryptor, ok := os.LookupEnv("DDNS_CRYPTOR") + if ok && cryptor != "" { + config.Cryptor = cryptor } return nil } From 90a33b6f8056885825efda7b9fcef90015ec62a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sun, 3 Sep 2023 15:45:28 +0200 Subject: [PATCH 16/17] [FIX] sanatizeParams -> sanitizeParams --- pkg/server/routes/web/index.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/server/routes/web/index.go b/pkg/server/routes/web/index.go index 0597a5e..deed3e5 100644 --- a/pkg/server/routes/web/index.go +++ b/pkg/server/routes/web/index.go @@ -64,7 +64,7 @@ func ProvideIndex(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "[web-ProvideIndex-4] could not find jobs: %s", err) return } - err = sanatizeParams(data.Jobs) + err = sanitizeParams(data.Jobs) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "[web-ProvideIndex-5] formatting params failed: %s", err) @@ -91,7 +91,7 @@ func ProvideIndex(w http.ResponseWriter, r *http.Request) { } } -func sanatizeParams(jobs []model.SyncJob) error { +func sanitizeParams(jobs []model.SyncJob) error { for j := range jobs { decParams, err := cipher.Decrypt(config.Get().Cryptor, jobs[j].Params) if err != nil { From 61b1af2bf70a3d3c593f881c514d28398bcf10fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Lukas=20=7E=20Znox=20/=20Pl=C3=A4nkler?= <60503970+Plaenkler@users.noreply.github.com> Date: Sun, 3 Sep 2023 15:58:52 +0200 Subject: [PATCH 17/17] [FIX] Return []byte --- pkg/cipher/cipher.go | 14 +++++++------- pkg/ddns/service.go | 2 +- pkg/server/routes/web/index.go | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/cipher/cipher.go b/pkg/cipher/cipher.go index 1fc8f5a..213a464 100644 --- a/pkg/cipher/cipher.go +++ b/pkg/cipher/cipher.go @@ -26,29 +26,29 @@ func Encrypt(key string, plaintext string) (string, error) { return base64.StdEncoding.EncodeToString(gcm.Seal(nonce, nonce, []byte(plaintext), nil)), nil } -func Decrypt(key string, ciphertext string) (string, error) { +func Decrypt(key string, ciphertext string) ([]byte, error) { c, err := aes.NewCipher([]byte(key)) if err != nil { - return "", fmt.Errorf("[cipher-Decrypt-1] decryption failed: %s", err) + return nil, fmt.Errorf("[cipher-Decrypt-1] decryption failed: %s", err) } gcm, err := cipher.NewGCM(c) if err != nil { - return "", fmt.Errorf("[cipher-Decrypt-2] decryption failed: %s", err) + return nil, fmt.Errorf("[cipher-Decrypt-2] decryption failed: %s", err) } cipherBytes, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { - return "", fmt.Errorf("[cipher-Decrypt-3] decryption failed: %s", err) + return nil, fmt.Errorf("[cipher-Decrypt-3] decryption failed: %s", err) } nonceSize := gcm.NonceSize() if len(cipherBytes) < nonceSize { - return "", fmt.Errorf("[cipher-Decrypt-4] decryption failed: %s", err) + return nil, fmt.Errorf("[cipher-Decrypt-4] decryption failed: %s", err) } nonce, cipherBytes := cipherBytes[:nonceSize], cipherBytes[nonceSize:] plaintext, err := gcm.Open(nil, nonce, cipherBytes, nil) if err != nil { - return "", fmt.Errorf("[cipher-Decrypt-5] decryption failed: %s", err) + return nil, fmt.Errorf("[cipher-Decrypt-5] decryption failed: %s", err) } - return string(plaintext), nil + return plaintext, nil } func GenerateRandomKey(length int) (string, error) { diff --git a/pkg/ddns/service.go b/pkg/ddns/service.go index 39eeb43..f4fe9cb 100644 --- a/pkg/ddns/service.go +++ b/pkg/ddns/service.go @@ -89,7 +89,7 @@ func updateDDNSEntries(db *gorm.DB, jobs []model.SyncJob, a model.IPAddress) { continue } request := updater.Request - err = json.Unmarshal([]byte(params), request) + err = json.Unmarshal(params, request) if err != nil { log.Errorf("[ddns-updateDDNSEntries-3] failed to unmarshal job params for job %v: %s", job.ID, err) continue diff --git a/pkg/server/routes/web/index.go b/pkg/server/routes/web/index.go index deed3e5..d41cd41 100644 --- a/pkg/server/routes/web/index.go +++ b/pkg/server/routes/web/index.go @@ -98,7 +98,7 @@ func sanitizeParams(jobs []model.SyncJob) error { return err } params := make(map[string]string) - err = json.Unmarshal([]byte(decParams), ¶ms) + err = json.Unmarshal(decParams, ¶ms) if err != nil { return err } @@ -120,9 +120,9 @@ func sanitizeParams(jobs []model.SyncJob) error { return nil } -func formatParams(paramsData string) (string, error) { +func formatParams(paramsData []byte) (string, error) { params := make(map[string]string) - err := json.Unmarshal([]byte(paramsData), ¶ms) + err := json.Unmarshal(paramsData, ¶ms) if err != nil { return "", err }