totpgenerator

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

commit fbb6ad0646e7bd20bc8a752aa8ccc2c998109a36
parent 469c713786018aa344604aebc283a1e943cb4ffb
Author: Wim Dupont <wim@wimdupont.com>
Date:   Sat, 23 Dec 2023 14:00:25 +0100

refactor and added tests

Diffstat:
Mpom.xml | 46++++++++++++++++++++++++++++++++++++----------
Msrc/main/java/com/wimdupont/Main.java | 26+++++++++++++++++---------
Asrc/main/java/com/wimdupont/service/GpgService.java | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/main/java/com/wimdupont/service/GpgUtil.java | 117-------------------------------------------------------------------------------
Asrc/main/java/com/wimdupont/service/KeyRetriever.java | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main/java/com/wimdupont/service/PasswordReader.java | 14+++++++++-----
Msrc/main/java/com/wimdupont/service/PropertiesLoader.java | 10++++++----
Asrc/test/java/com/wimdupont/service/GpgServiceTest.java | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/test/resources/private-key.asc | 16++++++++++++++++
Asrc/test/resources/totpfiles/encrypted-totp-secret.gpg | 0
10 files changed, 331 insertions(+), 145 deletions(-)

diff --git a/pom.xml b/pom.xml @@ -7,31 +7,48 @@ <groupId>com.wimdupont</groupId> <artifactId>TotpGenerator</artifactId> <version>1.0-SNAPSHOT</version> + + <properties> + <maven.compiler.source>21</maven.compiler.source> + <maven.compiler.target>21</maven.compiler.target> + <commons-codec.version>1.16.0</commons-codec.version> + <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-inline.version>5.2.0</mockito-inline.version> + <maven-surefire-plugin.version>3.2.3</maven-surefire-plugin.version> + </properties> + <dependencies> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> - <version>1.16.0</version> + <version>${commons-codec.version}</version> </dependency> - - <!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpg-jdk15on --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpg-jdk18on</artifactId> - <version>1.77</version> + <version>${bcpg-jdk18on.version}</version> </dependency> <dependency> <groupId>com.github.bastiaanjansen</groupId> <artifactId>otp-java</artifactId> - <version>2.0.3</version> + <version>${otp-java.version}</version> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>${junit-jupiter-engine.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-inline</artifactId> + <version>${mockito-inline.version}</version> + <scope>test</scope> </dependency> </dependencies> - <properties> - <maven.compiler.source>21</maven.compiler.source> - <maven.compiler.target>21</maven.compiler.target> - </properties> - <build> <plugins> <plugin> @@ -68,6 +85,15 @@ </archive> </configuration> </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>${maven-surefire-plugin.version}</version> + <configuration> +<!-- Should remove when no longer required--> + <argLine>-Dnet.bytebuddy.experimental=true</argLine> + </configuration> + </plugin> </plugins> </build> diff --git a/src/main/java/com/wimdupont/Main.java b/src/main/java/com/wimdupont/Main.java @@ -1,20 +1,28 @@ package com.wimdupont; import com.bastiaanjansen.otp.TOTPGenerator; -import com.wimdupont.service.GpgUtil; +import com.wimdupont.service.GpgService; import com.wimdupont.service.PropertiesLoader; -import org.bouncycastle.openpgp.PGPException; - -import java.io.IOException; public class Main { - public static void main(String[] args) throws PGPException, IOException { - String fileArgument = args.length > 0 + public static void main(String[] args) { + try { + var gpgService = new GpgService(new PropertiesLoader().loadProperties()); + System.out.println(TOTPGenerator + .withDefaultValues(gpgService + .decrypt(getFileArg(args))) + .now()); + } catch (Exception e) { + e.printStackTrace(); + } finally { + System.exit(0); + } + } + + private static String getFileArg(String... args) { + return args.length > 0 ? args[0] : null; - GpgUtil gpgUtil = new GpgUtil(new PropertiesLoader().loadProperties()); - System.out.println(TOTPGenerator.withDefaultValues(gpgUtil.decrypt(fileArgument)).now()); - System.exit(0); } } diff --git a/src/main/java/com/wimdupont/service/GpgService.java b/src/main/java/com/wimdupont/service/GpgService.java @@ -0,0 +1,122 @@ +package com.wimdupont.service; + +import org.bouncycastle.crypto.DataLengthException; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; + +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; + + 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 byte[] decrypt(String fileName) throws IOException, PGPException { + Security.addProvider(new BouncyCastleProvider()); + + var encryptedDataObjects = getPgpEncryptedDataIterator(fileName); + + if (encryptedDataObjects.hasNext()) { + var pgpPublicKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedDataObjects.next(); + var pgpPrivateKey = retrievePgpPrivateKey(pgpPublicKeyEncryptedData); + + return getByteArrayOutputStream(pgpPublicKeyEncryptedData, pgpPrivateKey); + } else { + throw new DataLengthException(String.format("No encrypted dataObjects found in file: %s", fileName)); + } + } + + private Iterator<PGPEncryptedData> getPgpEncryptedDataIterator(String fileName) throws IOException { + var secretDecoderStream = PGPUtil.getDecoderStream(KeyRetriever.getSecretKeyInputStream(fileName, dirPath)); + var pgpObjectFactory = new PGPObjectFactory(secretDecoderStream, new BcKeyFingerprintCalculator()); + var nextObject = pgpObjectFactory.nextObject(); + + return (nextObject instanceof PGPEncryptedDataList) + ? ((PGPEncryptedDataList) nextObject).getEncryptedDataObjects() + : ((PGPEncryptedDataList) pgpObjectFactory.nextObject()).getEncryptedDataObjects(); + } + + private PGPPrivateKey retrievePgpPrivateKey(PGPPublicKeyEncryptedData pgpPublicKeyEncryptedData) throws IOException, PGPException { + var pgpPrivateKey = readSecretKey(new FileInputStream(secretFile), pgpPublicKeyEncryptedData.getKeyID()) + .extractPrivateKey(new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()) + .build(PasswordReader.getPassword())); + + if (pgpPrivateKey == null) { + throw new IllegalArgumentException("Secret key for message not found."); + } + + return pgpPrivateKey; + } + + private PGPSecretKey readSecretKey(InputStream inputStream, + long keyId) throws IOException, PGPException { + inputStream = PGPUtil.getDecoderStream(inputStream); + var pgpSecretKeyRings = new PGPSecretKeyRingCollection(inputStream, new BcKeyFingerprintCalculator()); + var secretKey = pgpSecretKeyRings.getSecretKey(keyId); + + if (secretKey == null) { + throw new IllegalArgumentException("Can't find encryption key in key ring."); + } + + return secretKey; + } + + private byte[] getByteArrayOutputStream(PGPPublicKeyEncryptedData pgpPublicKeyEncryptedData, + PGPPrivateKey pgpPrivateKey) throws IOException, PGPException { + ByteArrayOutputStream byteArrayOutputStream; + + try (var dataStream = toDataStream(pgpPublicKeyEncryptedData, pgpPrivateKey)) { + byteArrayOutputStream = new ByteArrayOutputStream(); + int dataRead; + + while ((dataRead = dataStream.read()) >= 0) { + byteArrayOutputStream.write(dataRead); + } + } + + return byteArrayOutputStream.toByteArray(); + } + + private InputStream toDataStream(PGPPublicKeyEncryptedData pgpPublicKeyEncryptedData, + PGPPrivateKey pgpPrivateKey) throws PGPException, IOException { + var pgpDataStream = pgpPublicKeyEncryptedData.getDataStream( + new BcPublicKeyDataDecryptorFactory(pgpPrivateKey)); + var jcaPGPObjectFactory = new JcaPGPObjectFactory(pgpDataStream); + var pgpCompressedDataStream = ((PGPCompressedData) jcaPGPObjectFactory.nextObject()).getDataStream(); + jcaPGPObjectFactory = new JcaPGPObjectFactory(pgpCompressedDataStream); + + return ((PGPLiteralData) jcaPGPObjectFactory.nextObject()).getDataStream(); + } + +} diff --git a/src/main/java/com/wimdupont/service/GpgUtil.java b/src/main/java/com/wimdupont/service/GpgUtil.java @@ -1,117 +0,0 @@ -package com.wimdupont.service; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; -import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; -import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.security.Security; -import java.util.Arrays; -import java.util.Iterator; -import java.util.Properties; -import java.util.Scanner; -import java.util.stream.Collectors; - -public class GpgUtil { - - private final String dirPath; - private final String secretFile; - private static final String GPG_EXTENSION = ".gpg"; - - public GpgUtil(Properties properties) { - dirPath = properties.getProperty("dir.path"); - secretFile = properties.getProperty("secret.file"); - if (dirPath == null | secretFile == null) - throw new RuntimeException("Properties missing."); - } - - public byte[] decrypt(String fileArgument) throws IOException, PGPException { - final Scanner scanner = new Scanner(System.in); - File file; - if (fileArgument != null) { - file = new File(dirPath + fileArgument + GPG_EXTENSION).getAbsoluteFile(); - if (!file.exists()) throw new RuntimeException(String.format("File \"%s\" not found", file.getName())); - } else { - File[] files = new File(dirPath).listFiles(); - if (files == null || files.length < 1) throw new RuntimeException(dirPath + " contains no files"); - System.out.println("Which TOTP?"); - String fileNames = Arrays.stream(files) - .filter(f -> f.getName().contains(GPG_EXTENSION)) - .map(f -> f.getName().replace(GPG_EXTENSION, "")) - .collect(Collectors.joining(", ")); - System.out.println(fileNames); - String fileName = scanner.nextLine(); - file = Arrays.stream(files).filter(f -> f.getName().replace(GPG_EXTENSION, "").equals(fileName)).findAny() - .orElseThrow(() -> new RuntimeException("File not found")); - } - - return decryptFile(new FileInputStream(file.getAbsolutePath()), PasswordReader.getPassword()); - } - - private PGPSecretKey readSecretKeyFromCol(InputStream in, long keyId) throws IOException, PGPException { - in = PGPUtil.getDecoderStream(in); - PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(in, new BcKeyFingerprintCalculator()); - PGPSecretKey key = pgpSec.getSecretKey(keyId); - if (key == null) { - throw new IllegalArgumentException("Can't find encryption key in key ring."); - } - return key; - } - - private byte[] decryptFile(InputStream in, char[] pass) throws IOException, PGPException { - Security.addProvider(new BouncyCastleProvider()); - PGPSecretKey secKey; - in = PGPUtil.getDecoderStream(in); - JcaPGPObjectFactory pgpFact; - - PGPObjectFactory pgpF = new PGPObjectFactory(in, new BcKeyFingerprintCalculator()); - Object o = pgpF.nextObject(); - PGPEncryptedDataList encList; - if (o instanceof PGPEncryptedDataList) { - encList = (PGPEncryptedDataList) o; - } else { - encList = (PGPEncryptedDataList) pgpF.nextObject(); - } - Iterator<PGPEncryptedData> itt = encList.getEncryptedDataObjects(); - PGPPrivateKey sKey = null; - PGPPublicKeyEncryptedData encP = null; - while (sKey == null && itt.hasNext()) { - encP = (PGPPublicKeyEncryptedData) itt.next(); - secKey = readSecretKeyFromCol(new FileInputStream(secretFile), encP.getKeyID()); - sKey = secKey.extractPrivateKey(new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()).build(pass)); - } - if (sKey == null) { - throw new IllegalArgumentException("Secret key for message not found."); - } - InputStream clear = encP.getDataStream(new BcPublicKeyDataDecryptorFactory(sKey)); - pgpFact = new JcaPGPObjectFactory(clear); - PGPCompressedData c1 = (PGPCompressedData) pgpFact.nextObject(); - pgpFact = new JcaPGPObjectFactory(c1.getDataStream()); - PGPLiteralData ld = (PGPLiteralData) pgpFact.nextObject(); - ByteArrayOutputStream bOut = new ByteArrayOutputStream(); - InputStream inLd = ld.getDataStream(); - int ch; - while ((ch = inLd.read()) >= 0) { - bOut.write(ch); - } - return bOut.toByteArray(); - } -} diff --git a/src/main/java/com/wimdupont/service/KeyRetriever.java b/src/main/java/com/wimdupont/service/KeyRetriever.java @@ -0,0 +1,49 @@ +package com.wimdupont.service; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Scanner; +import java.util.stream.Collectors; + +public class KeyRetriever { + + private static final String GPG_EXTENSION = ".gpg"; + + public static InputStream getSecretKeyInputStream(String fileArgument, + String dirPath) throws IOException { + var scanner = new Scanner(System.in); + File file; + + if (fileArgument != null) { + file = new File(dirPath + fileArgument + GPG_EXTENSION).getAbsoluteFile(); + if (!file.exists()) { + throw new FileNotFoundException(String.format("File \"%s\" not found", file.getName())); + } + } else { + var files = new File(dirPath).listFiles(); + if (files == null || files.length < 1) { + throw new FileNotFoundException(dirPath + " contains no files"); + } + String fileNames = Arrays.stream(files) + .filter(f -> f.getName().contains(GPG_EXTENSION)) + .map(f -> f.getName().replace(GPG_EXTENSION, "")) + .collect(Collectors.joining(", ")); + + System.out.println("Which TOTP?"); + System.out.println(fileNames); + + String fileName = scanner.nextLine(); + + file = Arrays.stream(files) + .filter(f -> f.getName().replace(GPG_EXTENSION, "").equals(fileName)) + .findAny() + .orElseThrow(() -> new FileNotFoundException("File not found")); + } + + return new FileInputStream(file.getAbsolutePath()); + } +} diff --git a/src/main/java/com/wimdupont/service/PasswordReader.java b/src/main/java/com/wimdupont/service/PasswordReader.java @@ -22,17 +22,21 @@ public class PasswordReader { } private static char[] getPasswordByPanel(String prompt) { - JPanel panel = new JPanel(); - final JPasswordField passwordField = new JPasswordField(10); + var panel = new JPanel(); + var passwordField = new JPasswordField(10); panel.add(new JLabel("Password")); panel.add(passwordField); - JOptionPane pane = new JOptionPane(panel, JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION) { + var pane = new JOptionPane(panel, JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION) { @Override public void selectInitialValue() { passwordField.requestFocusInWindow(); } }; - pane.createDialog(null, prompt).setVisible(true); - return passwordField.getPassword().length == 0 ? new char[0] : passwordField.getPassword(); + pane.createDialog(null, prompt) + .setVisible(true); + + return passwordField.getPassword().length == 0 + ? new char[0] + : passwordField.getPassword(); } } diff --git a/src/main/java/com/wimdupont/service/PropertiesLoader.java b/src/main/java/com/wimdupont/service/PropertiesLoader.java @@ -1,22 +1,24 @@ package com.wimdupont.service; import java.io.IOException; -import java.io.InputStream; import java.util.MissingResourceException; import java.util.Properties; public class PropertiesLoader { public Properties loadProperties() { - final Properties properties = new Properties(); - InputStream inputStream = getClass().getResourceAsStream("/application.properties"); + final var properties = new Properties(); + var inputStream = getClass().getResourceAsStream("/application.properties"); try { if (inputStream == null) { throw new IOException(); } properties.load(inputStream); } catch (IOException e) { - throw new MissingResourceException("Missing application properties", Properties.class.getSimpleName(), "application.properties"); + throw new MissingResourceException( + "Missing application properties", + Properties.class.getSimpleName(), + "application.properties"); } return properties; } diff --git a/src/test/java/com/wimdupont/service/GpgServiceTest.java b/src/test/java/com/wimdupont/service/GpgServiceTest.java @@ -0,0 +1,75 @@ +package com.wimdupont.service; + + +import org.bouncycastle.openpgp.PGPException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +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; + +public class GpgServiceTest { + + private static final char[] PASSWORD = {'s', 'e', 'c', 'r', 'e', 't', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'}; + private GpgService gpgService; + + @BeforeEach + public void setup() { + 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)); + + gpgService = new GpgService(properties); + } + + @Test + public void decryptWithCorrectPasswordShouldReturnCorrectSecret() throws PGPException, IOException { + try (MockedStatic<PasswordReader> mocked = mockStatic(PasswordReader.class)) { + mocked.when(PasswordReader::getPassword).thenReturn(PASSWORD); + + var result = gpgService.decrypt("encrypted-totp-secret"); + + Assertions.assertNotNull(result); + 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'}); + + Exception exception = Assertions.assertThrows(PGPException.class, () -> + gpgService.decrypt("encrypted-totp-secret") + ); + + Assertions.assertEquals(exception.getClass(), PGPException.class); + } + } + + @Test + public void decryptUnknownFileShouldThrowException() { + try (MockedStatic<PasswordReader> mocked = mockStatic(PasswordReader.class)) { + mocked.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"); + } + } +} +\ No newline at end of file diff --git a/src/test/resources/private-key.asc b/src/test/resources/private-key.asc @@ -0,0 +1,16 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lIYEZYasRRYJKwYBBAHaRw8BAQdATREN8PaSPOGDlP6ZjBM5Ws/U8qu1KelaLx6N +EvEbU0z+BwMC6g7GBQQy+/X7KrmNSsP9wkSdBBbhgQx8C7TJdNtiRs8P7dX7p/CE +ifpr5ivke7/Qjalo53GljWwYQCWelxL8hawYsOoXplv8XKWZ/l54sLQTVE9UUCBH +ZW5lcmF0b3IgVGVzdIiTBBMWCgA7FiEE7p8C1GMJI6EaviAjtpe53CJEoPMFAmWG +rEUCGwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQtpe53CJEoPM3JgD8 +CsBls6FUiJiqsjstgdQ/QdANpuiRFoK3FJlmbgY6UzoA/0sBnCmazJHi/sjMjgu3 +xQugXhLHkdcvN3C24aeEs/gFnIsEZYasRRIKKwYBBAGXVQEFAQEHQCLZz+dcM5tC +WQenmrF/M7dpyVDP6xHIFniFwGkTgkAYAwEIB/4HAwImuPNsGIOTIPsaEzShrd87 +IPh/seYzcgB/pL3vE6510CpsErmkanbvgGr+sLV5jUOl/BA2KK8zQaMdoZUttdDj +vMFVEeeC1ddc3KwOjnn/iHgEGBYKACAWIQTunwLUYwkjoRq+ICO2l7ncIkSg8wUC +ZYasRQIbDAAKCRC2l7ncIkSg8zUSAP0euvfg3iXWD9trkcVaUtrZ6FBpfslmOKnc +v2JxzXlGZwEAgdm+ugRYRbS1iosIludyaSnUf+jlZtDOLGpSoF5nLQ8= +=+tXU +-----END PGP PRIVATE KEY BLOCK----- diff --git a/src/test/resources/totpfiles/encrypted-totp-secret.gpg b/src/test/resources/totpfiles/encrypted-totp-secret.gpg Binary files differ.