diff --git a/README.md b/README.md index dba5e4f..94eece2 100644 --- a/README.md +++ b/README.md @@ -1,13 +1 @@ -Simple CLI app that generates tokens compatible with Google Authenticator. I implemented this mainly to understand how it works, you should probably not use this. - -Example output: - -```sh -$ go run main.go "" -934523 (17 second(s) remaining) -``` - -Relevant RFCs: - -* http://tools.ietf.org/html/rfc4226 -* http://tools.ietf.org/html/rfc6238 +Simple CLI apps that generate tokens compatible with Google Authenticator. diff --git a/LICENSE b/golang/LICENSE similarity index 100% rename from LICENSE rename to golang/LICENSE diff --git a/golang/README.md b/golang/README.md new file mode 100644 index 0000000..dba5e4f --- /dev/null +++ b/golang/README.md @@ -0,0 +1,13 @@ +Simple CLI app that generates tokens compatible with Google Authenticator. I implemented this mainly to understand how it works, you should probably not use this. + +Example output: + +```sh +$ go run main.go "" +934523 (17 second(s) remaining) +``` + +Relevant RFCs: + +* http://tools.ietf.org/html/rfc4226 +* http://tools.ietf.org/html/rfc6238 diff --git a/main.go b/golang/main.go similarity index 100% rename from main.go rename to golang/main.go diff --git a/java/.gitignore b/java/.gitignore new file mode 100644 index 0000000..143cac6 --- /dev/null +++ b/java/.gitignore @@ -0,0 +1,9 @@ +.gradle/ +gradlew +gradlew.bat +bin/ +build/ +gradle/ +.project +.classpath +.settings/ diff --git a/java/README.md b/java/README.md new file mode 100644 index 0000000..7406f43 --- /dev/null +++ b/java/README.md @@ -0,0 +1,9 @@ + +# Java TOTP + +```bash +foo@bar:~$ gradle wrapper +foo@bar:~$ ./gradlew clean +foo@bar:~$ ./gradlew build +foo@bar:~$ ./gradlew --console plain run +``` diff --git a/java/build.gradle b/java/build.gradle new file mode 100644 index 0000000..0dcbfad --- /dev/null +++ b/java/build.gradle @@ -0,0 +1,32 @@ +apply plugin:'application' +mainClassName = "common.TwoFactorAuth" + +buildscript { + repositories { + mavenCentral() + //jcenter() + jcenter { + url "http://jcenter.bintray.com/" + } + } +} + +repositories { + //jcenter() + jcenter { + url "http://jcenter.bintray.com/" + } +} + +apply plugin: 'java' + +dependencies { + compile group: 'com.google.guava', name: 'guava', version: '25.1-jre' + compile group: 'commons-codec', name: 'commons-codec', version: '1.12' + +} + +run{ + standardInput = System.in +} + diff --git a/java/src/main/java/common/TwoFactorAuth.java b/java/src/main/java/common/TwoFactorAuth.java new file mode 100644 index 0000000..e397b62 --- /dev/null +++ b/java/src/main/java/common/TwoFactorAuth.java @@ -0,0 +1,80 @@ +package common; + +import com.google.common.primitives.UnsignedBytes; +import org.apache.commons.codec.binary.Base32; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Scanner; + +/** + * Pseudocode for one-time password (OTP) + *
+ * function GoogleAuthenticatorCode(string secret)
+ *       key := 5B5E7MMX344QRHYO
+ *       message := floor(current Unix time / 30)
+ *       hash := HMAC-SHA1(key, message)
+ *       offset := last nibble of hash
+ *       truncatedHash := hash[offset..offset+3]  //4 bytes starting at the offset
+ *       Set the first bit of truncatedHash to zero  //remove the most significant bit
+ *       code := truncatedHash mod 1000000
+ *       pad code with 0 from the left until length of code is 6
+ *       return code
+ * 
+ * + * @see Wiki + */ +public class TwoFactorAuth { + private static final String HMAC_SHA1 = "HmacSHA1"; + private static final short[] SHIFTS = {56, 48, 40, 32, 24, 16, 8, 0}; + + private static byte[] toBytes(long value) { + byte[] result = new byte[8]; + for (int i = 0; i < SHIFTS.length; i++) { + result[i] = (byte) ((value >> SHIFTS[i]) & 0xFF); + } + return result; + } + + private static int toUint32(byte[] bytes) { + return (UnsignedBytes.toInt(bytes[0]) << 24) + + (UnsignedBytes.toInt(bytes[1]) << 16) + + (UnsignedBytes.toInt(bytes[2]) << 8) + + (UnsignedBytes.toInt(bytes[3])); + } + + private static int oneTimePassword(byte[] key, byte[] value) throws InvalidKeyException, NoSuchAlgorithmException { + Mac mac = Mac.getInstance(HMAC_SHA1); + mac.init(new SecretKeySpec(key, HMAC_SHA1)); + mac.update(value); + byte[] hash = mac.doFinal(); + + int offset = hash[hash.length - 1] & 0x0F; + + byte[] truncatedHash = Arrays.copyOfRange(hash, offset, offset + 4); + + truncatedHash[0] = (byte) (truncatedHash[0] & 0x7F); + + long number = toUint32(truncatedHash); + + return (int) (number % 1000000); + } + + public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException { + Scanner scanner = new Scanner(System.in); + System.out.print("Input key:"); + String input = scanner.nextLine(); + scanner.close(); + + byte[] key = new Base32().decode(input); + long epochSeconds = System.currentTimeMillis() / 1000; + int pwd = oneTimePassword(key, toBytes(epochSeconds / 30)); + long secondsRemaining = 30 - (epochSeconds % 30); + + System.out.println(String.format("%06d (%d second(s) remaining)", pwd, secondsRemaining)); + } +} + diff --git a/python3/README.md b/python3/README.md new file mode 100644 index 0000000..1c2f547 --- /dev/null +++ b/python3/README.md @@ -0,0 +1,20 @@ +# TOTP Authenticator in Python + +* https://medium.freecodecamp.org/how-time-based-one-time-passwords-work-and-why-you-should-use-them-in-your-app-fdd2b9ed43c3 +* https://tools.ietf.org/html/rfc4226 +* https://tools.ietf.org/html/rfc6238 +* https://github.com/google/google-authenticator/wiki/Key-Uri-Format +* https://security.stackexchange.com/questions/35157/how-does-google-authenticator-work +* https://github.com/robbiev/two-factor-auth/blob/master/main.go +* https://stefansundin.github.io/2fa-qr/ + + +```bash +foo@bar:~$ virtualenv sandbox +foo@bar:~$ virtualenv -p $(which python3) sandbox +foo@bar:~$ source sandbox/bin/activate +foo@bar:~$ pip3 install --upgrade pip +foo@bar:~$ pip3 install -r requirements.txt +foo@bar:~$ python ./pass.py qr.png +foo@bar:~$ deactivate +``` diff --git a/python3/pass.py b/python3/pass.py new file mode 100644 index 0000000..b06797e --- /dev/null +++ b/python3/pass.py @@ -0,0 +1,58 @@ +#!/bin/python3 +import fastzbarlight +from PIL import Image +import time +import hmac +import hashlib +import urllib.parse as urlparse +import base64 +import sys + +if len(sys.argv) < 2: + print("Usage: python3 " + sys.argv[0] + " qr-code.png") + sys.exit(-1) + +qr_code = fastzbarlight.scan_codes('qrcode', Image.open(sys.argv[1])) +qr_code = str(qr_code[0].decode()) +print("QR code:", qr_code) + +secret = None +digits = 6 +period = 30 +algo = hashlib.sha1 +parsed = urlparse.urlparse(qr_code) +qs = urlparse.parse_qs(parsed.query) +for k,v in qs.items(): + if k == "secret": + secret = v[0] + if k == "digits": + digits = int(v[0]) + if k == "period": + period = int(v[0]) + if k == "algorithm": + algo = getattr(hashlib, v[0].lower()) +print("secret:", secret) +print("OTP length:", digits) +print("OTP lifetime:", period) + +secret = base64.b32decode(secret) + +currentUnixTime = int(time.time()) +print("Unix time:", currentUnixTime) + +counter = currentUnixTime // period +print("Counter:", counter) +counter = counter.to_bytes(8, byteorder = 'big') + +hash = hmac.new(secret, counter, algo) +digest = hash.digest() +print("HMAC Digest", hash.hexdigest()) + +offset = digest[19] & 0xf # last nibble operations +print("offset:", offset) +truncatedHash = (digest[offset] & 0x7f) << 24 | (digest[offset+1] & 0xff) << 16 | (digest[offset+2] & 0xff) << 8 | (digest[offset+3] & 0xff) +print("truncatedHash:", hex(truncatedHash)) +finalOTP = (truncatedHash % (10 ** digits)) +print("finalOTP:", finalOTP) + + diff --git a/python3/qr.png b/python3/qr.png new file mode 100644 index 0000000..e785852 Binary files /dev/null and b/python3/qr.png differ diff --git a/python3/requirements.txt b/python3/requirements.txt new file mode 100644 index 0000000..65c1f77 --- /dev/null +++ b/python3/requirements.txt @@ -0,0 +1,2 @@ +fastzbarlight==0.0.14 +urlparse2==1.1.1