Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions cmd/aem/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func (c *CLI) cryptoCmd() *cobra.Command {
}
cmd.AddCommand(c.cryptoSetupCmd())
cmd.AddCommand(c.cryptoProtectCmd())
cmd.AddCommand(c.cryptoUnprotectCmd())
return cmd
}

Expand Down Expand Up @@ -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
}
48 changes: 47 additions & 1 deletion pkg/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -97,3 +101,45 @@ func (c Crypto) Protect(value string) (string, error) {

return result.Protected, nil
}

var unprotectPlaintextRegex = regexp.MustCompile(`<input\s[^>]*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
}
75 changes: 75 additions & 0 deletions pkg/crypto_int_test.go
Original file line number Diff line number Diff line change
@@ -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"`,
`<script>alert("xss")&foo</script>`,
`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")
}