Skip to content

Commit

Permalink
Support multiple master keys in one build
Browse files Browse the repository at this point in the history
  • Loading branch information
kwin committed Sep 30, 2024
1 parent a2b09ea commit 0e7960d
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 48 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ This library can be used as extension for the filevault-package-maven plugin or

This library can be used with [`filevault-package-maven-plugin`][filevault-package-maven-plugin] in version 1.4.0 or newer to allow [resource filtering][filevault-filtering] with encryption support. This is useful to create encrypted values in content packages.

The key is looked up from either a Maven property with name `AEM_KEY` or a same named environment variable (in that order).
The master key is looked up from either a Maven property with name `AEM_KEY` (or a same named environment variable) or `AEM_KEY_<SUFFIX>` in case a specific master key is referenced.

#### Configuration of filevault-package-maven-plugin

Expand Down Expand Up @@ -76,6 +76,9 @@ The key is looked up from either a Maven property with name `AEM_KEY` or a same

This will encrypt the value provided through the environment variable `MY_SECRET`.

In order to use specific keys (e.g. when targeting multiple environments with different master keys in the same build) use a suffix after `vltaemencrypt` like `vltaemencryptprod.env.MY_SECRET`.
This will encrypt `MY_SECRET` with the master key provided in Maven property with name `AEM_KEY_PROD` or a same named environment variable (in that order). Note that the *suffix* (`prod` in this case) is automatically converted to uppercase letters before being used in the environment variable/property name.

### API

