diff --git a/.gitattributes b/.gitattributes index 342dd57..2811f57 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ /gradlew text eol=lf *.bat text eol=crlf *.jar binary -*.java text eol=lf \ No newline at end of file +*.java text eol=lf +*.config text eol=lf \ No newline at end of file diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..078ebef --- /dev/null +++ b/lombok.config @@ -0,0 +1,2 @@ + lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value + lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Lazy \ No newline at end of file diff --git a/src/main/java/de/mlessmann/certassist/models/Passphrase.java b/src/main/java/de/mlessmann/certassist/models/Passphrase.java new file mode 100644 index 0000000..30060fd --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/models/Passphrase.java @@ -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; +} diff --git a/src/main/java/de/mlessmann/certassist/repositories/PassphraseRepository.java b/src/main/java/de/mlessmann/certassist/repositories/PassphraseRepository.java new file mode 100644 index 0000000..956dd94 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/repositories/PassphraseRepository.java @@ -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 findByIdentifierEquals(String identifier); +} diff --git a/src/main/java/de/mlessmann/certassist/service/CertificatePasswordProviderImpl.java b/src/main/java/de/mlessmann/certassist/service/CertificatePasswordProviderImpl.java new file mode 100644 index 0000000..88fc885 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/service/CertificatePasswordProviderImpl.java @@ -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); + } +} diff --git a/src/main/java/de/mlessmann/certassist/service/CertificateProviderImpl.java b/src/main/java/de/mlessmann/certassist/service/CertificateProviderImpl.java index 163e835..8e511aa 100644 --- a/src/main/java/de/mlessmann/certassist/service/CertificateProviderImpl.java +++ b/src/main/java/de/mlessmann/certassist/service/CertificateProviderImpl.java @@ -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); } } } diff --git a/src/main/java/de/mlessmann/certassist/service/PassphraseService.java b/src/main/java/de/mlessmann/certassist/service/PassphraseService.java new file mode 100644 index 0000000..0c2b072 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/service/PassphraseService.java @@ -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; + } +} diff --git a/src/test/java/de/mlessmann/certassist/config/TestAppEnvConfig.java b/src/test/java/de/mlessmann/certassist/config/TestAppEnvConfig.java new file mode 100644 index 0000000..841a487 --- /dev/null +++ b/src/test/java/de/mlessmann/certassist/config/TestAppEnvConfig.java @@ -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); + } + } +} diff --git a/src/test/java/de/mlessmann/certassist/service/InMemoryCertificatePasswordProvider.java b/src/test/java/de/mlessmann/certassist/service/InMemoryCertificatePasswordProvider.java index 9ffae8b..cecc028 100644 --- a/src/test/java/de/mlessmann/certassist/service/InMemoryCertificatePasswordProvider.java +++ b/src/test/java/de/mlessmann/certassist/service/InMemoryCertificatePasswordProvider.java @@ -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 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