totpgenerator

Generate TOTP verification codes based on encrypted GPG files.
git clone git://git.wimdupont.com/totpgenerator.git
Log | Files | Refs | README | LICENSE

commit 2d00900f731f57723bf7418ffb484eed46bafb68
parent a0673eaa944125a46fb9d741d51389b54e1ab254
Author: Wim Dupont <wim@wimdupont.com>
Date:   Sat, 23 Dec 2023 21:10:30 +0100

properties as singleton

Diffstat:
Mpom.xml | 10++++++++--
Msrc/main/java/com/wimdupont/Main.java | 5++---
Msrc/main/java/com/wimdupont/service/ApplicationProperties.java | 10+++++-----
Msrc/main/java/com/wimdupont/service/GpgService.java | 19++++++-------------
Msrc/test/java/com/wimdupont/service/GpgServiceTest.java | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
5 files changed, 89 insertions(+), 45 deletions(-)

diff --git a/pom.xml b/pom.xml @@ -15,7 +15,7 @@ <bcpg-jdk18on.version>1.77</bcpg-jdk18on.version> <otp-java.version>2.0.3</otp-java.version> <junit-jupiter-engine.version>5.10.1</junit-jupiter-engine.version> - <mockito-core.version>5.8.0</mockito-core.version> + <mockito.version>5.8.0</mockito.version> <maven-surefire-plugin.version>3.2.3</maven-surefire-plugin.version> </properties> @@ -44,7 +44,13 @@ <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> - <version>${mockito-core.version}</version> + <version>${mockito.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-junit-jupiter</artifactId> + <version>${mockito.version}</version> <scope>test</scope> </dependency> </dependencies> diff --git a/src/main/java/com/wimdupont/Main.java b/src/main/java/com/wimdupont/Main.java @@ -1,16 +1,15 @@ package com.wimdupont; import com.bastiaanjansen.otp.TOTPGenerator; +import com.wimdupont.service.ApplicationProperties; import com.wimdupont.service.GpgService; -import com.wimdupont.service.PropertiesLoader; public class Main { public static void main(String[] args) { try { - var gpgService = new GpgService(new PropertiesLoader().loadProperties()); System.out.println(TOTPGenerator - .withDefaultValues(gpgService + .withDefaultValues(new GpgService(ApplicationProperties.getInstance()) .decrypt(getFileArg(args))) .now()); } catch (Exception e) { diff --git a/src/main/java/com/wimdupont/service/ApplicationProperties.java b/src/main/java/com/wimdupont/service/ApplicationProperties.java @@ -5,9 +5,9 @@ import java.security.InvalidParameterException; import java.util.MissingResourceException; import java.util.Properties; -public class PropertiesLoader { +public class ApplicationProperties { - private static PropertiesLoader instance; + private static ApplicationProperties instance; private final String dirPath; private final String secretFile; @@ -19,15 +19,15 @@ public class PropertiesLoader { return secretFile; } - public static PropertiesLoader getInstance() { + public static ApplicationProperties getInstance() { if (instance == null) { - instance = new PropertiesLoader(); + instance = new ApplicationProperties(); } return instance; } - private PropertiesLoader(){ + private ApplicationProperties(){ var properties = loadProperties(); dirPath = properties.getProperty("dir.path"); secretFile = properties.getProperty("secret.file"); diff --git a/src/main/java/com/wimdupont/service/GpgService.java b/src/main/java/com/wimdupont/service/GpgService.java @@ -23,23 +23,15 @@ import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.security.InvalidParameterException; import java.security.Security; import java.util.Iterator; -import java.util.Properties; public class GpgService { - private final String dirPath; - private final String secretFile; + private final ApplicationProperties applicationProperties; - public GpgService(Properties properties) { - dirPath = properties.getProperty("dir.path"); - secretFile = properties.getProperty("secret.file"); - - if (dirPath == null | secretFile == null) { - throw new InvalidParameterException("Properties missing."); - } + public GpgService(ApplicationProperties applicationProperties){ + this.applicationProperties = applicationProperties; } public byte[] decrypt(String fileName) throws IOException, PGPException { @@ -58,7 +50,8 @@ public class GpgService { } private Iterator<PGPEncryptedData> getPgpEncryptedDataIterator(String fileName) throws IOException { - var secretDecoderStream = PGPUtil.getDecoderStream(KeyRetriever.getSecretKeyInputStream(fileName, dirPath)); + var secretDecoderStream = PGPUtil.getDecoderStream(KeyRetriever + .getSecretKeyInputStream(fileName, applicationProperties.getDirPath())); var pgpObjectFactory = new PGPObjectFactory(secretDecoderStream, new BcKeyFingerprintCalculator()); var nextObject = pgpObjectFactory.nextObject(); @@ -68,7 +61,7 @@ public class GpgService { } private PGPPrivateKey retrievePgpPrivateKey(PGPPublicKeyEncryptedData pgpPublicKeyEncryptedData) throws IOException, PGPException { - var pgpPrivateKey = readSecretKey(new FileInputStream(secretFile), pgpPublicKeyEncryptedData.getKeyID()) + var pgpPrivateKey = readSecretKey(new FileInputStream(applicationProperties.getSecretFile()), pgpPublicKeyEncryptedData.getKeyID()) .extractPrivateKey(new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()) .build(PasswordReader.getPassword())); diff --git a/src/test/java/com/wimdupont/service/GpgServiceTest.java b/src/test/java/com/wimdupont/service/GpgServiceTest.java @@ -3,73 +3,119 @@ package com.wimdupont.service; import org.bouncycastle.openpgp.PGPException; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Paths; -import java.util.Properties; import static org.mockito.Mockito.mockStatic; +@ExtendWith(MockitoExtension.class) public class GpgServiceTest { + private static final String TOTP_SECRET_FILE = "encrypted-totp-secret"; private static final char[] PASSWORD = {'s', 'e', 'c', 'r', 'e', 't', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'}; + private static String dirPath; + private static String secretFile; + private GpgService gpgService; + @Mock + private ApplicationProperties applicationProperties; - @BeforeEach - public void setup() { + @BeforeAll + public static void setupClass() { var resourceDirectory = Paths.get("src", "test", "resources") .toAbsolutePath() .toString(); - var properties = new Properties(); - - properties.setProperty("dir.path", String.format("%s/totpfiles/", resourceDirectory)); - properties.setProperty("secret.file", String.format("%s/private-key.asc", resourceDirectory)); + dirPath = String.format("%s/totpfiles/", resourceDirectory); + secretFile = String.format("%s/private-key.asc", resourceDirectory); + } - gpgService = new GpgService(properties); + @BeforeEach + public void setup() { + gpgService = new GpgService(applicationProperties); } @Test public void decryptWithCorrectPasswordShouldReturnCorrectSecret() throws PGPException, IOException { - try (MockedStatic<PasswordReader> mocked = mockStatic(PasswordReader.class)) { - mocked.when(PasswordReader::getPassword).thenReturn(PASSWORD); + Mockito.when(applicationProperties.getDirPath()).thenReturn(dirPath); + Mockito.when(applicationProperties.getSecretFile()).thenReturn(secretFile); + try (MockedStatic<PasswordReader> mockedPasswordReader = mockStatic(PasswordReader.class)) { + mockedPasswordReader.when(PasswordReader::getPassword).thenReturn(PASSWORD); - var result = gpgService.decrypt("encrypted-totp-secret"); + var result = gpgService.decrypt(TOTP_SECRET_FILE); Assertions.assertNotNull(result); - Assertions.assertEquals("This is the big secret key", - new String(result, Charset.defaultCharset())); + Assertions.assertEquals("This is the big secret key", new String(result, Charset.defaultCharset())); } } @Test public void decryptWithWrongPasswordShouldThrowPgpException() { - try (MockedStatic<PasswordReader> mocked = mockStatic(PasswordReader.class)) { - mocked.when(PasswordReader::getPassword).thenReturn(new char[]{'w', 'r', 'o', 'n', 'g'}); + Mockito.when(applicationProperties.getDirPath()).thenReturn(dirPath); + Mockito.when(applicationProperties.getSecretFile()).thenReturn(secretFile); + try (MockedStatic<PasswordReader> mockedPasswordReader = mockStatic(PasswordReader.class)) { + mockedPasswordReader.when(PasswordReader::getPassword).thenReturn(new char[]{'w', 'r', 'o', 'n', 'g'}); Exception exception = Assertions.assertThrows(PGPException.class, () -> - gpgService.decrypt("encrypted-totp-secret") + gpgService.decrypt(TOTP_SECRET_FILE) ); - Assertions.assertEquals(exception.getClass(), PGPException.class); + Assertions.assertEquals(PGPException.class, exception.getClass()); } } @Test - public void decryptUnknownFileShouldThrowException() { - try (MockedStatic<PasswordReader> mocked = mockStatic(PasswordReader.class)) { - mocked.when(PasswordReader::getPassword).thenReturn(PASSWORD); + public void decryptUnknownFileNameArgShouldThrowException() { + Mockito.when(applicationProperties.getDirPath()).thenReturn(dirPath); + try (MockedStatic<PasswordReader> mockedPasswordReader = mockStatic(PasswordReader.class)) { + mockedPasswordReader.when(PasswordReader::getPassword).thenReturn(PASSWORD); Exception exception = Assertions.assertThrows(FileNotFoundException.class, () -> gpgService.decrypt("unexisting") ); - Assertions.assertEquals(exception.getClass(), FileNotFoundException.class); - Assertions.assertEquals(exception.getMessage(), "File \"unexisting.gpg\" not found"); + Assertions.assertEquals(FileNotFoundException.class, exception.getClass()); + Assertions.assertEquals("File \"unexisting.gpg\" not found", exception.getMessage()); + } + } + + @Test + public void decryptUnknownFileFromDirPathShouldThrowException() { + Mockito.when(applicationProperties.getDirPath()).thenReturn("/wrong/path/"); + try (MockedStatic<PasswordReader> mockedPasswordReader = mockStatic(PasswordReader.class)) { + mockedPasswordReader.when(PasswordReader::getPassword).thenReturn(PASSWORD); + + Exception exception = Assertions.assertThrows(FileNotFoundException.class, () -> + gpgService.decrypt(TOTP_SECRET_FILE) + ); + + Assertions.assertEquals(FileNotFoundException.class, exception.getClass()); + Assertions.assertEquals("File \"encrypted-totp-secret.gpg\" not found", exception.getMessage()); + } + } + + @Test + public void decryptUnknownFileFromSecretFileShouldThrowException() { + Mockito.when(applicationProperties.getDirPath()).thenReturn(dirPath); + Mockito.when(applicationProperties.getSecretFile()).thenReturn("/wrong/unexisting.asc"); + try (MockedStatic<PasswordReader> mockedPasswordReader = mockStatic(PasswordReader.class)) { + mockedPasswordReader.when(PasswordReader::getPassword).thenReturn(PASSWORD); + + Exception exception = Assertions.assertThrows(FileNotFoundException.class, () -> + gpgService.decrypt(TOTP_SECRET_FILE) + ); + + Assertions.assertEquals(FileNotFoundException.class, exception.getClass()); } } } \ No newline at end of file