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();
+ }
+}