Skip to content

Commit

Permalink
Adding crypt mojos
Browse files Browse the repository at this point in the history
  • Loading branch information
rmannibucau committed Jun 29, 2023
1 parent f018bcc commit 6baca33
Show file tree
Hide file tree
Showing 19 changed files with 931 additions and 2 deletions.
34 changes: 33 additions & 1 deletion _documentation/src/main/minisite/content/mojos.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -512,4 +512,36 @@ To enable it, enable the related extension:
</build>
----

TIP: see `io.yupiik.maven.extension.YupiikOSSExtension` for a more complex participant example.
TIP: see `io.yupiik.maven.extension.YupiikOSSExtension` for a more complex participant example.

== Crypt mojos

Crypt mojos are intended to encrypt/decrypt (using `AES/CBC/PKCS5Padding` algorithm - like link:https://maven.apache.org/guides/mini/guide-encryption.html[maven default encryption]) values.
It supports either inline values or properties files.

TIP: it is a neat way to handle secrets in a bundlebee placeholder properties file on CI/CD if you don't have a vault or equivalent mecanism for GitOps.

=== Value based mojo

`yupiik-tools:crypt-value` will enable to encrypt a value (symmetrically, `decrypt-value` will decrypt a value).

Configuration is:

* `value`: the value to crypt (decrypt),
* `useStdout`: should the value be printed using Maven logger or simply `stdout`,
* `masterPassword`: the master password for the ciphering.

=== Properties based mojo

`yupiik-tools:crypt-value` will enable to encrypt a value (symmetrically, `decrypt-value` will decrypt a value).

Configuration is:

* `input`: path to the properties file to crypt (decrypt) *values* for (keys are never encrypted),
* `output`: path to the properties file which will contain encrypted (decrypted) data,
* `masterPassword`: the master password for the ciphering,
* `includedKeys`: a list of values (direct key), `start:<some prefix>` prefixes or `regex:<pattern>` regex to filter the values to encrypt (decrypt) based on their keys,
* `excludedKeys`: a list of values (direct key), `start:<some prefix>` prefixes or `regex:<pattern>` regex to filter the values to *NOT* encrypt (decrypt) based on their keys.

IMPORTANT: `output` always sorts the keys and will ignore comments. An already encoded value will not be re-encoded.
This last point enables to set `input`=`output` to encrypt in place a file.
36 changes: 36 additions & 0 deletions codec-core/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2020 - 2023 - Yupiik SAS - https://www.yupiik.com
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.yupiik.maven</groupId>
<artifactId>yupiik-tools-maven-plugin-parent</artifactId>
<version>1.1.5-SNAPSHOT</version>
</parent>

<artifactId>codec-core</artifactId>
<name>Yupiik Tools :: Codec</name>
<description>Simple codec logic to encrypt/decrypt properties.</description>

<dependencies>

</dependencies>
</project>
24 changes: 24 additions & 0 deletions codec-core/src/main/java/io/yupiik/tools/codec/Codec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2020 - 2023 - Yupiik SAS - https://www.yupiik.com
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.yupiik.tools.codec;

public interface Codec {
boolean isEncrypted(String value);

String encrypt(String input);

String decrypt(String value);
}
199 changes: 199 additions & 0 deletions codec-core/src/main/java/io/yupiik/tools/codec/simple/SimpleCodec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
* Copyright (c) 2020 - 2023 - Yupiik SAS - https://www.yupiik.com
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.yupiik.tools.codec.simple;

import io.yupiik.tools.codec.Codec;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static java.util.Optional.ofNullable;

// mimic and is compatible with maven password encryption for now
// todo: support stronger algorithms
public class SimpleCodec implements Codec {
private static final Pattern ENCRYPTED_PATTERN = Pattern.compile(".*?[^\\\\]?\\{(.*?[^\\\\])\\}.*");
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private static final String KEY_ALGORITHM = "AES";
private static final String DIGEST_ALGORITHM = "SHA-256";

private final SimpleCodecConfiguration configuration;
private final SecureRandom secureRandom;

public SimpleCodec(final SimpleCodecConfiguration configuration) {
this.configuration = configuration;
requireNonNull(configuration.getMasterPassword(), "No master password set");

this.secureRandom = new SecureRandom();
this.secureRandom.setSeed(Instant.now().toEpochMilli());
}

@Override
public boolean isEncrypted(final String value) {
return ENCRYPTED_PATTERN.matcher(value).matches();
}

@Override
public String encrypt(final String input) {
final byte[] salt = secureRandom.generateSeed(8);
secureRandom.nextBytes(salt);

final MessageDigest digester;
try {
digester = MessageDigest.getInstance(DIGEST_ALGORITHM);
} catch (final NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}

final var keyAndIv = new byte[32];
byte[] result;
int currentPos = 0;
while (currentPos < keyAndIv.length) {
digester.update(configuration.getMasterPassword().getBytes(StandardCharsets.UTF_8));
if (salt != null) {
digester.update(salt, 0, 8);
}
result = digester.digest();

final int stillNeed = keyAndIv.length - currentPos;
if (result.length > stillNeed) {
final var b = new byte[stillNeed];
System.arraycopy(result, 0, b, 0, b.length);
result = b;
}

System.arraycopy(result, 0, keyAndIv, currentPos, result.length);
currentPos += result.length;
if (currentPos < keyAndIv.length) {
digester.reset();
digester.update(result);
}
}

final var key = new byte[16];
final var iv = new byte[16];
System.arraycopy(keyAndIv, 0, key, 0, key.length);
System.arraycopy(keyAndIv, key.length, iv, 0, iv.length);

final byte[] encryptedBytes;
try {
final var cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, KEY_ALGORITHM), new IvParameterSpec(iv));
encryptedBytes = cipher.doFinal(input.getBytes(UTF_8));
} catch (final NoSuchPaddingException | NoSuchAlgorithmException |
InvalidKeyException | InvalidAlgorithmParameterException |
IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException(e);
}

final int len = encryptedBytes.length;
final byte padLen = (byte) (16 - (8 + len + 1) % 16);
final int totalLen = 8 + len + padLen + 1;
final byte[] allEncryptedBytes = secureRandom.generateSeed(totalLen);
System.arraycopy(salt, 0, allEncryptedBytes, 0, 8);
allEncryptedBytes[8] = padLen;

System.arraycopy(encryptedBytes, 0, allEncryptedBytes, 8 + 1, len);
return '{' + Base64.getEncoder().encodeToString(allEncryptedBytes) + '}';
}

