feat: Implement feature to store symmetric passphrases in DB
This commit is contained in:
parent
ac3821c949
commit
2b6473929a
9 changed files with 169 additions and 11 deletions
1
.gitattributes
vendored
1
.gitattributes
vendored
|
@ -2,3 +2,4 @@
|
||||||
*.bat text eol=crlf
|
*.bat text eol=crlf
|
||||||
*.jar binary
|
*.jar binary
|
||||||
*.java text eol=lf
|
*.java text eol=lf
|
||||||
|
*.config text eol=lf
|
2
lombok.config
Normal file
2
lombok.config
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value
|
||||||
|
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Lazy
|
49
src/main/java/de/mlessmann/certassist/models/Passphrase.java
Normal file
49
src/main/java/de/mlessmann/certassist/models/Passphrase.java
Normal 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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -61,7 +61,7 @@ public class CertificateProviderImpl implements CertificateProvider {
|
||||||
Files.walkFileTree(this.tempDir, new DeleteRecursiveFileVisitor());
|
Files.walkFileTree(this.tempDir, new DeleteRecursiveFileVisitor());
|
||||||
Files.deleteIfExists(this.tempDir);
|
Files.deleteIfExists(this.tempDir);
|
||||||
} catch (IOException e) {
|
} 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,29 +1,23 @@
|
||||||
package de.mlessmann.certassist.service;
|
package de.mlessmann.certassist.service;
|
||||||
|
|
||||||
import de.mlessmann.certassist.openssl.CertificatePasswordProvider;
|
import de.mlessmann.certassist.openssl.CertificatePasswordProvider;
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ConditionalOnMissingBean(CertificatePasswordProvider.class)
|
||||||
public class InMemoryCertificatePasswordProvider implements CertificatePasswordProvider {
|
public class InMemoryCertificatePasswordProvider implements CertificatePasswordProvider {
|
||||||
|
|
||||||
private final Map<String, String> passwords = new ConcurrentHashMap<>();
|
private final Map<String, String> passwords = new ConcurrentHashMap<>();
|
||||||
private static final String CHARACTERS =
|
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,.<>?";
|
|
||||||
private static final int PASSPHRASE_LENGTH = 16;
|
private static final int PASSPHRASE_LENGTH = 16;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String generateNewPassword() {
|
public String generateNewPassword() {
|
||||||
SecureRandom random = new SecureRandom();
|
return PassphraseService.generateNewPassword(PASSPHRASE_LENGTH);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
Loading…
Add table
Reference in a new issue