totpgenerator

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

commit 8e4a2b1769308bf927d3057e885c9a91727d0468
Author: WimDupont <WimDupont@users.noreply.gitlab.com>
Date:   Mon, 27 Dec 2021 17:05:24 +0100

init

Diffstat:
A.gitignore | 35+++++++++++++++++++++++++++++++++++
ALICENSE | 21+++++++++++++++++++++
AREADME.md | 17+++++++++++++++++
Apom.xml | 30++++++++++++++++++++++++++++++
Asrc/main/java/Main.java | 9+++++++++
Asrc/main/java/service/DefaultTotpGenerator.java | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/service/GpgUtil.java | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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(); + } +}