Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,26 @@

package org.kitodo.production.forms;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.text.MessageFormat;
import java.util.Objects;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.util.Strings;
import org.kitodo.config.ConfigCore;
import org.kitodo.config.enums.ParameterCore;
import org.kitodo.data.database.beans.LdapServer;
import org.kitodo.data.database.enums.PasswordEncryption;
import org.kitodo.data.database.exceptions.DAOException;
import org.kitodo.production.enums.ObjectType;
import org.kitodo.production.helper.Helper;
import org.kitodo.production.security.AESUtil;
import org.kitodo.production.security.encrypt.EncryptionUtils;
import org.kitodo.production.security.encrypt.KitodoEncryptionException;
import org.kitodo.production.services.ServiceManager;

@Named("LdapServerEditView")
Expand Down Expand Up @@ -107,21 +101,17 @@ public void load(int id) {
* @return The manager password
*/
public String getManagerPassword() {
if (AESUtil.isEncrypted(ldapServer.getManagerPassword())) {
String securitySecret = ConfigCore.getParameterOrDefaultValue(ParameterCore.SECURITY_SECRET_LDAPMANAGERPASSWORD);

if (StringUtils.isNotBlank(securitySecret)) {
try {
return AESUtil.decrypt(ldapServer.getManagerPassword(), securitySecret);
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException
| InvalidKeyException | BadPaddingException | IllegalBlockSizeException
| InvalidKeySpecException e) {
Helper.setErrorMessage(ERROR_LOADING_ONE, new Object[] {ObjectType.LDAP_SERVER.getTranslationSingular()}, logger, e);
}
String managerPassword = ldapServer.getManagerPassword();
String securitySecret = ConfigCore.getParameterOrDefaultValue(ParameterCore.SECURITY_SECRET_LDAPMANAGERPASSWORD);
if (Objects.nonNull(managerPassword) && !Strings.isBlank(securitySecret)) {
try {
return EncryptionUtils.decrypt(ldapServer.getManagerPassword(), securitySecret);
} catch (KitodoEncryptionException e) {
Helper.setErrorMessage(ERROR_LOADING_ONE, new Object[] {ObjectType.LDAP_SERVER.getTranslationSingular()}, logger, e);
}

}
return ldapServer.getManagerPassword();
return managerPassword;
}

/**
Expand All @@ -133,11 +123,10 @@ public void setManagerPassword(String managerPassword) {
try {
String securitySecret = ConfigCore.getParameterOrDefaultValue(ParameterCore.SECURITY_SECRET_LDAPMANAGERPASSWORD);
if (StringUtils.isNotBlank(securitySecret)) {
managerPassword = AESUtil.encrypt(managerPassword, securitySecret);
managerPassword = EncryptionUtils.encrypt(managerPassword, securitySecret);
}
ldapServer.setManagerPassword(managerPassword);
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException
| InvalidKeyException | BadPaddingException | IllegalBlockSizeException | InvalidKeySpecException e) {
} catch (KitodoEncryptionException e) {
Helper.setErrorMessage(ERROR_SAVING, new Object[] {ObjectType.LDAP_SERVER.getTranslationSingular()}, logger, e);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@
package org.kitodo.production.forms;

import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
Expand All @@ -28,10 +24,6 @@
import java.util.Set;
import java.util.stream.Collectors;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Named;

Expand All @@ -58,7 +50,8 @@
import org.kitodo.production.helper.tasks.UpdateInternalMetaInformationTask;
import org.kitodo.production.migration.NewspaperProcessesMigrator;
import org.kitodo.production.migration.TasksToWorkflowConverter;
import org.kitodo.production.security.AESUtil;
import org.kitodo.production.security.encrypt.EncryptionUtils;
import org.kitodo.production.security.encrypt.KitodoEncryptionException;
import org.kitodo.production.services.ServiceManager;
import org.kitodo.production.services.migration.MigrationService;
import org.kitodo.production.workflow.model.Converter;
Expand Down Expand Up @@ -583,19 +576,17 @@ public void startLdapManagerPasswordsMigration() {
List<LdapServer> ldapServers = getLdapServers();
ldapServers.parallelStream().forEach(ldapServer -> {
String managerPassword = ldapServer.getManagerPassword();
if (StringUtils.isNotBlank(managerPassword) && !AESUtil.isEncrypted(managerPassword)) {
if (StringUtils.isNotBlank(managerPassword) && EncryptionUtils.needsUpgrade(managerPassword)) {
try {
ldapServer.setManagerPassword(AESUtil.encrypt(managerPassword, securitySecret));
ldapServer.setManagerPassword(EncryptionUtils.upgradeEncryption(managerPassword, securitySecret));
ServiceManager.getLdapServerService().save(ldapServer);
} catch (DAOException | NoSuchPaddingException | NoSuchAlgorithmException
| InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException
| IllegalBlockSizeException | InvalidKeySpecException e) {
} catch (KitodoEncryptionException | DAOException e) {
Helper.setErrorMessage(e.getLocalizedMessage(), logger, e);
}
}
});

Helper.setMessage("All unencrypted LDAP Manager passwords were successfully encrypted.");
Helper.setMessage("All LDAP Manager passwords were successfully encrypted or upgraded to a more modern encryption algorithm.");
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* (c) Kitodo. Key to digital objects e. V. <contact@kitodo.org>
*
* This file is part of the Kitodo project.
*
* It is licensed under GNU General Public License version 3 or later.
*
* For the full copyright and license information, please read the
* GPL3-License.txt file that was distributed with this source code.
*/

package org.kitodo.production.security.encrypt;

import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.params.Argon2Parameters;

/**
* Encryption strategy based on AES-GCM with Argon2id encoded 256-bit secret key.
*
* <p>DO NOT CHANGE! If this file is changed, it may not be possible to decrypt previously encrypted values.
* Instead, add a new encryption strategy implemnentation to EncryptionUtils and migrate to that.</p>
*/
public class AesGcmEncryptionStrategy implements KitodoEncryptionStrategy {

private static final SecureRandom secureRandom = new SecureRandom();

// encryption parameters
private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
private static final String SALT_PREFIX = "KITODO";
private static final int SALT_LENGTH = 16;
private static final int GCM_IV_LENGTH = 16;
private static final int GCM_TAG_LENGTH = 128;

// argon2id parameter based on spring-security defaults
// see org.springframework.security.crypto.argon2.Argon2PasswordEncoder
private static final int ARGON2ID_MEMORY_KB = 1 << 14; // 16 MiB
private static final int ARGON2ID_ITERATIONS = 2;
private static final int ARGON2ID_PARALLELISM = 1;
private static final int ARGON2ID_KEY_LENGTH = 32; // 256 bits (required for AES)

@Override
public String encrypt(String value, String secret) throws KitodoEncryptionException {
try {
// generate salt
byte[] salt = new byte[SALT_LENGTH];
secureRandom.nextBytes(salt);
System.arraycopy(SALT_PREFIX.getBytes(), 0, salt, 0, SALT_PREFIX.getBytes().length);

// generate iv (or nonce)
byte[] iv = new byte[GCM_IV_LENGTH];
secureRandom.nextBytes(iv);

// encrypt
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(secret, salt), new GCMParameterSpec(GCM_TAG_LENGTH, iv));
byte[] cipherText = cipher.doFinal(value.getBytes(StandardCharsets.UTF_8));

// combine to [salt + cipher + iv]
byte[] combined = new byte[salt.length + iv.length + cipherText.length];
System.arraycopy(salt, 0, combined, 0, SALT_LENGTH);
System.arraycopy(cipherText, 0, combined, SALT_LENGTH, cipherText.length);
System.arraycopy(iv, 0, combined, SALT_LENGTH + cipherText.length, iv.length);

// encode with base64
return Base64.getEncoder().encodeToString(combined);
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeyException
| BadPaddingException | IllegalBlockSizeException e) {
throw new KitodoEncryptionException(e);
}
}

@Override
public String decrypt(String encryptedValue, String secret) throws KitodoEncryptionException {
try {
// decode from base64
byte[] combined = Base64.getDecoder().decode(encryptedValue);

// extract [salt + cipher + iv] from combined string
byte[] salt = Arrays.copyOfRange(combined, 0, SALT_LENGTH);
byte[] cipherText = Arrays.copyOfRange(combined, SALT_LENGTH, combined.length - GCM_IV_LENGTH);
byte[] iv = Arrays.copyOfRange(combined, combined.length - GCM_IV_LENGTH, combined.length);

// decrypt
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(secret, salt), new GCMParameterSpec(GCM_TAG_LENGTH, iv));
byte[] value = cipher.doFinal(cipherText);

return new String(value, StandardCharsets.UTF_8);
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeyException
| BadPaddingException | IllegalBlockSizeException | IllegalArgumentException e) {
throw new KitodoEncryptionException(e);
}
}

/**
* Generate a 256bit key from the user-defined secret+salt (with variable length) such that these 256bit
* can be used as secret key for the AES encryption.
*
* @param secret the user-defined secret
* @param salt the salt
* @return the 256bit secret key to be uesd with AES
*/
private static SecretKey getSecretKey(String secret, byte[] salt) {
// specify argon2id parameters
Argon2Parameters params =
new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
.withSalt(salt)
.withMemoryAsKB(ARGON2ID_MEMORY_KB)
.withIterations(ARGON2ID_ITERATIONS)
.withParallelism(ARGON2ID_PARALLELISM)
.build();

// generate 256-bit key
byte[] password = secret.getBytes(StandardCharsets.UTF_8);
byte[] key = new byte[ARGON2ID_KEY_LENGTH];
Argon2BytesGenerator generator = new Argon2BytesGenerator();
generator.init(params);
generator.generateBytes(password, key);

// return key as spec to be used for AES encryption
return new SecretKeySpec(key, "AES");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* (c) Kitodo. Key to digital objects e. V. <contact@kitodo.org>
*
* This file is part of the Kitodo project.
*
* It is licensed under GNU General Public License version 3 or later.
*
* For the full copyright and license information, please read the
* GPL3-License.txt file that was distributed with this source code.
*/

package org.kitodo.production.security.encrypt;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class EncryptionUtils {

private static final String LEGACY_ENCRYPTION_STRATEGY_KEY = null;
private static final String AES_GCM_ENCRYPTION_STRATEGY_KEY = "aes-gcm";
private static final String DEFAULT_ENCRYPTION_STRATEGY_KEY = AES_GCM_ENCRYPTION_STRATEGY_KEY;

private static final Map<String, KitodoEncryptionStrategy> STRATEGIES;

static {
STRATEGIES = new HashMap<>();
STRATEGIES.put(LEGACY_ENCRYPTION_STRATEGY_KEY, new LegacyEncryptionStrategy());
STRATEGIES.put(AES_GCM_ENCRYPTION_STRATEGY_KEY, new AesGcmEncryptionStrategy());
}

/**
* Encrypt text using a secret based on the default encryption strategy.
*
* @param value the text to be encrypted
* @param secret the secret used for encryption
* @return the encrypted text
* @throws KitodoEncryptionException in case encryption fails
*/
public static String encrypt(String value, String secret) throws KitodoEncryptionException {
if (Objects.isNull(secret) || secret.isBlank()) {
throw new KitodoEncryptionException("Cannot encrypt without secret");
}
if (Objects.isNull(value)) {
throw new KitodoEncryptionException("Cannot encrypt null value");
}
String strategyKey = DEFAULT_ENCRYPTION_STRATEGY_KEY;
return String.format("{%s}%s", strategyKey, STRATEGIES.get(strategyKey).encrypt(value, secret));
}

/**
* Decrypt previously encrypted text using a secret.
*
* <p>Automatically detects the encryption strategy (via a prefix) and applies the correct decryption algorithm.</p>
*
* @param encryptedValue the encrypted text
* @param secret the secret that was used for encryption
* @return the decrypted text
* @throws KitodoEncryptionException in case decryption fails
*/
public static String decrypt(String encryptedValue, String secret) throws KitodoEncryptionException {
if (Objects.isNull(secret) || secret.isBlank()) {
throw new KitodoEncryptionException("Cannot decrypt without secret");
}
if (Objects.isNull(encryptedValue)) {
throw new KitodoEncryptionException("Cannot decrypt null value");
}
String strategyKey = identifyStrategy(encryptedValue);
String encryptedValueWithoutSuffix = encryptedValue.substring(Objects.isNull(strategyKey) ? 0 : strategyKey.length() + 2);
return STRATEGIES.get(strategyKey).decrypt(encryptedValueWithoutSuffix, secret);
}

/**
* Return true if an encrypted value needs upgrading to a better encryption strategy.
*
* @param encryptedValue the encrypted value
* @return true if upgrading is needed
*/
public static boolean needsUpgrade(String encryptedValue) {
String strategyKey = identifyStrategy(encryptedValue);
if (DEFAULT_ENCRYPTION_STRATEGY_KEY.equals(strategyKey)) {
return false;
}
return true;
}

/**
* Upgrade an encrypted value to the current default encryption strategy.
*
* @param encryptedValue the encrypted value
* @param secret the secret key that was used for encryption
* @return the value encrypted with the current default encryption strategy
* @throws KitodoEncryptionException in case encryption or decryption fails
*/
public static String upgradeEncryption(String encryptedValue, String secret) throws KitodoEncryptionException {
return encrypt(decrypt(encryptedValue, secret), secret);
}

/**
* Identifies the encryption strategy that was used to encrypt the provided value.
*
* @param encryptedValue the encrypted value
* @return the strategy key of the strategy that was used for encryption (or null if not known)
*/
private static String identifyStrategy(String encryptedValue) {
for (String key : STRATEGIES.keySet()) {
if (encryptedValue.startsWith(String.format("{%s}", key))) {
return key;
}
}
return LEGACY_ENCRYPTION_STRATEGY_KEY;
}

}
Loading
Loading