From 7c41fd92a2c922cb5574e17ccc4b39373b9376a0 Mon Sep 17 00:00:00 2001 From: Roy Teeuwen Date: Sat, 21 Mar 2026 21:34:40 +0100 Subject: [PATCH] solves #335: Add unprotect / decrypt support for AEM 6.5 LTS / Cloud --- cmd/aem/crypto.go | 29 ++++++++++++++++ pkg/crypto.go | 48 ++++++++++++++++++++++++++- pkg/crypto_int_test.go | 75 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 pkg/crypto_int_test.go diff --git a/cmd/aem/crypto.go b/cmd/aem/crypto.go index 1e9e0d38..806b5679 100644 --- a/cmd/aem/crypto.go +++ b/cmd/aem/crypto.go @@ -13,6 +13,7 @@ func (c *CLI) cryptoCmd() *cobra.Command { } cmd.AddCommand(c.cryptoSetupCmd()) cmd.AddCommand(c.cryptoProtectCmd()) + cmd.AddCommand(c.cryptoUnprotectCmd()) return cmd } @@ -91,3 +92,31 @@ func (c *CLI) cryptoProtectCmd() *cobra.Command { return cmd } + +func (c *CLI) cryptoUnprotectCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "unprotect", + Aliases: []string{"decrypt"}, + Short: "Unprotect value", + Run: func(cmd *cobra.Command, args []string) { + instance, err := c.aem.InstanceManager().One() + if err != nil { + c.Error(err) + return + } + protectedValue, _ := cmd.Flags().GetString("value") + plainValue, err := instance.Crypto().Unprotect(protectedValue) + if err != nil { + c.Error(err) + return + } + c.SetOutput("value", plainValue) + c.Ok("value unprotected by Crypto") + }, + } + + cmd.Flags().StringP("value", "v", "", "Value to unprotect") + _ = cmd.MarkFlagRequired("value") + + return cmd +} diff --git a/pkg/crypto.go b/pkg/crypto.go index e073f55c..dfbada9d 100644 --- a/pkg/crypto.go +++ b/pkg/crypto.go @@ -6,10 +6,14 @@ import ( "github.com/wttech/aemc/pkg/common/filex" "github.com/wttech/aemc/pkg/common/fmtx" "github.com/wttech/aemc/pkg/common/pathx" + "html" + "io" + "regexp" ) const ( - CryptoProtectPath = "/system/console/crypto/.json" + CryptoProtectPath = "/system/console/crypto/.json" + CryptoUnprotectPath = "/system/console/crypto" ) type Crypto struct { @@ -97,3 +101,45 @@ func (c Crypto) Protect(value string) (string, error) { return result.Protected, nil } + +var unprotectPlaintextRegex = regexp.MustCompile(`]*name="unprotect_plaintext"[^>]*>`) +var inputValueRegex = regexp.MustCompile(`value="([^"]*)"`) + +func (c Crypto) Unprotect(value string) (string, error) { + log.Infof("%s > decrypting text using Crypto", c.instance.IDColor()) + response, err := c.instance.http.RequestFormData(map[string]any{ + "action": "unprotect", + "unprotect_ciphertext": value, + }).Post(CryptoUnprotectPath) + + if err != nil { + return "", fmt.Errorf("%s > cannot decrypt text using Crypto: %w", c.instance.IDColor(), err) + } else if response.IsError() { + return "", fmt.Errorf("%s > cannot decrypt text using Crypto: %s", c.instance.IDColor(), response.Status()) + } + + rawBody := response.RawBody() + defer rawBody.Close() + bodyBytes, err := io.ReadAll(rawBody) + if err != nil { + return "", fmt.Errorf("%s > cannot read Crypto unprotect response: %w", c.instance.IDColor(), err) + } + + body := string(bodyBytes) + inputMatch := unprotectPlaintextRegex.FindString(body) + if inputMatch == "" { + return "", fmt.Errorf("%s > cannot parse Crypto unprotect response: plaintext not found in response; note that unprotect is only available on AEM 6.5 LTS and AEM Cloud SDK", c.instance.IDColor()) + } + + valueMatch := inputValueRegex.FindStringSubmatch(inputMatch) + if valueMatch == nil || len(valueMatch) < 2 { + return "", fmt.Errorf("%s > cannot parse Crypto unprotect response: value not found in response", c.instance.IDColor()) + } + + plaintext := html.UnescapeString(valueMatch[1]) + if plaintext == "Exception occurred while decrypting: cannot unprotected ciphertext" { + return "", fmt.Errorf("%s > cannot decrypt text using Crypto: decryption failed (invalid ciphertext or wrong keys)", c.instance.IDColor()) + } + + return plaintext, nil +} diff --git a/pkg/crypto_int_test.go b/pkg/crypto_int_test.go new file mode 100644 index 00000000..b9606b46 --- /dev/null +++ b/pkg/crypto_int_test.go @@ -0,0 +1,75 @@ +//go:build int_test + +package pkg_test + +import ( + "github.com/wttech/aemc/pkg" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCryptoProtect(t *testing.T) { + t.Parallel() + a := assert.New(t) + + aem := pkg.DefaultAEM() + instance := aem.InstanceManager().NewLocalAuthor() + + protected, err := instance.Crypto().Protect("hello") + a.Nil(err, "cannot protect value") + a.NotEmpty(protected, "protected value should not be empty") + a.Contains(protected, "{", "protected value should be wrapped in curly braces") + a.Contains(protected, "}", "protected value should be wrapped in curly braces") +} + +func TestCryptoUnprotect(t *testing.T) { + t.Parallel() + a := assert.New(t) + + aem := pkg.DefaultAEM() + instance := aem.InstanceManager().NewLocalAuthor() + + protected, err := instance.Crypto().Protect("hello") + a.Nil(err, "cannot protect value") + + plain, err := instance.Crypto().Unprotect(protected) + a.Nil(err, "cannot unprotect value") + a.Equal("hello", plain, "unprotected value should match original") +} + +func TestCryptoUnprotectSpecialChars(t *testing.T) { + t.Parallel() + a := assert.New(t) + + aem := pkg.DefaultAEM() + instance := aem.InstanceManager().NewLocalAuthor() + + values := []string{ + `he said "hello"`, + ``, + `it's a test`, + `jdbc:mysql://host:3306/db?user=admin&password=p@ss"w0rd`, + } + + for _, original := range values { + protected, err := instance.Crypto().Protect(original) + a.Nil(err, "cannot protect value: %s", original) + + plain, err := instance.Crypto().Unprotect(protected) + a.Nil(err, "cannot unprotect value: %s", original) + a.Equal(original, plain, "roundtrip failed for value: %s", original) + } +} + +func TestCryptoUnprotectInvalidCiphertext(t *testing.T) { + t.Parallel() + a := assert.New(t) + + aem := pkg.DefaultAEM() + instance := aem.InstanceManager().NewLocalAuthor() + + _, err := instance.Crypto().Unprotect("{invalid_cipher_text}") + a.NotNil(err, "unprotecting invalid ciphertext should return an error") + a.Contains(err.Error(), "decryption failed", "error should indicate decryption failure") +}