I am all for pushing the envelope in computer security; however, I don’t live on the bleeding edge. I am a security practitioner and implementor. I like currently deployed encryption technologies. It is the failure to encrypt and the failure to protect that really gets me upset.Recently, the Secure Quick Reliable Login (SQRL) protocol (or should I say process) has been getting a fair amount of press. I had some initial doubts. After reviewing the various blogs and security newsgroups, I believe that the basic technical premise may be critically flawed. It is primarily the concept embodied in this phrase from the SQRL website that causes concern: “The primary enabling feature of the SQRL system's underlying crypto technology is its ability to use the randomly distributed arbitrary output of the 256-bit SHA256 hash function as its private key.” What Steve Gibson seems to be saying is that SQRL will calculate a value based on a master key and site address to use as the private key. From that calculated private key, SQRL purports to calculate a consistent public key to be used for asymmetrical encryption and identity (authentication). That's far from standard practice in public key cryptography.
In any case, in the meantime, I decided to demonstrate an implementation of something like SQRL (just the guts) without some of the technological blood and hype. This uses currently existing and readily accessible technologies to accomplish the PRIMARY features of SQRL -- web application authentication. I call this Secure Quick Real Login (SQReaL).
I don’t want to delve into discussions of the philosophy of application authentication, mobile device security or privacy security data protection. If you are not taking the basic steps of backing up your mobile device, setting a device timeout and login password, etc. then there is no security system that can protect you. I’m also not persuaded that there is more value in this than in regular username / password authentication, nor that it bests other authentication services. Perhaps to attain a consistent, protected anonymous persona, this might be a good authentication mechanism, if that even makes sense.
So what limited features does SQReaL attempt to implement?
1) A master key, stored on the device that provides (access to) private / public key pairs
2) Site-specific private / public key pairs that are consistent across sessions (hence, can be used for identity)
3) Encryption of a challenge code with the private key that can be decrypted by the authentication service using the public key that is delivered with the encrypted challenge code
And, no rocket science is required.
This is a quick implementation using code that I’ve published elsewhere, including my book, Expert Oracle and Java Security. For ease of discussion, I’m storing all the data in files. On the application server, you would want to use a database. I’m also using Triple-DES for encrypted local storage and RSA for public / private keys. I’m not trying to persuade anyone that this is a superior set of algorithms – they are just common, available and quick enough for this demonstration.
On a practical issue with SQRL, to change Master Keys in SQReaL, you simply move the Site Key Store into memory, change the DES Cipher to use the new Master Key and serialize the Site Key Store back to storage.
Here is SQReaL:
// David Coffin, 11/02/2013
// Requires Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 6
// Installed in every JRE and JDK/jre
package dac;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.math.BigInteger;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
/*
* The difficult (mis-)conception in the SQRL description comes in these words:
* "The primary enabling feature ... is its ability to use ... as its private key"
* Simply put, SQRL portends to use a calculated value AS the private key
* And to calculate a public key from that value to be used for asymmetrical encryption
* That is not done
*
* Here is a solution which uses a secure local data store of fixed key pairs
* That are designated for use with specific URL hosts.
*
* Note that the Master key has nothing to do with generation of site keys,
* but is used for security of the store
* And, likewise, the site URL is not used for generation of the site key,
* but is used as the index for the key store value
*/
public class SQReaL {
private static boolean isTest = false;
private static String masterKeyLocation = "C:/dac/Master.ser";
private static String publicKeyLocation = "C:/dac/Public.ser";
public static void main(String[] args) {
SQReaL.ReturnValues returnValues = getReturnValues( "http://try.this.one/", "abcdefghijk" );
System.out.println( "Modulus: " + returnValues.sitePublicModulus );
System.out.println( "Exponent: " + returnValues.sitePublicExponent );
System.out.println( "Encrypted: " + returnValues.encryptedChallengeCode );
System.out.println( "User: " + serverIdentifyUser(
returnValues.sitePublicModulus, returnValues.sitePublicExponent ) );
System.out.println( "Decrypted: " + serverDecryptChallengeCode(
returnValues.sitePublicModulus, returnValues.sitePublicExponent,
returnValues.encryptedChallengeCode ) );
System.out.println( "----------------" );
// Next will use same site key (modulus and exponent) but different challenge
returnValues = getReturnValues( "http://try.this.one/", "1abcdefghijk" );
System.out.println( "Modulus: " + returnValues.sitePublicModulus );
System.out.println( "Exponent: " + returnValues.sitePublicExponent );
System.out.println( "Encrypted: " + returnValues.encryptedChallengeCode );
System.out.println( "User: " + serverIdentifyUser(
returnValues.sitePublicModulus, returnValues.sitePublicExponent ) );
System.out.println( "Decrypted: " + serverDecryptChallengeCode(
returnValues.sitePublicModulus, returnValues.sitePublicExponent,
returnValues.encryptedChallengeCode ) );
System.out.println( "----------------" );
// Next will use different site key, but same challenge - should be encrypted differently
returnValues = getReturnValues( "http://try.this.two/", "1abcdefghijk" );
System.out.println( "Modulus: " + returnValues.sitePublicModulus );
System.out.println( "Exponent: " + returnValues.sitePublicExponent );
System.out.println( "Encrypted: " + returnValues.encryptedChallengeCode );
// Note that for this test, we are acting like a single server application
// So since we passed a different URL, we will have a different key
// And we will report this as a different user
System.out.println( "User: " + serverIdentifyUser(
returnValues.sitePublicModulus, returnValues.sitePublicExponent ) );
System.out.println( "Decrypted: " + serverDecryptChallengeCode(
returnValues.sitePublicModulus, returnValues.sitePublicExponent,
returnValues.encryptedChallengeCode ) );
System.out.println( "----------------" );
}
private static String masterKey = null;
/**
* The sitePublicKey (since using RSA, the key is a modulus and exponent)
* is returned to the host and is used for two processes:
* 1) To identify me uniquely (should never change) - lack of randomness needed for identity
* 2) To decrypt the challenge code - assures that I have the matching public key
* Note that the algorithm must be common between the client and server
* Returning as Strings for inclusion in html post, or other
* Using my encode() method for value obfuscation
*/
public class ReturnValues {
String sitePublicModulus;
String sitePublicExponent;
String encryptedChallengeCode;
}
private static Map<String,KeyPubPriv> keyStore = null;
public static ReturnValues getReturnValues( String hostUR, String challengeCode ) {
SQReaL sqrl = new SQReaL();
return sqrl.calcReturnValues( hostUR, challengeCode );
}
private ReturnValues calcReturnValues( String hostUR, String challengeCode ) {
ReturnValues returnValues = new ReturnValues();
try {
getMasterKey();
KeyPubPriv sitepair = getSiteKeyPair( hostUR );
// encode() for return, but don't store encoded
returnValues.encryptedChallengeCode =
encodeBytes( getEncryptedChallengeCode( sitepair.privateKey, challengeCode ) );
returnValues.sitePublicModulus = sitepair.publicKey.getModulus().toString();
returnValues.sitePublicExponent = sitepair.publicKey.getPublicExponent().toString();
} catch( Exception x ) {
x.toString();
}
return returnValues;
}
// Synchronized in order to keep masterKeyFile access limited to one thread at a time
private static synchronized void getMasterKey() throws Exception {
if( masterKey != null ) return;
ObjectInputStream oIn = null;
ObjectOutputStream oOut = null;
try {
File masterKeyFile = new File( masterKeyLocation );
if( masterKeyFile.exists() ) {
oIn = new ObjectInputStream( new FileInputStream( masterKeyFile ) );
masterKey = (String)oIn.readObject();
oIn.close();
} else {
oOut = new ObjectOutputStream( new FileOutputStream( masterKeyFile ) );
// No need to recreate this, so just use random values
String newMasterKey = "";
// 32 bytes is 256 bits
for( int i = 0; i < 32; i++ ) {
// I want printable ASCII characters (32 to 126) only
newMasterKey += (char)( rand.nextInt( 126 - 32 ) + 32 );
}
masterKey = new String( newMasterKey );
oOut.writeObject( masterKey );
oOut.flush();
oOut.close();
}
if( isTest ) System.out.println( "master Key: " + masterKey );
} catch( Exception x ) {
x.printStackTrace();
throw x;
} finally {
if( oOut != null ) oOut.close();
if( oIn != null ) oIn.close();
}
}
private static byte[] salt = { (byte) 0x7f, (byte) 0xd7, (byte) 0xef,
(byte) 0x59, (byte) 0xd2, (byte) 0x74, (byte) 0x54, (byte) 0x7a };
private static int itCount = 18;
private static PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, itCount);
private static SecretKeyFactory keyFac;
private static Cipher pbeCipher;
private static String algorithm = "PBEWithSHA1AndDESede";
static {
try {
keyFac = SecretKeyFactory.getInstance(algorithm);
algorithm = keyFac.getAlgorithm();
pbeCipher = Cipher.getInstance( algorithm );
} catch (Exception x) {}
}
// Synchronized in order to keep publicKeyFile access limited to one thread at a time
// Synchronized method instead of blocks to assure we retain all keyStore updates
@SuppressWarnings("unchecked")
private static synchronized KeyPubPriv getSiteKeyPair( String hostUR ) throws Exception {
KeyPubPriv sitePair = new KeyPubPriv();
ObjectInputStream oIn = null;
ObjectOutputStream oOut = null;
try {
File publicKeyFile = new File( publicKeyLocation );
PBEKeySpec pbeKeySpec = new PBEKeySpec(masterKey.toCharArray());
SecretKey pbeKey = keyFac.generateSecret(pbeKeySpec);
if( keyStore == null ) {
if( publicKeyFile.exists() ) {
pbeCipher.init(Cipher.DECRYPT_MODE, pbeKey, pbeParamSpec);
oIn = new ObjectInputStream(
new CipherInputStream( new FileInputStream( publicKeyFile ), pbeCipher ) );
keyStore = (HashMap<String,KeyPubPriv>)oIn.readObject();
if( isTest ) System.out.println( "entry count: " + keyStore.size() );
oIn.close();
}
else keyStore = new HashMap<String,KeyPubPriv>();
}
if( keyStore.containsKey( hostUR ) )
sitePair = keyStore.get( hostUR );
else {
sitePair = makeRSAKeyPair();
keyStore.put( hostUR, sitePair );
pbeCipher.init(Cipher.ENCRYPT_MODE, pbeKey, pbeParamSpec);
oOut = new ObjectOutputStream(
new CipherOutputStream( new FileOutputStream( publicKeyFile ), pbeCipher ) );
oOut.writeObject( keyStore );
oOut.flush();
oOut.close();
}
} catch( Exception x ) {
x.printStackTrace();
throw x;
} finally {
if( oOut != null ) oOut.close();
if( oIn != null ) oIn.close();
}
return sitePair;
}
private static int keyLengthRSA = 1024;
private static SecureRandom rand = new SecureRandom();
static {
rand.nextLong();
rand.nextLong();
}
private static KeyPubPriv makeRSAKeyPair() throws Exception {
KeyPubPriv sitePair = new KeyPubPriv();
try {
KeyPairGenerator generator = KeyPairGenerator.getInstance( "RSA" );
generator.initialize( keyLengthRSA, rand );
KeyPair pair = generator.generateKeyPair();
sitePair.privateKey = pair.getPrivate();
sitePair.publicKey = ( RSAPublicKey )pair.getPublic();
} catch( Exception x ) {
throw x;
}
return sitePair;
}
private static Cipher cipherRSA;
static {
try {
cipherRSA = Cipher.getInstance( "RSA" );
} catch( Exception x ) {}
}
private byte[] getEncryptedChallengeCode( Key privateKey, String challengeCode )
throws Exception
{
byte[] encryptedBytes = new byte[0];
cipherRSA.init( Cipher.ENCRYPT_MODE, privateKey );
byte[] challengeCodeBytes = challengeCode.getBytes();
if( isTest ) System.out.println( "clear String: " + challengeCode );
if( isTest ) System.out.println( "clear bytes: " + new String( challengeCodeBytes ) );
encryptedBytes = cipherRSA.doFinal( challengeCodeBytes );
if( isTest ) System.out.println( "encrypted bytes: " + new String( encryptedBytes ) );
return encryptedBytes;
}
private static String userIDLocation = "C:/dac/UserID.ser";
private static Map<String,UserClass> userStore = null;
private static int userNo = 0;
// Synchronized in order to keep userIDFile access limited to one thread at a time
// Synchronized method instead of blocks to assure we retain all userID updates
// Preferably, in your web app server, you would store the userIDs in a database
@SuppressWarnings("unchecked")
private static synchronized String serverIdentifyUser(
String sitePublicModulus, String sitePublicExponent )
{
UserClass thisUser = new UserClass();
ObjectInputStream oIn = null;
ObjectOutputStream oOut = null;
try {
String userID = sitePublicModulus + "/" + sitePublicExponent;
File userIDFile = new File( userIDLocation );
if( userStore == null ) {
if( userIDFile.exists() ) {
oIn = new ObjectInputStream( new FileInputStream( userIDFile ) );
userStore = (HashMap<String,UserClass>)oIn.readObject();
userNo = userStore.size();
if( isTest ) System.out.println( "user count: " + userNo );
oIn.close();
}
else userStore = new HashMap<String,UserClass>();
}
if( userStore.containsKey( userID ) ) {
thisUser = userStore.get( userID );
if( thisUser.name.startsWith( "New User ") )
thisUser.name = thisUser.name.substring( 4 );
} else {
thisUser.userID = userID;
thisUser.name = "New User " + userNo;
userNo++;
userStore.put( userID, thisUser );
oOut = new ObjectOutputStream( new FileOutputStream( userIDFile ) );
oOut.writeObject( userStore );
oOut.flush();
oOut.close();
}
} catch( Exception x ) {
x.printStackTrace();
} finally {
try {
if( oOut != null ) oOut.close();
if( oIn != null ) oIn.close();
} catch( Exception y ) {}
}
return thisUser.name;
}
private static String serverDecryptChallengeCode( String sitePublicModulus,
String sitePublicExponent, String encryptedChallengeCode )
{
byte[] encryptedBytes = decodeByteString( encryptedChallengeCode );
if( isTest ) System.out.println( "decoded encrypted bytes: " + new String( encryptedBytes ) );
return serverDecryptChallengeCode( sitePublicModulus, sitePublicExponent, encryptedBytes );
}
private static String serverDecryptChallengeCode( String sitePublicModulus,
String sitePublicExponent, byte[] encryptedBytes )
{
String decryptedChallengeCode = "";
try {
BigInteger modulus = new BigInteger( sitePublicModulus );
BigInteger exponent = new BigInteger( sitePublicExponent );
RSAPublicKeySpec keySpec = new RSAPublicKeySpec( modulus, exponent );
KeyFactory kFactory = KeyFactory.getInstance( "RSA" );
RSAPublicKey extRSAPubKey = ( RSAPublicKey )kFactory.generatePublic( keySpec );
cipherRSA.init( Cipher.DECRYPT_MODE, extRSAPubKey );
encryptedBytes = cipherRSA.doFinal( encryptedBytes );
if( isTest ) System.out.println( "decrypted bytes: " + new String( encryptedBytes ) );
decryptedChallengeCode = new String( encryptedBytes );
if( isTest ) System.out.println( "decrypted String: " + decryptedChallengeCode );
} catch( Exception x ) {
x.printStackTrace();
}
return decryptedChallengeCode;
}
static String encodeBytes( byte[] bytes ) {
StringBuffer sBuf = new StringBuffer();
try {
for( byte b: bytes ) {
sBuf.append( String.format( "%02X", b ) );
}
} catch( Exception x ){}
return sBuf.toString();
}
static byte[] decodeByteString( String byteString ) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
String byteChars;
int byteVal;
for( int i = 0; i < byteString.length(); i++ ) {
byteChars = byteString.substring( i, i+2 );
i++;
//if( isTest ) System.out.println( byteChars );
// radix 16 is Hex
byteVal = Integer.parseInt( byteChars, 16 );
out.write( byteVal );
}
} catch( Exception x ){
} finally {
try {
out.flush();
out.close();
} catch( Exception y ) {}
}
return out.toByteArray();
}
}
class KeyPubPriv implements Serializable {
private static final long serialVersionUID = 1L;
RSAPublicKey publicKey;
Key privateKey;
}
class UserClass implements Serializable {
private static final long serialVersionUID = 1L;
String userID = "";
String name;
}