@Override
public String decrypt(final String value) {
final Matcher matcher = ENCRYPTED_PATTERN.matcher(value);
if (!matcher.matches() && !matcher.find()) {
return value; // not encrypted, just use it
}

final String bare = matcher.group(1);
if (value.startsWith("${env.")) {
final String key = bare.substring("env.".length());
return ofNullable(System.getenv(key)).orElseGet(() -> System.getProperty(bare));
}
if (value.startsWith("${")) { // all is system prop, no interpolation yet
return System.getProperty(bare);
}

if (bare.contains("[") && bare.contains("]") && bare.contains("type=")) {
throw new IllegalArgumentException("Unsupported encryption for " + value);
}

final var allEncryptedBytes = Base64.getMimeDecoder().decode(bare);
final int totalLen = allEncryptedBytes.length;
final var salt = new byte[8];
System.arraycopy(allEncryptedBytes, 0, salt, 0, 8);
final byte padLen = allEncryptedBytes[8];
final var encryptedBytes = new byte[totalLen - 8 - 1 - padLen];
System.arraycopy(allEncryptedBytes, 8 + 1, encryptedBytes, 0, encryptedBytes.length);

try {
final var digest = MessageDigest.getInstance(DIGEST_ALGORITHM);
byte[] keyAndIv = new byte[16 * 2];
byte[] result;
int currentPos = 0;

while (currentPos < keyAndIv.length) {
digest.update(configuration.getMasterPassword().getBytes(StandardCharsets.UTF_8));

digest.update(salt, 0, 8);
result = digest.digest();

final int stillNeed = keyAndIv.length - currentPos;
if (result.length > stillNeed) {
final byte[] b = new byte[stillNeed];
System.arraycopy(result, 0, b, 0, b.length);
result = b;
}

System.arraycopy(result, 0, keyAndIv, currentPos, result.length);

currentPos += result.length;
if (currentPos < keyAndIv.length) {
digest.reset();
digest.update(result);
}
}

final var key = new byte[16];
final var iv = new byte[16];
System.arraycopy(keyAndIv, 0, key, 0, key.length);
System.arraycopy(keyAndIv, key.length, iv, 0, iv.length);

final var cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, KEY_ALGORITHM), new IvParameterSpec(iv));

final var clearBytes = cipher.doFinal(encryptedBytes);
return new String(clearBytes, StandardCharsets.UTF_8);
} catch (final Exception e) {
throw new IllegalStateException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2020 - 2023 - Yupiik SAS - https://www.yupiik.com
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.yupiik.tools.codec.simple;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import static lombok.AccessLevel.PRIVATE;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor(access = PRIVATE)
public class SimpleCodecConfiguration {
private String masterPassword;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2020 - 2023 - Yupiik SAS - https://www.yupiik.com
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.yupiik.tools.codec;

import io.yupiik.tools.codec.simple.SimpleCodec;
import io.yupiik.tools.codec.simple.SimpleCodecConfiguration;
import org.junit.jupiter.api.Test;

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

class SimpleCodecTest {
@Test
void roundTrip() {
final var codec = new SimpleCodec(SimpleCodecConfiguration.builder()
.masterPassword("123456")
.build());
final var encrypted = codec.encrypt("foo");
assertNotEquals("foo", encrypted);
assertTrue(encrypted.startsWith("{") && encrypted.endsWith("}"), encrypted);
assertEquals("foo", codec.decrypt(encrypted));
}
}
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
<module>yupiik-tools-cli</module>
<module>html-versioning-injector</module>
<module>_documentation</module>
<module>codec-core</module>
</modules>

<dependencyManagement>
Expand Down
5 changes: 5 additions & 0 deletions yupiik-tools-cli/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
<artifactId>minisite-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>codec-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>slides-core</artifactId>
Expand Down

0 comments on commit 6baca33

Please sign in to comment.