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