Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated CFC to use Apache Commons #4

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
## Updates
I've updated the CFC in a couple of ways

* I've removed the native CF solution in favour of the Apache Commons Codec to implement Base32 encoding/decoding
* Added new function getOTPQRURL() which return the a QR code URL you can put straight in to an image tag

## Intro

This is a ColdFusion native implementation of RFC6238 (TOTP: Time-Based One-Time Password Algorithm) specifically designed to work with the Google Authenticator app. You can use this for providing Two Factor Authentication for your applications.

It has been tested on Adobe Coldfusion 10 because that's what I run locally. It uses a few Java classes and bit twiddling, so YMMV on Railo. It should work on CF9, I don't think I've done anything CF10 specific - but feel free to do a Pull Request if there's a small change required to make this work on CF9 or Railo!
It has been tested on Adobe Coldfusion 10 because that's what I run locally. It uses a few Java classes and bit twiddling, so YMMV on Lucee/Railo. It should work on CF9, I don't think I've done anything CF10 specific - but feel free to do a Pull Request if there's a small change required to make this work on CF9 or Lucee/Railo!

## Background

Expand All @@ -15,7 +23,7 @@ To use the Google Authenticator in your own app you would do something like:

## Implementation notes

This is a purely "native" CF solution - I could've saved some code and time by using Apache Commons Codec to implement Base32 encoding/decoding, however the version bundled with ACF10 is v1.3 and Base32 was added in v1.5 - I didn't want to introduce another dependency. In fact, the whole project might've been better to be implemented as a Java library since it makes so much use of Java arrays, bit twiddling, etc! Still it was a fun coding exercise!
This version uses Apache Commons Codec to implement Base32 encoding/decoding which will make it incompativle with ACF10.

## Notes on security

Expand All @@ -26,11 +34,9 @@ This is a purely "native" CF solution - I could've saved some code and time by u

There's a simple sample in the project where you can generate a secret key and then see the token values for that key (and compare to the Authenticator app). This sample is definitely *not* best practice or recommended to be used for anything other than playing around.