```
Expand Down
3 changes: 2 additions & 1 deletion src/it/simple-filter/jcr_root/apps/foo/.content.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
escapedValue="${vltattributeescape.customProperty}"
encryptedValue="${vltaemencrypt.customProperty}" />
encryptedValue="${vltaemencrypt.customProperty}"
encryptedValueCustom="${vltaemencryptcustom.customProperty}" />
1 change: 1 addition & 0 deletions src/it/simple-filter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<properties>
<customProperty>plainText</customProperty>
<AEM_KEY>TXlTZWNyZXRLZXkxMjM0NQ==</AEM_KEY> <!-- base64 of "MySecretKey12345" -->
<AEM_KEY_CUSTOM>TXlTZWNyZXRLZXk1NDMyMQ==</AEM_KEY_CUSTOM> <!-- base64 of "MySecretKey54321" -->
</properties>
<build>
<plugins>
Expand Down
2 changes: 2 additions & 0 deletions src/it/simple-filter/verify.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ try (ZipFile zipFile = new ZipFile(file)) {
def root = new XmlSlurper().parse(input)
assert(root.@escapedValue == "plainText")
assert(root.@encryptedValue.text() ==~ $/\{[a-z0-9]*\}/$)
assert(root.@encryptedValueCustom.text() ==~ $/\{[a-z0-9]*\}/$)
assert(root.@encryptedCustom.text() != root.@encryptedValueCustom.text())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,75 +14,99 @@
* #L%
*/

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.function.Supplier;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

import com.adobe.granite.crypto.CryptoSupport;
import org.codehaus.plexus.interpolation.AbstractValueSource;
import org.codehaus.plexus.interpolation.InterpolationPostProcessor;
import org.codehaus.plexus.interpolation.StringSearchInterpolator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.granite.crypto.CryptoSupport;

/** Enhances the regular FileVault resource filtering expressions with handling {@value #EXPRESSION_PREFIX}
* prefixes which will automatically encrypt the interpolated value of
* the suffix accordingly with the encryption from {@link CryptoSupport}. */
* the suffix accordingly with the encryption from {@link CryptoSupport}.
* It supports encryption with a default master key or a custom key with a given id.
* Custom keys require an additional infix in the expression, e.g. {@value #EXPRESSION_PREFIX}customKeyId.
*/
public class CryptoSupportInterpolatorCustomizer extends AbstractValueSource implements InterpolationPostProcessor {

private static final Logger LOGGER = LoggerFactory.getLogger(CryptoSupportInterpolatorCustomizer.class);
public static final String EXPRESSION_PREFIX = "vltaemencrypt.";
public static final String EXPRESSION_PREFIX = "vltaemencrypt";

static final String DEFAULT_KEY_ID = "default";
private final CryptoSupportFactory cryptoSupportFactory;
private final Supplier<String> keySupplier;
private CryptoSupport cryptoSupport; // lazily initialized
private final Function<String, String> keyProvider;
private Map<String, CryptoSupport> cryptoSupportMap; // lazily initialized

public CryptoSupportInterpolatorCustomizer(
CryptoSupportFactory cryptoSupportFactory, Supplier<String> keySupplier) {
CryptoSupportFactory cryptoSupportFactory, Function<String, String> keyProvider) {
super(false);
this.cryptoSupportFactory = cryptoSupportFactory;
this.keySupplier = keySupplier;
this.keyProvider = keyProvider;
this.cryptoSupportMap = new HashMap<>();
}

@Override
public Object getValue(String expression) {
if (expression.startsWith(EXPRESSION_PREFIX)) {
// FIXME: currently the delimiter is hardcoded
// (https://github.com/codehaus-plexus/plexus-interpolation/issues/76)
return StringSearchInterpolator.DEFAULT_START_EXPR
+ expression.substring(EXPRESSION_PREFIX.length())
+ StringSearchInterpolator.DEFAULT_END_EXPR;
// FIXME: currently the delimiter is hardcoded
// (https://github.com/codehaus-plexus/plexus-interpolation/issues/76)
return extractKeyIdAndSuffix(expression)
.map(entry -> {
return StringSearchInterpolator.DEFAULT_START_EXPR
+ entry.getValue()
+ StringSearchInterpolator.DEFAULT_END_EXPR;
})
.orElse(null);
}

static Optional<Map.Entry<String, String>> extractKeyIdAndSuffix(String expression) {
if (!expression.startsWith(EXPRESSION_PREFIX)) {
return Optional.empty();
}
String suffix = expression.substring(EXPRESSION_PREFIX.length());
int posDelimiter = suffix.indexOf('.');
final Map.Entry<String, String> result;
if (posDelimiter > 0 && posDelimiter < suffix.length() - 1) {
result = new AbstractMap.SimpleEntry<>(
suffix.substring(0, posDelimiter), suffix.substring(posDelimiter + 1));
} else if (posDelimiter == 0 && posDelimiter < suffix.length() - 1) {
result = new AbstractMap.SimpleEntry<>(DEFAULT_KEY_ID, suffix.substring(1));
} else {
return null;
result = null;
}
return Optional.ofNullable(result);
}

@Override
public Object execute(String expression, Object value) {
if (expression.startsWith(EXPRESSION_PREFIX)) {
try {
return getCryptoSupport().protect(value.toString());
} catch (Exception e) {
throw new IllegalStateException("Can not encrypt value", e);
}
}
return null;
return extractKeyIdAndSuffix(expression)
.map(entry -> {
try {
return getCryptoSupport(entry.getKey()).protect(value.toString());
} catch (Exception e) {
throw new IllegalStateException("Can not encrypt value", e);
}
})
.orElse(null);
}

private CryptoSupport getCryptoSupport()
throws ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException, InstantiationException {
if (cryptoSupport == null) {
cryptoSupport = cryptoSupportFactory.create(keySupplier.get());
Thread factoryClose = new Thread(() -> {
try {
cryptoSupportFactory.close();
} catch (IOException e) {
LOGGER.error("Cannot close CryptoSupportFactory", e);
}
});
Runtime.getRuntime().addShutdownHook(factoryClose);
}
return cryptoSupport;
private CryptoSupport getCryptoSupport(String keyId) {
return cryptoSupportMap.computeIfAbsent(keyId, k -> {
try {
return cryptoSupportFactory.create(keyProvider.apply(k));
} catch (ClassNotFoundException
| NoSuchMethodException
| SecurityException
| IllegalAccessException
| IllegalArgumentException
| InstantiationException
| InvocationTargetException e) {
throw new IllegalStateException("Could not initialize CryptoSupport", e);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
* #L%
*/

import javax.annotation.PreDestroy;
import javax.inject.Named;

import java.io.IOException;
import java.util.Locale;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.function.Function;

import org.apache.jackrabbit.filevault.maven.packaging.InterpolatorCustomizerFactory;
import org.apache.maven.execution.MavenSession;
Expand All @@ -43,11 +45,17 @@ protected CryptoSupportInterpolatorCustomizerFactory() {

@Override
public Consumer<Interpolator> create(MavenSession mavenSession, MavenProject mavenProject) {
Supplier<String> keySupplier = () -> {
String key = mavenProject.getProperties().getProperty(PROPERTY_NAME_KEY, System.getenv(PROPERTY_NAME_KEY));
Function<String, String> keySupplier = (id) -> {
final String propertyName;
if (CryptoSupportInterpolatorCustomizer.DEFAULT_KEY_ID.equals(id)) {
propertyName = PROPERTY_NAME_KEY;
} else {
propertyName = PROPERTY_NAME_KEY + "_" + id.toUpperCase(Locale.ROOT);
}
String key = mavenProject.getProperties().getProperty(propertyName, System.getenv(propertyName));
if (key == null) {
throw new IllegalStateException(
"Could not find key in either Maven property or environment variable " + PROPERTY_NAME_KEY);
"Could not find key in either Maven property or environment variable " + propertyName);
}
return key;
};
Expand All @@ -58,4 +66,9 @@ public Consumer<Interpolator> create(MavenSession mavenSession, MavenProject mav
i.addValueSource(customizer);
};
}

@PreDestroy
public void close() throws IOException {
cryptoSupportFactory.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package biz.netcentric.aem.crypto;

import java.util.AbstractMap;
import java.util.Optional;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

/*-
* #%L
* aem-crypto-support
* %%
* Copyright (C) 2024 Cognizant Netcentric
* %%
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
* #L%
*/

class CryptoSupportInterpolatorCustomizerTest {

@Test
void testExtractKeyIdAndSuffix() {
assertEquals(Optional.empty(), CryptoSupportInterpolatorCustomizer.extractKeyIdAndSuffix("other.key"));
assertEquals(
Optional.of(new AbstractMap.SimpleEntry<String, String>("default", "test")),
CryptoSupportInterpolatorCustomizer.extractKeyIdAndSuffix("vltaemencrypt.test"));
assertEquals(
Optional.of(new AbstractMap.SimpleEntry<String, String>("id1", "test")),
CryptoSupportInterpolatorCustomizer.extractKeyIdAndSuffix("vltaemencryptid1.test"));
assertEquals(Optional.empty(), CryptoSupportInterpolatorCustomizer.extractKeyIdAndSuffix("vltaemencryptid1."));
assertEquals(Optional.empty(), CryptoSupportInterpolatorCustomizer.extractKeyIdAndSuffix("vltaemencrypt."));
}
}

0 comments on commit 0e7960d

Please sign in to comment.