feat: Implement feature to store symmetric passphrases in DB

This commit is contained in:
Magnus Leßmann (@MarkL4YG) 2024-11-22 09:45:55 +01:00
parent ac3821c949
commit 2b6473929a
9 changed files with 169 additions and 11 deletions

1
.gitattributes vendored
View file

@ -2,3 +2,4 @@
*.bat text eol=crlf
*.jar binary
*.java text eol=lf
*.config text eol=lf

2
lombok.config Normal file
View file

@ -0,0 +1,2 @@
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Lazy

View file

@ -0,0 +1,49 @@
package de.mlessmann.certassist.models;
import jakarta.persistence.*;
import java.time.ZonedDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
/**
* Database entity representing a passphrase.
* Unlike passwords which would be stored using BCrypt, passphrases are supposed to be reversible and are encrypted.
* @see de.mlessmann.certassist.service.PassphraseService
*/
@Entity
@Getter
@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "identifier" }) })
public class Passphrase {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Setter(AccessLevel.NONE)
private String id;
@Setter
@Column(nullable = false)
private String identifier;
@Setter
@Column(nullable = false)
private String encPassphrase;
@Setter
@Column(nullable = false)
private String salt;
@CreationTimestamp
@Column(nullable = false, updatable = false)
private ZonedDateTime created;
@UpdateTimestamp
@Column(nullable = false)
private ZonedDateTime updated;
@Setter
@Transient
private String passphrase;
}

View file

@ -0,0 +1,8 @@
package de.mlessmann.certassist.repositories;
import de.mlessmann.certassist.models.Passphrase;
import org.springframework.data.repository.CrudRepository;
public interface PassphraseRepository extends CrudRepository<Passphrase, String> {
Passphrase findByIdentifierEquals(String identifier);
}

View file

@ -0,0 +1,27 @@
package de.mlessmann.certassist.service;
import de.mlessmann.certassist.openssl.CertificatePasswordProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CertificatePasswordProviderImpl implements CertificatePasswordProvider {
private final PassphraseService passphraseService;
@Override
public String generateNewPassword() {
return PassphraseService.generateNewPassword(16);
}
@Override
public String getPasswordFor(String certificateFingerprint) {
return passphraseService.readPassphrase("cert:" + certificateFingerprint).getPassphrase();
}
@Override
public void setPasswordFor(String certificateFingerprint, String password) {
passphraseService.storePassphrase("cert:" + certificateFingerprint, password);
}
}

View file

@ -61,7 +61,7 @@ public class CertificateProviderImpl implements CertificateProvider {
Files.walkFileTree(this.tempDir, new DeleteRecursiveFileVisitor());
Files.deleteIfExists(this.tempDir);
} catch (IOException e) {
log.error("Unable to clean up temporary directory: " + this.tempDir, e);
log.error("Unable to clean up temporary directory: {}", this.tempDir, e);
}
}
}

View file

@ -0,0 +1,57 @@
package de.mlessmann.certassist.service;
import de.mlessmann.certassist.models.Passphrase;
import de.mlessmann.certassist.repositories.PassphraseRepository;
import java.security.SecureRandom;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.lang.NonNull;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class PassphraseService {
private static final String CHARACTERS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,.<>?";
public static String generateNewPassword(int length) {
SecureRandom random = new SecureRandom();
StringBuilder passphrase = new StringBuilder(length);
for (int i = 0; i < length; i++) {
passphrase.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length())));
}
return passphrase.toString();
}
@Value("${APP_KEY}")
private final String appKey;
private final PassphraseRepository passphraseRepository;
@NonNull
public Passphrase storePassphrase(@NonNull String identifier, @NonNull String passphrase) {
String salt = KeyGenerators.string().generateKey();
String encPassphrase = Encryptors.delux(appKey, salt).encrypt(passphrase);
var phrase = new Passphrase();
phrase.setIdentifier(identifier);
phrase.setSalt(salt);
phrase.setEncPassphrase(encPassphrase);
return passphraseRepository.save(phrase);
}
@NonNull
public Passphrase readPassphrase(String identifier) {
var passphrase = passphraseRepository.findByIdentifierEquals(identifier);
if (passphrase == null) {
throw new IllegalArgumentException("No passphrase found for identifier: " + identifier);
}
String unencPassphrase = Encryptors.delux(appKey, passphrase.getSalt()).decrypt(passphrase.getEncPassphrase());
passphrase.setPassphrase(unencPassphrase);
return passphrase;
}
}

View file

@ -0,0 +1,20 @@
package de.mlessmann.certassist.config;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
@Configuration
@Profile("test")
public class TestAppEnvConfig implements EnvironmentAware {
public static final String TEST_APP_KEY = "5i4Y)Ja0{YCl";
@Override
public void setEnvironment(Environment environment) {
if (environment.getProperty("APP_KEY") == null) {
System.setProperty("APP_KEY", TEST_APP_KEY);
}
}
}

View file

@ -1,29 +1,23 @@
package de.mlessmann.certassist.service;
import de.mlessmann.certassist.openssl.CertificatePasswordProvider;
import java.security.SecureRandom;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.stereotype.Service;
@Service
@ConditionalOnMissingBean(CertificatePasswordProvider.class)
public class InMemoryCertificatePasswordProvider implements CertificatePasswordProvider {
private final Map<String, String> passwords = new ConcurrentHashMap<>();
private static final String CHARACTERS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,.<>?";
private static final int PASSPHRASE_LENGTH = 16;
@Override
public String generateNewPassword() {
SecureRandom random = new SecureRandom();
StringBuilder passphrase = new StringBuilder(PASSPHRASE_LENGTH);
for (int i = 0; i < PASSPHRASE_LENGTH; i++) {
passphrase.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length())));
}
return passphrase.toString();
return PassphraseService.generateNewPassword(PASSPHRASE_LENGTH);
}
@Override