The samples use [qrcode.js](http://davidshimjs.github.io/qrcodejs/).

## Tests

There are some [mxunit](http://mxunit.org/) based tests that can be run from `/tests/index.cfm`. They assume that mxunit is mapped at the server level to /mxunit. If we had Railo CLI I could make them not depend on a web server!
There are some [mxunit](http://mxunit.org/) based tests that can be run from `/tests/index.cfm`. They assume that mxunit is mapped at the server level to /mxunit. If we had Lucee/Railo CLI I could make them not depend on a web server!

## Licence

Expand Down
150 changes: 8 additions & 142 deletions authenticator/GoogleAuthenticator.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ component output="false" {
return 'otpauth://totp/#arguments.email#?secret=#arguments.key#';
}

public string function getOTPQRURL(required string OTPURL){
local.qrURL = "https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl=200x200&chld=M|0&cht=qr&chl=";
Copy link

@displague displague Nov 28, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like a bad idea to send your secret tokens to Google. They seem nice but I assume this grants thousands of Google employees the ability to grep your referral domain from their logs and get a very small list of possible secrets for any TOTP enabled account on your domain.

return local.qrURL & arguments.OTPURL;
}

/**
* The core TOTP function that gets the current value of the token for a particular secret key and numeric counter
*
Expand Down Expand Up @@ -154,97 +159,8 @@ component output="false" {
*/
public string function Base32encode (required any inputBytes)
{
var values = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
if (arrayLen(inputBytes) == 0)
{
return "";
}
var bytes = 0;
if (ArrayLen(inputBytes) % 5 != 0)
{
var paddedLength = ArrayLen(inputBytes) + (5 - (ArrayLen(inputBytes) % 5));
var buffer = createObject("java", "java.nio.ByteBuffer").allocate(paddedLength);
buffer.put(inputBytes, 0, ArrayLen(inputBytes));
bytes = buffer.array();
}
else
{
bytes = inputBytes;
}

var encoded = "";
for (var i = 1; i <= arrayLen(bytes); i += 5)
{
byte = bytes[i];
if (byte < 0) byte += 256;
byte = bitSHRN(byte, 3);
byte = bitAnd(byte, 31);
encoded &= Mid(values, byte + 1, 1);

byte = bytes[i];
if (byte < 0) byte += 256;
byte = bitAnd(byte, 7);
byte = bitSHLN(byte, 2);
byte2 = bytes[i+1];
if (byte2 < 0) byte2 += 256;
byte2 = bitSHRN(byte2, 6);
byte2 = bitAnd(byte2, 3);
byte = bitOr(byte, byte2);
encoded &= Mid(values, byte + 1, 1);

byte = bytes[i+1];
if (byte < 0) byte += 256;
byte = bitAnd(byte, 62);
byte = bitSHRN(byte, 1);
encoded &= Mid(values, byte + 1, 1);

byte = bytes[i+1];
if (byte < 0) byte += 256;
byte = bitAnd(byte, 1);
byte = bitSHLN(byte, 4);
byte2 = bytes[i+2];
if (byte2 < 0) byte2 += 256;
byte2 = bitSHRN(byte2, 4);
byte = bitOr(byte, byte2);
encoded &= Mid(values, byte + 1, 1);

byte = bytes[i+2];
if (byte < 0) byte += 256;
byte = bitAnd(byte, 15);
byte = bitSHLN(byte, 1);
byte2 = bytes[i+3];
if (byte2 < 0) byte2 += 256;
byte2 = bitSHRN(byte2, 7);
byte = bitOr(byte, byte2);
encoded &= Mid(values, byte + 1, 1);

byte = bytes[i+3];
if (byte < 0) byte += 256;
byte = bitSHRN(byte, 2);
byte = bitAnd(byte, 31);
encoded &= Mid(values, byte + 1, 1);
return createObject("java", "org.apache.commons.codec.binary.Base32").encodeToString( arguments.inputBytes );

byte = bytes[i+3];
if (byte < 0) byte += 256;
byte = bitAnd(byte, 3);
byte = bitSHLN(byte, 3);
byte2 = bytes[i+4];
if (byte2 < 0) byte2 += 256;
byte2 = bitSHRN(byte2, 5);
byte = bitOr(byte, byte2);
encoded &= Mid(values, byte + 1, 1);

byte = bytes[i+4];
if (byte < 0) byte += 256;
byte = bitAnd(byte, 31);
encoded &= Mid(values, byte + 1, 1);
}

encoded = Left(encoded, (arrayLen(inputBytes) / 5) * 8 + 1);
if (len(encoded) % 8 != 0) {
encoded &= repeatString("=", 8 - (len(encoded) % 8) );
}
return encoded;
}

/**
Expand All @@ -254,72 +170,22 @@ component output="false" {
{
return base32encode(string.getBytes());
}

/* borrowed from org.apache.commons.codec.binary.Base32 */
this.DECODE_TABLE = [
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 63, // 20-2f
-1, -1, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, -1, -1, -1, // 30-3f 2-7
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 40-4f A-N
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 // 50-5a O-Z
];

/**
* Decodes a Base32 encoded string
* @param encoded the encoded string to decode
* @return a byte[] array of decoded values
*/
public any function base32decode (required string encoded)
{
var decoded = "";
var byte = 0;
var byte2 = 0;
var byte3 = 0;
var encodedBytes = javaCast("string", encoded).getBytes();
var unpaddedLength = Len(replace(encoded, "=", "", "all"));
var decodedBytes = createObject("java", "java.io.ByteArrayOutputStream").init();
for (var i = 1; i <= arrayLen(encodedBytes); i += 8)
{
if (encodedBytes[i + 1] == 61) break;
byte = bitSHLN(this.DECODE_TABLE[encodedBytes[i]], 3);
byte2 = bitSHRN(this.DECODE_TABLE[encodedBytes[i + 1]], 2);
decodedBytes.write(bitOr(byte, byte2));

if (encodedBytes[i + 3] == 61) break;
byte = bitSHLN(bitAnd(this.DECODE_TABLE[encodedBytes[i + 1]], 3), 6);
byte2 = bitSHLN(this.DECODE_TABLE[encodedBytes[i + 2]], 1);
byte3 = bitSHRN(this.DECODE_TABLE[encodedBytes[i + 3]], 4);
decodedBytes.write(bitOr(bitOr(byte, byte2), byte3));

if (encodedBytes[i + 4] == 61) break;
byte = bitSHLN(bitAnd(this.DECODE_TABLE[encodedBytes[i + 3]], 15), 4);
byte2 = bitSHRN(this.DECODE_TABLE[encodedBytes[i + 4]], 1);
decodedBytes.write(bitOr(byte, byte2));

if (encodedBytes[i + 5] == 61) break;
byte = bitSHLN(bitAnd(this.DECODE_TABLE[encodedBytes[i + 4]], 1), 7);
byte2 = bitSHLN(this.DECODE_TABLE[encodedBytes[i + 5]], 2);
byte3 = bitSHRN(this.DECODE_TABLE[encodedBytes[i + 6]], 3);
decodedBytes.write(bitOr(bitOr(byte, byte2), byte3));

if (encodedBytes[i + 7] == 61) break;
byte = bitSHLN(bitAnd(this.DECODE_TABLE[encodedBytes[i + 6]], 7), 5);
byte2 = this.DECODE_TABLE[encodedBytes[i + 7]];
decodedBytes.write(bitOr(byte, byte2));

}

return decodedBytes.toByteArray();
return createObject("java", "org.apache.commons.codec.binary.Base32").decode( arguments.encoded );
}

/**
* Convenience function for decoding a Base32 string to a string
*/
public string function Base32decodeString (required any string, string encoding = "utf-8")
{
return charsetEncode(base32decode(string), encoding);//createObject("java", "java.lang.String").init(base32decode(string));
return charsetEncode(base32decode(string), encoding);
}

private numeric function getCurrentTime()
Expand Down
Loading