From ca86013d378eceb710cabdd1f0fad4f1cf593812 Mon Sep 17 00:00:00 2001 From: Hugh Pearse Date: Fri, 3 May 2019 10:26:11 +0100 Subject: [PATCH 1/5] added python3 implementation --- README.md | 14 +------------ LICENSE => golang/LICENSE | 0 golang/README.md | 13 ++++++++++++ main.go => golang/main.go | 0 python3/README.md | 19 +++++++++++++++++ python3/pass.py | 42 ++++++++++++++++++++++++++++++++++++++ python3/qr.png | Bin 0 -> 1500 bytes python3/requirements.txt | 2 ++ 8 files changed, 77 insertions(+), 13 deletions(-) rename LICENSE => golang/LICENSE (100%) create mode 100644 golang/README.md rename main.go => golang/main.go (100%) create mode 100644 python3/README.md create mode 100644 python3/pass.py create mode 100644 python3/qr.png create mode 100644 python3/requirements.txt 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/python3/README.md b/python3/README.md new file mode 100644 index 0000000..389a983 --- /dev/null +++ b/python3/README.md @@ -0,0 +1,19 @@ +# TOTP Authenticator in Python + +* 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 +foo@bar:~$ deactivate +``` diff --git a/python3/pass.py b/python3/pass.py new file mode 100644 index 0000000..18e32cf --- /dev/null +++ b/python3/pass.py @@ -0,0 +1,42 @@ +#!/bin/python3 +import fastzbarlight +from PIL import Image +import time +import hmac +import hashlib +import urllib.parse as urlparse +import base64 + +numberOfDigitsRequiredInOTP = 6 +lifetimeOfOTPInSeconds = 30 +print("OTP length:", numberOfDigitsRequiredInOTP) +print("OTP lifetime:", lifetimeOfOTPInSeconds) + +qr_code = fastzbarlight.scan_codes('qrcode', Image.open("./qr.png")) +qr_code = str(qr_code[0].decode()) +print("QR code:", qr_code) + +parsed = urlparse.urlparse(qr_code) +secret = urlparse.parse_qs(parsed.query)["secret"][0] +print("secret:", secret) +secret = base64.b32decode(secret) + +currentUnixTime = int(time.time()) +print("Unix time:", currentUnixTime) + +counter = currentUnixTime // lifetimeOfOTPInSeconds +print("Counter:", counter) +counter = counter.to_bytes(8, byteorder = 'big') + +hash = hmac.new(secret, counter, hashlib.sha1) +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 ** numberOfDigitsRequiredInOTP)) +print("finalOTP:", finalOTP) + + diff --git a/python3/qr.png b/python3/qr.png new file mode 100644 index 0000000000000000000000000000000000000000..7aeb434dd5d4258d4059f77bf7220ac5c1d39764 GIT binary patch literal 1500 zcmah}eNfV89OvAo=9R{}x<+azJKe6AYr459gcga0b?W(sX;ML*4OW_pSg5t8m)%lx zj%-UvI|(8E5t^x>Xxe4EfJy?UDQ(vxQ2C|u5)d*%pnrC^d+vF@chAfBzTeO1^L#F* zBqglzgZV)qkTux7ajA~@@cs1la;$Jf^q3=f6ZR#6N*|ydy7gPkl(}n=FONWNQ4?sM7d6Rvo=v`BfHr z@9hP!YXZ;18KXrakzk-*k=M%);)weiJcXE>v7FD8 z#nWNOB1R`VwhJO3YpELf`G)Ah@h)*R!BtWvc^jHFGkIXoTJX!QChpFW@+kMw&gKW% z8e`rIMS+`x^4vuF7((YJun2D^e~o)5ij( zN0&7xV|h_X@SkcI7z={D?~+Ycp)NA5+*DkrDDqTOmPLf3_B3?2r%K~_NV@i;oEF7! zThVtwiG-K5X&*iTBc1QCqotj}FU z#5Hdw=bbf4KutBZ_SP&(HF!iQOr{HUg>tqyqWuAnhE;gJnY5Hon|XY@M6=;keS#_$$X;Wdvk6} z@Q@R%sM{qYegh&1-}}A<&(|4}#n){GZjRm-1d|s<8kyZTeTIenBJNR5heOI0hIiAn z52`yyL-T46U=2(hvj&fYSG7rvvR)6^FYpNcWO~EwK`8172X7qBMT= zB~3+yMfi{gw0PtU)s4bQ&FQi0__fEm*EyYc_3;{(yJniEwOGxn))quai|H0?t8q#5 zyZyumZFFziA^`iVMWC)+ILekoLq+v>)VTpfr``OZ9Tg;Y;AT3Ao#1dqY~x#9VW+7G zW1QOK!K|PLqwGt%*P2&G;mIui`WU%GAwDY(7Mw!N(9&hrL_+>&XY8-K$ltVkydf}a z^DSBsA!rE+#tsM+N0Z3*#nt+}EvoX7ek)^h6d6x6-UqUqMoYM+VO&Iw4 iV$a?GH^~+6d>4RR9BN{l0yjGB8iK_n#R2HU=l=z6pX5#e literal 0 HcmV?d00001 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 From 7ac403c796b3bd4206348dcd1d5ea703e01b61f1 Mon Sep 17 00:00:00 2001 From: Hugh Pearse Date: Fri, 3 May 2019 10:29:41 +0100 Subject: [PATCH 2/5] added blogpost --- python3/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/python3/README.md b/python3/README.md index 389a983..8c9afd4 100644 --- a/python3/README.md +++ b/python3/README.md @@ -1,5 +1,6 @@ # 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 From c1a27bdcf2a13476a47c87891fdb57b7c9e7adb2 Mon Sep 17 00:00:00 2001 From: Hugh Pearse Date: Fri, 3 May 2019 11:45:18 +0100 Subject: [PATCH 3/5] added java --- java/.gitignore | 9 +++ java/README.md | 9 +++ java/build.gradle | 32 ++++++++ java/src/main/java/common/TwoFactorAuth.java | 80 ++++++++++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 java/.gitignore create mode 100644 java/README.md create mode 100644 java/build.gradle create mode 100644 java/src/main/java/common/TwoFactorAuth.java 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)); + } +} + From 3d1277495affb509d7fab100197bbaedcaec1f01 Mon Sep 17 00:00:00 2001 From: Hugh Pearse Date: Wed, 8 May 2019 18:22:23 +0100 Subject: [PATCH 4/5] updated code for image param parsing --- python3/pass.py | 34 +++++++++++++++++++++++++--------- python3/qr.png | Bin 1500 -> 10477 bytes 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/python3/pass.py b/python3/pass.py index 18e32cf..b06797e 100644 --- a/python3/pass.py +++ b/python3/pass.py @@ -6,29 +6,45 @@ import hashlib import urllib.parse as urlparse import base64 +import sys -numberOfDigitsRequiredInOTP = 6 -lifetimeOfOTPInSeconds = 30 -print("OTP length:", numberOfDigitsRequiredInOTP) -print("OTP lifetime:", lifetimeOfOTPInSeconds) +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("./qr.png")) +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) -secret = urlparse.parse_qs(parsed.query)["secret"][0] +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 // lifetimeOfOTPInSeconds +counter = currentUnixTime // period print("Counter:", counter) counter = counter.to_bytes(8, byteorder = 'big') -hash = hmac.new(secret, counter, hashlib.sha1) +hash = hmac.new(secret, counter, algo) digest = hash.digest() print("HMAC Digest", hash.hexdigest()) @@ -36,7 +52,7 @@ 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 ** numberOfDigitsRequiredInOTP)) +finalOTP = (truncatedHash % (10 ** digits)) print("finalOTP:", finalOTP) diff --git a/python3/qr.png b/python3/qr.png index 7aeb434dd5d4258d4059f77bf7220ac5c1d39764..e7858526eccf4093a86bceb088ab20c1b753333e 100644 GIT binary patch literal 10477 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 literal 1500 zcmah}eNfV89OvAo=9R{}x<+azJKe6AYr459gcga0b?W(sX;ML*4OW_pSg5t8m)%lx zj%-UvI|(8E5t^x>Xxe4EfJy?UDQ(vxQ2C|u5)d*%pnrC^d+vF@chAfBzTeO1^L#F* zBqglzgZV)qkTux7ajA~@@cs1la;$Jf^q3=f6ZR#6N*|ydy7gPkl(}n=FONWNQ4?sM7d6Rvo=v`BfHr z@9hP!YXZ;18KXrakzk-*k=M%);)weiJcXE>v7FD8 z#nWNOB1R`VwhJO3YpELf`G)Ah@h)*R!BtWvc^jHFGkIXoTJX!QChpFW@+kMw&gKW% z8e`rIMS+`x^4vuF7((YJun2D^e~o)5ij( zN0&7xV|h_X@SkcI7z={D?~+Ycp)NA5+*DkrDDqTOmPLf3_B3?2r%K~_NV@i;oEF7! zThVtwiG-K5X&*iTBc1QCqotj}FU z#5Hdw=bbf4KutBZ_SP&(HF!iQOr{HUg>tqyqWuAnhE;gJnY5Hon|XY@M6=;keS#_$$X;Wdvk6} z@Q@R%sM{qYegh&1-}}A<&(|4}#n){GZjRm-1d|s<8kyZTeTIenBJNR5heOI0hIiAn z52`yyL-T46U=2(hvj&fYSG7rvvR)6^FYpNcWO~EwK`8172X7qBMT= zB~3+yMfi{gw0PtU)s4bQ&FQi0__fEm*EyYc_3;{(yJniEwOGxn))quai|H0?t8q#5 zyZyumZFFziA^`iVMWC)+ILekoLq+v>)VTpfr``OZ9Tg;Y;AT3Ao#1dqY~x#9VW+7G zW1QOK!K|PLqwGt%*P2&G;mIui`WU%GAwDY(7Mw!N(9&hrL_+>&XY8-K$ltVkydf}a z^DSBsA!rE+#tsM+N0Z3*#nt+}EvoX7ek)^h6d6x6-UqUqMoYM+VO&Iw4 iV$a?GH^~+6d>4RR9BN{l0yjGB8iK_n#R2HU=l=z6pX5#e From 9b64fb86a3645593c4093f15a62d46a006c8efb1 Mon Sep 17 00:00:00 2001 From: Hugh Pearse Date: Mon, 13 May 2019 13:00:52 +0100 Subject: [PATCH 5/5] image param --- python3/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python3/README.md b/python3/README.md index 8c9afd4..1c2f547 100644 --- a/python3/README.md +++ b/python3/README.md @@ -15,6 +15,6 @@ 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 +foo@bar:~$ python ./pass.py qr.png foo@bar:~$ deactivate ```