commit 8e4a2b1769308bf927d3057e885c9a91727d0468
Author: WimDupont <WimDupont@users.noreply.gitlab.com>
Date:   Mon, 27 Dec 2021 17:05:24 +0100
init
Diffstat:
7 files changed, 314 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,35 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+/src/main/resources/application.properties
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Bastiaan Jansen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
@@ -0,0 +1,17 @@
+# Totp Generator
+Program to generate TOTP verification codes
+
+## GPG commands for setup
+If you're unfamiliar with GPG then I recommend to look for online guides. However, here are the commands you'll need to run the program: 
+
+1. create new keypair:  
+$ gpg --full-gen-key
+2. encrypt file:  
+$ gpg -e filename
+3. export private key:  
+$ gpg --export-secret-keys --armor keyIDNumber/email > secret.asc
+
+## How to use
+1. Create an application.properties file with following properties: 
+   1. dir.path: path to directory with .gpg extension files which contain the TOTP secret code
+   2. secret.file: gpg exported asc file to decrypt the TOTP files
diff --git a/pom.xml b/pom.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.dupont</groupId>
+    <artifactId>TotpGenerator</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <dependencies>
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+            <version>1.15</version>
+        </dependency>
+
+        <!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpg-jdk15on -->
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcpg-jdk15on</artifactId>
+            <version>1.70</version>
+        </dependency>
+    </dependencies>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+    </properties>
+
+</project>
diff --git a/src/main/java/Main.java b/src/main/java/Main.java
@@ -0,0 +1,9 @@
+import service.DefaultTotpGenerator;
+import service.GpgUtil;
+
+public class Main {
+    public static void main(String[] args) {
+        System.out.println(new DefaultTotpGenerator().generate(new GpgUtil().decrypt().orElseThrow()));
+        System.exit(0);
+    }
+}
diff --git a/src/main/java/service/DefaultTotpGenerator.java b/src/main/java/service/DefaultTotpGenerator.java
@@ -0,0 +1,71 @@
+package service;
+
+import org.apache.commons.codec.binary.Base32;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.time.Duration;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+/*
+Extracts from https://github.com/BastiaanJansen/OTP-Java
+go to that repository for further documentation and/or code expansion
+ */
+public class DefaultTotpGenerator {
+    public String generate(String secret) {
+        long secondsSince1970 = TimeUnit.MILLISECONDS.toSeconds(new Date().getTime());
+        final Duration period = Duration.ofSeconds(30);
+        long counter = TimeUnit.SECONDS.toMillis(secondsSince1970) / period.toMillis();
+
+        if (counter < 0)
+            throw new IllegalArgumentException("Counter must be greater than or equal to 0");
+
+        byte[] secretBytes = decodeBase32(secret.getBytes());
+        byte[] counterBytes = longToBytes(counter);
+
+        byte[] hash;
+
+        try {
+            hash = generateHash(secretBytes, counterBytes);
+        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+            throw new IllegalStateException();
+        }
+
+        return getCodeFromHash(hash);
+    }
+
+    private byte[] decodeBase32(final byte[] value) {
+        Base32 codec = new Base32();
+        return codec.decode(value);
+    }
+
+    private byte[] longToBytes(final long value) {
+        return ByteBuffer.allocate(Long.BYTES).putLong(value).array();
+    }
+
+    private byte[] generateHash(final byte[] secret, final byte[] data) throws InvalidKeyException, NoSuchAlgorithmException {
+        // Create a secret key with correct SHA algorithm
+        SecretKeySpec signKey = new SecretKeySpec(secret, "RAW");
+        // Mac is 'message authentication code' algorithm (RFC 2104)
+        Mac mac = Mac.getInstance("HmacSHA1");
+        mac.init(signKey);
+        // Hash data with generated sign key
+        return mac.doFinal(data);
+    }
+
+    private String getCodeFromHash(final byte[] hash) {
+        int mask = ~(~0 << 4);
+        byte lastByte = hash[hash.length - 1];
+        int offset = lastByte & mask;
+        byte[] truncatedHashInBytes = {hash[offset], hash[offset + 1], hash[offset + 2], hash[offset + 3]};
+        ByteBuffer byteBuffer = ByteBuffer.wrap(truncatedHashInBytes);
+        long truncatedHash = byteBuffer.getInt();
+        truncatedHash &= 0x7FFFFFFF;
+        truncatedHash %= Math.pow(10, 6);
+        return String.format("%0" + 6 + "d", truncatedHash);
+    }
+}
diff --git a/src/main/java/service/GpgUtil.java b/src/main/java/service/GpgUtil.java
@@ -0,0 +1,131 @@
+package 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.Optional;
+import java.util.Properties;
+import java.util.Scanner;
+import java.util.stream.Collectors;
+
+public class GpgUtil {
+
+    private static String dirPath;
+    private static String secretFile;
+    private static final String GPG_EXTENSION = ".gpg";
+
+    public GpgUtil() {
+        try {
+            Properties properties = loadProperties();
+            dirPath = properties.getProperty("dir.path");
+            secretFile = properties.getProperty("secret.file");
+            if (dirPath == null | secretFile == null)
+                throw new RuntimeException("Properties missing.");
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public Optional<String> decrypt() {
+        final Scanner scanner = new Scanner(System.in);
+        File[] files = new File(dirPath).listFiles();
+        if (files == null) 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();
+        Optional<File> file = Arrays.stream(files).filter(f -> f.getName().replace(GPG_EXTENSION, "").equals(fileName)).findAny();
+        if (file.isEmpty()) throw new RuntimeException("File not found");
+
+        System.out.println("Type in GPG password.");
+        String pwd = scanner.nextLine();
+        try {
+            return Optional.ofNullable(decryptFile(new FileInputStream(file.get().getAbsolutePath()), pwd.toCharArray()));
+        } catch (IOException | PGPException e) {
+            e.printStackTrace();
+            return Optional.empty();
+        }
+    }
+
+    private Properties loadProperties() throws IOException {
+        final Properties properties = new Properties();
+        InputStream is = getClass().getResourceAsStream("/application.properties");
+        properties.load(is);
+        return properties;
+    }
+
+    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 String 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.toString();
+    }
+}