Skip to content

Commit

Permalink
[crypt][properties] extract properties crypto logic from maven plugin…
Browse files Browse the repository at this point in the history
… to let it be reused more easily
  • Loading branch information
rmannibucau committed Jan 8, 2024
1 parent 6e82858 commit 5cf0213
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,35 @@
* specific language governing permissions and limitations
* under the License.
*/
package io.yupiik.maven.properties;
package io.yupiik.tools.codec.simple.properties;

import lombok.Data;
import org.apache.maven.plugin.logging.Log;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.function.Consumer;
import java.util.stream.Collector;

/**
* This class intends to be able to load properties but keep the original layout as much as possible
* and rewrite it with the comments in place.
*/
public class LightProperties {
private final Log log;
private final Consumer<String> warn;
private final List<Line> lines = new ArrayList<>();
private final Properties properties = new Properties();

public LightProperties(final Log log) {
this.log = log;
public LightProperties(final Consumer<String> warn) {
this.warn = warn;
}

public void load(final BufferedReader reader, final boolean usePlainProperties) throws IOException {
Expand Down Expand Up @@ -75,7 +77,7 @@ public void write(final Properties transformed, final Writer outputWriter) throw
} else {
final var newValue = transformed.getProperty(line.key);
if (newValue == null) {
log.warn("Missing value for '" + line.key + "'");
warn.accept("Missing value for '" + line.key + "'");
continue; // ignore
}

Expand Down Expand Up @@ -141,6 +143,13 @@ private void doLoad(final BufferedReader reader) {
}
}

public LightProperties load(final Path from, final boolean skipComments) throws IOException {
try (final var read = Files.newBufferedReader(from)) {
load(read, skipComments);
}
return this;
}

@Data
private static class Line {
private final String comment;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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.properties;

import io.yupiik.tools.codec.Codec;

import java.util.Collection;
import java.util.Objects;
import java.util.Properties;
import java.util.stream.Collector;

import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toMap;

/**
* Helper to (de)cipher a full properties file.
*/
public class PropertiesCodec {
private final Codec codec;

public PropertiesCodec(final Codec codec) {
this.codec = codec;
}

/**
* Cipher a properties instance.
*
* @param keys keys to cipher (others being ignored).
* @param from the values source.
* @param alreadyCiphered the already ciphered properties if it exists, enables to reduce the diff and only recipher what is clear {@code null} means ignore this.
* @return ciphered properties respecting keys and from parameters.
*/
public Properties crypt(final Collection<String> keys, final Properties from, final Properties alreadyCiphered) {
final Properties to = new Properties();
to.putAll((keys == null ? from.stringPropertyNames() : keys).stream().collect(toMap(identity(), e -> {
final var value = from.getProperty(e, "");
if (!value.isBlank() && codec.isEncrypted(value)) {
return value;
}

if (alreadyCiphered != null) {
final var existingValue = alreadyCiphered.getProperty(e);
if (existingValue != null && codec.isEncrypted(existingValue) && equals(codec, existingValue, value)) {
return existingValue;
}
}

return codec.encrypt(value);
})));
return to;
}

/**
* Decipher a properties instance.
*
* @param from the data to decipher (values).
* @return the clear properties.
*/
public Properties decrypt(final Properties from) {
return from.stringPropertyNames().stream().collect(Collector.of(
Properties::new,
(a, e) -> {
final var property = from.getProperty(e, "");
if (codec.isEncrypted(property)) {
a.setProperty(e, codec.decrypt(property));
} else {
a.setProperty(e, property);
}
},
(a, b) -> {
a.putAll(b);
return a;
}));
}

private boolean equals(final Codec codec, final String existingValue, final String value) {
try {
return Objects.equals(value, codec.decrypt(existingValue));
} catch (final RuntimeException re) {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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.properties;

import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import static java.util.Collections.enumeration;
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toList;

/**
* Simple helper to sort properties per key.
*/
public class SortedProperties extends Properties { // sorted...depends jvm version so override most of them
@Override
public Set<String> stringPropertyNames() {
return super.stringPropertyNames().stream().sorted().collect(toCollection(LinkedHashSet::new));
}

@Override
public Enumeration<Object> keys() {
return enumeration(Collections.list(super.keys()).stream()
.sorted(Comparator.comparing(Object::toString))
.collect(toList()));
}

@Override
public Set<Map.Entry<Object, Object>> entrySet() {
return super.entrySet().stream()
.sorted(Comparator.comparing(a -> a.getKey().toString()))
.collect(toCollection(LinkedHashSet::new));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,23 @@
* specific language governing permissions and limitations
* under the License.
*/
package io.yupiik.maven.properties;
package io.yupiik.tools.codec.simple.properties;

import org.apache.maven.plugin.logging.SystemStreamLog;
import org.junit.jupiter.api.Test;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Properties;

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

class LightPropertiesTest {
@Test
void unescape() throws IOException {
final var props = new LightProperties(new SystemStreamLog());
final var props = new LightProperties(m -> {
throw new IllegalStateException(m);
});
try (final var r = new BufferedReader(new StringReader("" +
"escaped = yes\\=true\n" +
"split = foo\\\n" +
Expand All @@ -48,7 +48,9 @@ void unescape() throws IOException {

@Test
void rewrite() throws IOException {
final var props = new LightProperties(new SystemStreamLog());
final var props = new LightProperties(m -> {
throw new IllegalStateException(m);
});
try (final var r = new BufferedReader(new StringReader("" +
"# this is a comment\n" +
"\n" +
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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.properties;

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

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.Set;

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

class PropertiesCodecTest {
@Test
void roundTrip() throws IOException {
final var loaded = new LightProperties(e -> {
throw new IllegalStateException(e);
});
try (final var b = new BufferedReader(new StringReader("#test\na = b\nc=d"))) {
loaded.load(b, false);
}
final var clear = loaded.toWorkProperties();

final var configuration = new SimpleCodecConfiguration();
configuration.setMasterPassword("foo");

final var codec = new PropertiesCodec(new SimpleCodec(configuration));
final var ciphered = codec.crypt(null, clear, null);
assertEquals(Set.of("a", "c"), ciphered.stringPropertyNames());
// ensure it is ciphered
ciphered.stringPropertyNames().forEach(k -> {
final var value = ciphered.getProperty(k);
assertTrue(value.length() > 2 && value.startsWith("{") && value.endsWith("}"), () -> k + "=" + ciphered.getProperty(k));
});
assertEquals(clear, codec.decrypt(ciphered));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,23 @@
*/
package io.yupiik.maven.mojo;

import io.yupiik.maven.properties.LightProperties;
import io.yupiik.tools.codec.Codec;
import io.yupiik.tools.codec.simple.properties.LightProperties;
import io.yupiik.tools.codec.simple.properties.SortedProperties;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Parameter;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Pattern;

import static java.util.Collections.enumeration;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;

public abstract class BaseCryptPropertiesMojo extends BaseCryptMojo {
Expand Down Expand Up @@ -83,10 +74,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
}

try {
final var input = new LightProperties(getLog());
try (final var read = Files.newBufferedReader(from)) {
input.load(read, !preserveComments);
}
final var input = new LightProperties(getLog()::warn).load(from, !preserveComments);
final var inputProps = input.toWorkProperties();

final var keyFilter = createKeyFilter();
Expand All @@ -101,26 +89,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
.filter(keyFilter)
.collect(toMap(identity(), inputProps::getProperty)));

final var transformed = new Properties() { // sorted...depends jvm version so override most of them
@Override
public Set<String> stringPropertyNames() {
return super.stringPropertyNames().stream().sorted().collect(toCollection(LinkedHashSet::new));
}

@Override
public Enumeration<Object> keys() {
return enumeration(Collections.list(super.keys()).stream()
.sorted(Comparator.comparing(Object::toString))
.collect(toList()));
}

@Override
public Set<Map.Entry<Object, Object>> entrySet() {
return super.entrySet().stream()
.sorted(Comparator.comparing(a -> a.getKey().toString()))
.collect(toCollection(LinkedHashSet::new));
}
};
final var transformed = new SortedProperties();
transform(codec(), transformedSource, transformed);
if (!untouched.isEmpty()) {
transformed.putAll(untouched);
Expand Down

0 comments on commit 5cf0213

Please sign in to comment.