com.metaeffekt.artifact.analysis.flow.ng.KeypairGenerator Maven / Gradle / Ivy
/*
* Copyright 2021-2024 the original author or authors.
*
* 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 com.metaeffekt.artifact.analysis.flow.ng;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.metaeffekt.artifact.analysis.flow.ng.keyholder.UserKeysForConsumer;
import com.metaeffekt.artifact.analysis.flow.ng.keyholder.UserKeysForSupplier;
import com.metaeffekt.artifact.analysis.flow.ng.keyholder.UserKeysStorage;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.generators.RSAKeyPairGenerator;
import org.bouncycastle.crypto.params.RSAKeyGenerationParameters;
import org.bouncycastle.crypto.params.RSAKeyParameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.HashMap;
import java.util.UUID;
public class KeypairGenerator {
// WARNING: changing these constants may influence security properties.
// also, changing these may cause incompatibility.
private static final BigInteger publicExponent = BigInteger.valueOf(65535);
private static final int keySize = 4096;
public static final String supplierKeysFileEnding = ".supplier.keys.json";
public static final String consumerKeysFileEnding = ".consumer.keys";
public static final String consumerKeysMetadataFileEnding = ".meta.json";
private static final Logger LOG = LoggerFactory.getLogger(KeypairGenerator.class);
public static String generateUserKeysPassword(int length) {
SecureRandom random = new SecureRandom();
StringBuilder passwordBuilder = new StringBuilder();
// count and limit generation attempts as a cheap sanity-check at runtime
long failureCounter = 0;
long maxFailures = length * 128L;
while (passwordBuilder.length() < length) {
// get random integer truncated to 8 bits to only cover the ascii range
int randInt = random.nextInt(256);
// clip to ascii ranges
if (('a' < randInt && randInt < 'z') || ('A' < randInt && randInt < 'Z')) {
passwordBuilder.appendCodePoint(randInt);
} else {
failureCounter++;
if (failureCounter > maxFailures) {
throw new RuntimeException("should never happen: failure count exceeded while generating password");
}
}
}
return passwordBuilder.toString();
}
/**
* Generates a new keypair using the supplied random.
* Returns a pair where left is the "ForSupplier" part and right is "ForConsumer".
* @param random the random to use in generation.
* @return returns a keypair. Left is "ForSupplier", right "ForConsumer".
*/
public static Pair createUserKeyPair(SecureRandom random) {
RSAKeyPairGenerator gen = new RSAKeyPairGenerator();
gen.init(new RSAKeyGenerationParameters(publicExponent, random, keySize, 256));
AsymmetricCipherKeyPair kp = gen.generateKeyPair();
RSAKeyParameters publicKey = (RSAKeyParameters) kp.getPublic();
RSAKeyParameters privateKey = (RSAKeyParameters) kp.getPrivate();
byte[] hmacSecretKey = new byte[32];
random.nextBytes(hmacSecretKey);
UserKeysForSupplier keysForSupplier = new UserKeysForSupplier(publicKey, hmacSecretKey);
UserKeysForConsumer keysForConsumer = new UserKeysForConsumer(privateKey, hmacSecretKey);
return Pair.of(keysForSupplier, keysForConsumer);
}
public static void writeMetadataFile(String keyUuid, String password, File outputFile) throws IOException {
HashMap interMap = new HashMap<>();
interMap.put("userPassword", password);
interMap.put("creationDate", Instant.now().toString());
interMap.put("keyUuid", keyUuid);
try (OutputStream fos = Files.newOutputStream(outputFile.toPath(), StandardOpenOption.CREATE_NEW);
OutputStreamWriter writer = new OutputStreamWriter(fos, StandardCharsets.UTF_8)) {
new ObjectMapper().writerWithDefaultPrettyPrinter().writeValue(writer, interMap);
}
}
public static void generateNewUserKeypair(KeypairGeneratorSpec spec) throws IOException {
// filename as a user identifier. intended to uniquely associate a keypair with its user.
final String keyUuid = UUID.randomUUID().toString();
// ascii password used to encrypt the user keyfile.
final String consumerKeyfileEncryptionPassword = generateUserKeysPassword(spec.getPasswordLength());
// basic checks, abort early if anything is amiss
if (StringUtils.isBlank(spec.getReadableIdentifier())) {
throw new IllegalArgumentException("The readable identifier must not be blank.");
}
if (StringUtils.isBlank(consumerKeyfileEncryptionPassword)) {
throw new IllegalArgumentException("Generation requires a non-blank password.");
}
// i doubt anyone would configure the length this low but let's implement some warnings
if (consumerKeyfileEncryptionPassword.length() < 8) {
LOG.warn("Password is very short!");
} if (consumerKeyfileEncryptionPassword.length() < 23) {
LOG.warn("Password doesn't contain good security (should have at least 23 characters, 24 recommended).");
}
final File identifiableKeyOutputDir = new File(spec.getKeyOutputDir(), spec.getReadableIdentifier());
// where supplier's and consumer's keys will be put
final File forSupplierDir = new File(identifiableKeyOutputDir, "supplier");
final File forConsumerDir = new File(identifiableKeyOutputDir, "consumer");
// create files
final File supplierOutputFile = new File(forSupplierDir, keyUuid + supplierKeysFileEnding);
final File consumerEncryptedOutputFile = new File(forConsumerDir, keyUuid + consumerKeysFileEnding);
final File consumerPasswordOutputFile = new File(forConsumerDir, keyUuid + consumerKeysMetadataFileEnding);
// refuse output if any part of a keypair of the exact same identifier is already present
if (supplierOutputFile.exists()) {
throw new IllegalStateException("Won't write keys: supplier key file with specified id exists!");
}
if (consumerEncryptedOutputFile.exists()) {
throw new IllegalStateException("Won't write keys: consumer key file with specified id exists!");
}
if (consumerPasswordOutputFile.exists()) {
throw new IllegalStateException("Won't write keys: consumer password file with specified id exists!");
}
// generate the keypair
SecureRandom random = new SecureRandom();
LOG.info("Beginning generation for uuid '" + keyUuid + "'");
Pair keypair = createUserKeyPair(random);
UserKeysForSupplier keysForSupplier = keypair.getLeft();
UserKeysForConsumer keysForConsumer = keypair.getRight();
LOG.info("Beginning write for uuid '" + keyUuid + "'");
// create directories
if (!forSupplierDir.isDirectory()) {
if (!forSupplierDir.mkdirs()) {
LOG.error("Couldn't create dir at '" + forSupplierDir.getPath() + "'.");
throw new IOException("Could not create forSupplierDir.");
}
}
if (!forConsumerDir.isDirectory()) {
if (!forConsumerDir.mkdirs()) {
LOG.error("Couldn't create dir at '" + forConsumerDir.getPath() + "'.");
throw new IOException("Could not create forConsumerDir.");
}
}
// write keys
UserKeysStorage.writeUserKeys(keysForSupplier, supplierOutputFile);
// write the consumer key to the output file (encrypted with the password)
UserKeysStorage.writeUserKeys(keysForConsumer, consumerKeyfileEncryptionPassword, consumerEncryptedOutputFile);
// write the password to the output directory
writeMetadataFile(keyUuid, consumerKeyfileEncryptionPassword, consumerPasswordOutputFile);
LOG.info("Successfully wrote key '" + keyUuid + "'");
// check that the output keys are readable
UserKeysStorage.readUserKeysForConsumer(consumerEncryptedOutputFile, consumerKeyfileEncryptionPassword, true);
UserKeysStorage.readUserKeysForPublisher(supplierOutputFile, true);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy