/* * JBoss, Home of Professional Open Source * Copyright 2013, Red Hat, Inc. and/or its affiliates, and individual * contributors by the @authors tag. See the copyright.txt in the * distribution for a full listing of individual contributors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jboss.totp; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.util.Calendar; import java.util.Random; import java.util.Timer; import java.util.TimerTask; import javax.microedition.lcdui.Alert; import javax.microedition.lcdui.AlertType; import javax.microedition.lcdui.Choice; import javax.microedition.lcdui.ChoiceGroup; import javax.microedition.lcdui.Command; import javax.microedition.lcdui.CommandListener; import javax.microedition.lcdui.Display; import javax.microedition.lcdui.Displayable; import javax.microedition.lcdui.Form; import javax.microedition.lcdui.Gauge; import javax.microedition.lcdui.List; import javax.microedition.lcdui.StringItem; import javax.microedition.lcdui.TextField; import javax.microedition.midlet.MIDlet; import javax.microedition.rms.RecordEnumeration; import javax.microedition.rms.RecordStore; import javax.microedition.rms.RecordStoreException; import javax.microedition.rms.RecordStoreNotFoundException; import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.digests.SHA1Digest; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.digests.SHA512Digest; import org.bouncycastle.crypto.macs.HMac; import org.bouncycastle.crypto.params.KeyParameter; /** * TOTP generator for Java ME. * * @author Josef Cacek */ public class TOTPMIDlet extends MIDlet implements CommandListener { private static final boolean DEBUG = false; private static final String STORE_CONFIG_OLD = "config"; private static final String STORE_PROFILE_CONFIG = "profile-config"; private static final String STORE_KEY_OLD = "key"; private static final String BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; private static final int[] BASE32_LOOKUP = { 0xFF, 0xFF, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; private static final char[] HEX_TABLE = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; private static final String SHA1 = "SHA-1"; private static final String SHA256 = "SHA-256"; private static final String SHA512 = "SHA-512"; private static final String[] HMAC_ALGORITHMS = { SHA1, SHA256, SHA512 }; private static final int[] HMAC_BYTE_COUNT = { 160 / 8, 256 / 8, 512 / 8 }; private static final int DEFAULT_TIMESTEP = 30; private static final byte[] DEFAULT_SECRET = null; private static final int DEFAULT_DIGITS = 6; private static final long DEFAULT_DELTA = 0L; private static final int DEFAULT_HMAC_ALG_IDX = 0; private static final String DEFAULT_PROFILE = "Default"; private static final long INVALID_COUNTER = -1L; private static final int INDEFINITE = 1; private static final int IDLE = 0; private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; private static final byte[] DEFAULT_CONFIG_BYTES = getProfileConfig(DEFAULT_PROFILE, EMPTY_BYTE_ARRAY, DEFAULT_TIMESTEP, DEFAULT_HMAC_ALG_IDX, DEFAULT_DIGITS, DEFAULT_DELTA); // GUI components // main screen private Command cmdExit = new Command("Exit", Command.EXIT, 1); private Command cmdProfiles = new Command("Profiles", Command.SCREEN, 2); private Command cmdOptions = new Command("Options", Command.SCREEN, 3); // main+options screen private Command cmdGenerator = new Command("Key generator", Command.SCREEN, 4); // options screen private Command cmdOK = new Command("OK", Command.OK, 1); private Command cmdReset = new Command("Default values", Command.SCREEN, 3); // keyGenerator screen private Command cmdNewKey = new Command("New key", Command.SCREEN, 1); private Command cmdGeneratorOK = new Command("OK", Command.OK, 1); // profiles screen private Command cmdAddProfile = new Command("Add", Command.SCREEN, 1); private Command cmdRemoveProfile = new Command("Remove", Command.SCREEN, 2); // confirmation screen private Command cmdCancel = new Command("Cancel", Command.CANCEL, 1); private final StringItem siKeyHex = new StringItem("HEX", null); private final StringItem siKeyBase32 = new StringItem("Base32 (no zeros)", null); private final StringItem siToken = new StringItem("Token", null); private final StringItem siProfile = new StringItem(null, null); private final StringItem siConfirm = new StringItem(null, null); private final Gauge gauValidity = new Gauge(null, false, DEFAULT_TIMESTEP - 1, DEFAULT_TIMESTEP); private final TextField tfSecret = new TextField("Secret key (Base32, no zeros)", null, 105, TextField.ANY); private final TextField tfProfile = new TextField("Profile name", null, 105, TextField.ANY); private final TextField tfTimeStep = new TextField("Time step (sec)", String.valueOf(DEFAULT_TIMESTEP), 3, TextField.NUMERIC); private final TextField tfDigits = new TextField("Number of digits", String.valueOf(DEFAULT_DIGITS), 2, TextField.NUMERIC); //http://docs.oracle.com/javame/config/cldc/ref-impl/midp2.0/jsr118/javax/microedition/lcdui/TextField.htm getMaxSize private final TextField tfDelta = new TextField("Time correction (sec)", String.valueOf(DEFAULT_DELTA), 20, TextField.ANY); private final ChoiceGroup chgHmacAlgorithm = new ChoiceGroup("HMAC algorithm", Choice.EXCLUSIVE); private final Alert alertWarn = new Alert("Warning", "Something went wrong!", null, AlertType.ALARM); private final Form fMain = new Form("TOTP ME ${project.version}"); private final Form fOptions = new Form("TOTP configuration"); private final Form fGenerator = new Form("Key generator"); private final Form fConfirm = new Form("Confirm action"); private final List listProfiles = new List("Profiles", Choice.IMPLICIT); private final Timer timer = new Timer(); private final RefreshTokenTask refreshTokenTask = new RefreshTokenTask(); private long cachedCounter; private HMac hmac; private final Random rand = new Random(); private int[] recordIds; // Constructors ---------------------------------------------------------- /** * Constructor - initializes GUI components. */ public TOTPMIDlet() { // Main display fMain.append(siToken); fMain.append(gauValidity); fMain.append(siProfile); fMain.addCommand(cmdExit); fMain.addCommand(cmdProfiles); fMain.addCommand(cmdOptions); fMain.addCommand(cmdGenerator); fMain.setCommandListener(this); // Key generator fGenerator.append(siKeyHex); fGenerator.append(siKeyBase32); fGenerator.addCommand(cmdGeneratorOK); fGenerator.addCommand(cmdNewKey); fGenerator.setCommandListener(this); // Configuration display fOptions.append(tfSecret); fOptions.append(tfProfile); fOptions.append(tfTimeStep); fOptions.append(tfDigits); for (int i = 0; i < HMAC_ALGORITHMS.length; i++) { chgHmacAlgorithm.append(HMAC_ALGORITHMS[i], null); } fOptions.append(chgHmacAlgorithm); fOptions.append(tfDelta); fOptions.addCommand(cmdOK); fOptions.addCommand(cmdGenerator); fOptions.addCommand(cmdReset); fOptions.setCommandListener(this); // Profiles listProfiles.addCommand(cmdAddProfile); listProfiles.addCommand(cmdRemoveProfile); listProfiles.setCommandListener(this); // Confirm dialog fConfirm.append(siConfirm); fConfirm.addCommand(cmdOK); fConfirm.addCommand(cmdCancel); fConfirm.setCommandListener(this); // set alert alertWarn.setTimeout(Alert.FOREVER); tfTimeStep.setString(String.valueOf(DEFAULT_TIMESTEP)); tfDigits.setString(String.valueOf(DEFAULT_DIGITS)); chgHmacAlgorithm.setSelectedIndex(DEFAULT_HMAC_ALG_IDX, true); } // Public methods -------------------------------------------------------- /** * Loads configuration and initializes token-refreshing timer. * * @see javax.microedition.midlet.MIDlet#startApp() */ public void startApp() { try { loadProfiles(); reorderProfiles(); if (listProfiles.getSelectedIndex() < 0) listProfiles.setSelectedIndex(0, true); if (listProfiles.size() > 1) { Display.getDisplay(this).setCurrent(listProfiles); } else { loadSelectedProfile(); } timer.schedule(refreshTokenTask, 0L, 1000L); } catch (Exception e) { debugErr("TOTPMIDlet.startApp() - " + e.getMessage()); error(e); } } /* * (non-Javadoc) * * @see javax.microedition.midlet.MIDlet#pauseApp() */ public void pauseApp() { } /** * Saves configuration to the record store and exits the refreshing timer. * * @see javax.microedition.midlet.MIDlet#destroyApp(boolean) */ public void destroyApp(boolean unconditional) { refreshTokenTask.cancel(); timer.cancel(); notifyDestroyed(); } /** * Handles command actions from all forms. * * @see javax.microedition.lcdui.CommandListener#commandAction(javax.microedition.lcdui.Command, * javax.microedition.lcdui.Displayable) */ public void commandAction(Command aCmd, Displayable aDisp) { if (DEBUG && aCmd != null) { debug("Options - Command action: " + aCmd.getLabel()); } final Display display = Display.getDisplay(this); if (aDisp == fConfirm) { if (aCmd == cmdOK) { removeProfile(listProfiles.getSelectedIndex()); } display.setCurrent(listProfiles); return; } if (aCmd == cmdOK) { final String warning = validateInput(); if (warning.length() == 0) { siProfile.setText(tfProfile.getString()); final int algorithmIdx = chgHmacAlgorithm.getSelectedIndex(); final byte[] secretKey = base32Decode(tfSecret.getString()); HMac newHmac = null; if (secretKey != null) { Digest digest = null; if (SHA1.equals(HMAC_ALGORITHMS[algorithmIdx])) { digest = new SHA1Digest(); } else if (SHA256.equals(HMAC_ALGORITHMS[algorithmIdx])) { digest = new SHA256Digest(); } else if (SHA512.equals(HMAC_ALGORITHMS[algorithmIdx])) { digest = new SHA512Digest(); } newHmac = new HMac(digest); newHmac.init(new KeyParameter(secretKey)); } setHMac(newHmac); refreshTokenTask.run(); display.setCurrent(fMain); if (aDisp != null) save(); reorderProfiles(); } else { displayAlert("Invalid input:\n" + warning, fOptions); } } else if (aCmd == cmdGenerator) { final byte[] key = base32Decode(tfSecret.getString()); // set current key siKeyHex.setText(key == null ? "" : toHexString(key, 0, key.length)); siKeyBase32.setText(base32Encode(key)); display.setCurrent(fGenerator); } else if (aCmd == cmdProfiles) { display.setCurrent(listProfiles); } else if (aCmd == cmdAddProfile) { final Calendar cal = Calendar.getInstance(); // use date-time as generated profile name YYYYMMDD-HHMMSS final String profileName = cal.get(Calendar.YEAR) + zeroLeftPad(cal.get(Calendar.MONTH) + 1, 2) + zeroLeftPad(cal.get(Calendar.DAY_OF_MONTH), 2) + "-" + zeroLeftPad(cal.get(Calendar.HOUR_OF_DAY), 2) + zeroLeftPad(cal.get(Calendar.MINUTE), 2) + zeroLeftPad(cal.get(Calendar.SECOND), 2); if (DEBUG) debug("Creating profile" + profileName); int newPos = 0; while (newPos < listProfiles.size() && profileName.compareTo(listProfiles.getString(newPos)) > 0) { newPos++; } listProfiles.insert(newPos, profileName, null); listProfiles.setSelectedIndex(newPos, true); int[] newRecIds = new int[recordIds.length + 1]; System.arraycopy(recordIds, 0, newRecIds, 0, newPos); System.arraycopy(recordIds, newPos, newRecIds, newPos + 1, recordIds.length - newPos); recordIds = newRecIds; final byte[] profileConfig = getProfileConfig(profileName, EMPTY_BYTE_ARRAY, DEFAULT_TIMESTEP, DEFAULT_HMAC_ALG_IDX, DEFAULT_DIGITS, DEFAULT_DELTA); recordIds[newPos] = addProfileToRecordStore(profileConfig); } else if (aDisp == listProfiles && aCmd == List.SELECT_COMMAND) { if (listProfiles.getSelectedIndex() >= 0) loadSelectedProfile(); } else if (aCmd == cmdRemoveProfile) { switch (listProfiles.size()) { case 0: displayAlert("There is no profile to delete.", listProfiles); break; case 1: displayAlert("You can't remove the last profile.", listProfiles); break; default: if (listProfiles.getSelectedIndex() >= 0) { siConfirm.setText("Do you really want to delete profile " + listProfiles.getString(listProfiles.getSelectedIndex()) + "?"); display.setCurrent(fConfirm); } break; } } else if (aCmd == cmdNewKey) { final byte[] secretKey = generateNewKey(); siKeyHex.setText(toHexString(secretKey, 0, secretKey.length)); siKeyBase32.setText(base32Encode(secretKey)); tfSecret.setString(siKeyBase32.getText()); } else if (aCmd == cmdGeneratorOK) { display.setCurrent(fOptions); } else if (aCmd == cmdOptions) { display.setCurrent(fOptions); } else if (aCmd == cmdReset) { setHMac(null); gauValidity.setMaxValue(INDEFINITE); gauValidity.setValue(IDLE); tfSecret.setString(base32Encode(DEFAULT_SECRET)); tfTimeStep.setString(Integer.toString(DEFAULT_TIMESTEP)); tfDigits.setString(Integer.toString(DEFAULT_DIGITS)); chgHmacAlgorithm.setSelectedIndex(DEFAULT_HMAC_ALG_IDX, true); tfDelta.setString(Long.toString(DEFAULT_DELTA)); tfProfile.setString(listProfiles.getString(listProfiles.getSelectedIndex())); } else if (aCmd == cmdExit) { destroyApp(false); } cachedCounter = INVALID_COUNTER; } // Protected methods ----------------------------------------------------- /** * Generates the current token. If the token can't be generated it returns * an empty String. * * @return current token or an empty String */ protected static String genToken(final long counter, final HMac hmac, final int digits) { if (hmac == null || digits <= 0) { return ""; } // generate 8 byte HOTP counter value (RFC 4226) final byte msg[] = new byte[8]; for (int i = 0; i < 8; i++) { msg[7 - i] = (byte) (counter >>> (i * 8)); } // compute the HMAC final byte[] hash = new byte[hmac.getMacSize()]; hmac.update(msg, 0, msg.length); hmac.doFinal(hash, 0); // Transform the HMAC to a HOTP value according to RFC 4226. final int off = hash[hash.length - 1] & 0xF; // Truncate the HMAC (look at RFC 4226 section 5.3, step 2). int binary = ((hash[off] & 0x7f) << 24) | ((hash[off + 1] & 0xff) << 16) | ((hash[off + 2] & 0xff) << 8) | ((hash[off + 3] & 0xff)); // use requested number of digits final byte[] digitsArray = new byte[digits]; for (int i = 0; i < digits; i++) { digitsArray[digits - 1 - i] = (byte) ('0' + (char) (binary % 10)); binary /= 10; } return new String(digitsArray, 0, digits); } /** * Returns counter value for given time and timeStep. * * @param timeInSec * @param timeStep * @return counter (HOTP) */ protected static long getCounter(final long timeInSec, final int timeStep) { return timeStep > 0 ? timeInSec / timeStep : INVALID_COUNTER; } // Private methods ------------------------------------------------------- /** * Returns HMac. * * @return */ private synchronized HMac getHMac() { return hmac; } /** * Sets HMac. * * @param hmac */ private synchronized void setHMac(HMac hmac) { this.hmac = hmac; } /** * Validates (and makes basic corrections in) the options form. It returns * warning message(s) if the validation error occurs. An empty string is * returned if the validation is successful. * * @return warning message */ private String validateInput() { final StringBuffer warnings = new StringBuffer(); int algIdx = chgHmacAlgorithm.getSelectedIndex(); if (algIdx < 0) { algIdx = 0; chgHmacAlgorithm.setSelectedIndex(algIdx, true); } String str = tfSecret.getString(); if (str == null) { str = ""; } else { final StringBuffer sb = new StringBuffer(); str = str.toUpperCase().replace('0', 'O').replace('1', 'L'); for (int i = 0; i < str.length(); i++) { char ch = str.charAt(i); if (BASE32_CHARS.indexOf(ch) >= 0) { sb.append(ch); } } str = sb.toString(); } tfSecret.setString(str); int step = 0; try { step = Integer.parseInt(tfTimeStep.getString()); } catch (NumberFormatException e) { tfTimeStep.setString(Integer.toString(DEFAULT_TIMESTEP)); step = DEFAULT_TIMESTEP; } if (step <= 0) { if (warnings.length() > 0) warnings.append("\n"); warnings.append("Time step must be positive number."); } gauValidity.setMaxValue((str.length() > 0 && step > 1) ? step - 1 : INDEFINITE); int digits = 0; try { digits = Integer.parseInt(tfDigits.getString()); } catch (NumberFormatException e) { tfDigits.setString(Integer.toString(DEFAULT_DIGITS)); } if (digits <= 0) { if (warnings.length() > 0) warnings.append("\n"); warnings.append("Number of digits must be positive number."); } //Example: If device manufactur has set limited lifetime [2000..2017) //In Browser console: (Date.UTC(2017,0,1)-Date.UTC(2000,0,1))/1000 //delta = 536544000 long delta = 0L; try { delta = Long.parseLong(tfDelta.getString()); } catch (NumberFormatException e) { tfDelta.setString(Long.toString(DEFAULT_DELTA)); if (warnings.length() > 0) warnings.append("\n"); warnings.append("Incorrect delta value '"); warnings.append(tfDelta.getString()); warnings.append("' (is not a number)."); } return warnings.toString(); } /** * Shows {@link Alert} warning screen with given message. * * @param msg * @param nextDisplayable * Next screen, which is displayed after warning confirmation by * a user. */ private void displayAlert(final String msg, Displayable nextDisplayable) { alertWarn.setString(msg); Display.getDisplay(this).setCurrent(alertWarn, nextDisplayable); } /** * Adds new profile to {@link RecordStore} and returns ID of the new record. * It returns -1 if adding fails. * * @param configBytes * byte array profile representation * @return new record ID or -1 (if adding fails) */ private int addProfileToRecordStore(final byte[] configBytes) { RecordStore tmpRS = null; try { tmpRS = RecordStore.openRecordStore(STORE_PROFILE_CONFIG, true); return tmpRS.addRecord(configBytes, 0, configBytes.length); } catch (Exception e) { debugErr("addProfile - " + e.getClass().getName() + " - " + e.getMessage()); } finally { if (tmpRS != null) { try { tmpRS.closeRecordStore(); } catch (RecordStoreException e) { debugErr("addProfile (close) - " + e.getClass().getName() + " - " + e.getMessage()); } } } return -1; } /** * Removes record with given ID from a {@link RecordStore} with given name. * * @param storeName * @param recordId */ private void removeRecordFromStore(final String storeName, final int recordId) { RecordStore tmpRS = null; if (DEBUG) debug("removeRecordFromStore - " + storeName + " - " + recordId); try { tmpRS = RecordStore.openRecordStore(storeName, false); tmpRS.deleteRecord(recordId); } catch (RecordStoreNotFoundException e) { if (DEBUG) debug("removeRecordFromStore - RecordStoreNotFoundException - " + storeName); } catch (Exception e) { debugErr("removeRecordFromStore - " + e.getClass().getName() + " - " + storeName + " - " + recordId + ": " + e.getMessage()); } finally { if (tmpRS != null) { try { tmpRS.closeRecordStore(); } catch (RecordStoreException e) { debugErr("removeRecordFromStore (close) - " + e.getClass().getName() + " - " + storeName + " - " + recordId + ": " + e.getMessage()); } } } } /** * Sets record with given ID and value to a {@link RecordStore} with given * name. * * @param storeName * @param recordId * @param value * @return */ private boolean saveRecordToStore(final String storeName, final int recordId, final byte[] value) { RecordStore tmpRS = null; try { tmpRS = RecordStore.openRecordStore(storeName, true); tmpRS.setRecord(recordId, value, 0, value.length); } catch (Exception e) { debugErr("saveRecordToStore - " + e.getClass().getName() + " - " + storeName + " - " + recordId + ": " + e.getMessage()); return false; } finally { if (tmpRS != null) { try { tmpRS.closeRecordStore(); } catch (RecordStoreException e) { debugErr("saveRecordToStore (close) - " + e.getClass().getName() + " - " + storeName + " - " + recordId + ": " + e.getMessage()); } } } return true; } /** * Loads value of a record with given ID from a {@link RecordStore} with * given name. * * @param storeName * @param recordId * @return */ private byte[] loadRecordFromStore(final String storeName, final int recordId) { RecordStore tmpRS = null; byte[] value = EMPTY_BYTE_ARRAY; try { tmpRS = RecordStore.openRecordStore(storeName, false); value = tmpRS.getRecord(recordId); } catch (RecordStoreNotFoundException e) { if (DEBUG) { debug("loadRecordFromStore - RecordStoreNotFoundException - " + storeName); } } catch (Exception e) { debugErr("loadRecordFromStore - " + e.getClass().getName() + " - " + storeName + " - " + recordId + ": " + e.getMessage()); } finally { if (tmpRS != null) { try { tmpRS.closeRecordStore(); } catch (RecordStoreException e) { debugErr("loadRecordFromStore (close) - " + e.getClass().getName() + " - " + storeName + " - " + recordId + ": " + e.getMessage()); } } } return value; } /** * Removes profile with given index from GUI list and the * {@link RecordStore}. * * @param profileIdx */ private void removeProfile(final int profileIdx) { if (profileIdx >= listProfiles.size() || profileIdx < 0) { return; } listProfiles.delete(profileIdx); listProfiles.setSelectedIndex(profileIdx < listProfiles.size() ? profileIdx : profileIdx - 1, true); removeRecordFromStore(STORE_PROFILE_CONFIG, recordIds[profileIdx]); int[] newRecIds = new int[recordIds.length - 1]; System.arraycopy(recordIds, 0, newRecIds, 0, profileIdx); System.arraycopy(recordIds, profileIdx + 1, newRecIds, profileIdx, newRecIds.length - profileIdx); recordIds = newRecIds; } /** * Loads configuration of the selected profile. */ private void loadSelectedProfile() { final int profileIdx = listProfiles.getSelectedIndex(); // load from profile debug("Loading profile config record."); final byte[] profileConfig = loadRecordFromStore(STORE_PROFILE_CONFIG, recordIds[profileIdx]); ByteArrayInputStream bais = new ByteArrayInputStream(profileConfig); DataInputStream dis = new DataInputStream(bais); String base32EncodedSecret = ""; try { tfProfile.setString(dis.readUTF()); byte[] key = new byte[dis.readByte()]; dis.readFully(key); base32EncodedSecret = base32Encode(key); tfTimeStep.setString(String.valueOf(dis.readInt())); chgHmacAlgorithm.setSelectedIndex(dis.readInt(), true); tfDigits.setString(String.valueOf(dis.readByte())); tfDelta.setString(String.valueOf(dis.readLong())); } catch (Exception e) { e.printStackTrace(); debugErr("loading profile configuration - " + e.getClass().getName() + " - " + e.getMessage()); } finally { try { dis.close(); } catch (IOException e) { e.printStackTrace(); } } tfSecret.setString(base32EncodedSecret); siProfile.setText(tfProfile.getString()); siToken.setText(""); int timeStep = -1; try { timeStep = Integer.parseInt(tfTimeStep.getString()); } catch (NumberFormatException e) { debugErr(e.getMessage()); } gauValidity.setMaxValue((base32EncodedSecret.length() > 0 && timeStep > 1) ? timeStep - 1 : INDEFINITE); if (base32EncodedSecret.length() == 0) { final Display display = Display.getDisplay(this); display.setCurrent(fOptions); } else { // use validation - to check loaded data commandAction(cmdOK, null); } } /** * Loads list of profile names and IDs from the {@link RecordStore}. */ private void loadProfiles() { RecordStore tmpRS = null; recordIds = new int[0]; try { tmpRS = RecordStore.openRecordStore(STORE_PROFILE_CONFIG, true); if (tmpRS.getNumRecords() == 0) { byte[] newRecord = DEFAULT_CONFIG_BYTES; // try to load old-style (1.3) configuration byte[] secret = loadRecordFromStore(STORE_KEY_OLD, 1); if (secret.length > 0) { debug("Loading old config."); final byte[] configBytes = loadRecordFromStore(STORE_CONFIG_OLD, 1); final ByteArrayInputStream bais = new ByteArrayInputStream(configBytes); final DataInputStream dis = new DataInputStream(bais); int ts = DEFAULT_TIMESTEP, idx = DEFAULT_HMAC_ALG_IDX; long delta = DEFAULT_DELTA; int digits = DEFAULT_DIGITS; try { ts = dis.readInt(); idx = dis.readInt(); digits = dis.readByte(); delta = dis.readLong(); } catch (Exception e) { debugErr("loading old configuration - " + e.getClass().getName() + " - " + e.getMessage()); } finally { try { dis.close(); } catch (IOException e) { debugErr("loading old configuration (close) - " + e.getClass().getName() + " - " + e.getMessage()); } } newRecord = getProfileConfig(DEFAULT_PROFILE, secret, ts, idx, digits, delta); try { RecordStore.deleteRecordStore(STORE_KEY_OLD); } catch (RecordStoreException e) { // nothing to do here } try { RecordStore.deleteRecordStore(STORE_CONFIG_OLD); } catch (RecordStoreException e) { // nothing to do here } } debug("Adding new configuration record."); tmpRS.addRecord(newRecord, 0, newRecord.length); } // load profile record IDs recordIds = new int[tmpRS.getNumRecords()]; RecordEnumeration recEnum = tmpRS.enumerateRecords(null, null, false); int i = 0; while (recEnum.hasNextElement()) { recordIds[i++] = recEnum.nextRecordId(); } // load profile names for (i = 0; i < recordIds.length; i++) { final int recordId = recordIds[i]; debug("Parsing profile name for record " + recordId); final String profileName = parseProfileName(tmpRS.getRecord(recordId)); debug("Parsed profile name: " + profileName); listProfiles.append(profileName, null); } } catch (Exception e) { debugErr("loadProfiles - " + e.getClass().getName() + " - " + e.getMessage()); } finally { if (tmpRS != null) { try { tmpRS.closeRecordStore(); } catch (RecordStoreException e) { debug("loadProfiles (close) - " + e.getClass().getName() + " - " + e.getMessage()); } } } } /** * Returns profile name from given profile record value. * * @param profileBytes * @return profile name */ private String parseProfileName(byte[] profileBytes) { final ByteArrayInputStream bais = new ByteArrayInputStream(profileBytes); final DataInputStream dis = new DataInputStream(bais); try { return dis.readUTF(); } catch (IOException e) { debugErr(e.getMessage()); } finally { try { dis.close(); } catch (IOException e) { debugErr(e.getMessage()); } } return DEFAULT_PROFILE; } /** * Saves profile to a record store. */ private void save() { final int profileIdx = listProfiles.getSelectedIndex(); final int recordId = recordIds[profileIdx]; // store configuration of current profile final byte[] configBytes = getProfileConfig(tfProfile.getString(), base32Decode(tfSecret.getString()), Integer.parseInt(tfTimeStep.getString()), chgHmacAlgorithm.getSelectedIndex(), Integer.parseInt(tfDigits.getString()), Long.parseLong(tfDelta.getString())); saveRecordToStore(STORE_PROFILE_CONFIG, recordId, configBytes); // update also profile name listProfiles.set(profileIdx, tfProfile.getString(), null); } /** * Creates profile record from provided values. * * @param profileName * @param key * @param timeStep * @param hmacIdx * @param digits * @param delta * @return */ private static byte[] getProfileConfig(String profileName, byte[] key, int timeStep, int hmacIdx, int digits, long delta) { if (key == null) key = EMPTY_BYTE_ARRAY; final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final DataOutputStream dos = new DataOutputStream(baos); try { dos.writeUTF(profileName); dos.writeByte(key.length); dos.write(key); dos.writeInt(timeStep); dos.writeInt(hmacIdx); dos.writeByte(digits); dos.writeLong(delta); } catch (IOException e) { debugErr("Creating configuration failed - " + e.getMessage()); } finally { try { dos.close(); } catch (IOException e) { debugErr("Creating configuration failed (close)- " + e.getMessage()); } } return baos.toByteArray(); } /** * Debug function * * @param aWhat */ private static void debug(final String aWhat) { if (DEBUG) { System.out.println(">>>DEBUG " + aWhat); } } /** * Debug function for errors * * @param aWhat */ private static void debugErr(final String aWhat) { if (DEBUG) { System.err.println(">>>ERROR " + aWhat); } } /** * Prints error. * * @param anErr */ private static void error(final Object anErr) { if (anErr instanceof Throwable) { ((Throwable) anErr).printStackTrace(); } else { System.err.println(">>>ERROR " + anErr); } } /** * Encodes byte array to Base32 String. Returns not-null String. * * @param bytes * Bytes to encode. * @return Encoded byte array <code>bytes</code> as a String. * */ private static String base32Encode(final byte[] bytes) { if (bytes == null) { return ""; } int i = 0, index = 0, digit = 0, outchars = 0; int currByte, nextByte; StringBuffer base32 = new StringBuffer((bytes.length + 7) * 8 / 5); while (i < bytes.length) { currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign // Is the current digit going to span a byte boundary? if (index > 3) { if ((i + 1) < bytes.length) { nextByte = (bytes[i + 1] >= 0) ? bytes[i + 1] : (bytes[i + 1] + 256); } else { nextByte = 0; } digit = currByte & (0xFF >> index); index = (index + 5) % 8; digit <<= index; digit |= nextByte >> (8 - index); i++; } else { digit = (currByte >> (8 - (index + 5))) & 0x1F; index = (index + 5) % 8; if (index == 0) i++; } base32.append(BASE32_CHARS.charAt(digit)); outchars++; if (outchars % 4 == 0) base32.append(" "); } return base32.toString(); } /** * Decodes the given Base32 String to a raw byte array. * * @param base32 * @return Decoded <code>base32</code> String as a raw byte array. */ private static byte[] base32Decode(final String aBase32) { if (aBase32 == null || aBase32.length() == 0) return null; final String base32 = aBase32.toUpperCase(); int i, index, lookup, offset, digit; byte[] bytes = new byte[base32.length() * 5 / 8]; for (i = 0, index = 0, offset = 0; i < base32.length(); i++) { lookup = base32.charAt(i) - '0'; /* Skip chars outside the lookup table */ if (lookup < 0 || lookup >= BASE32_LOOKUP.length) { continue; } digit = BASE32_LOOKUP[lookup]; /* If this digit is not in the table, ignore it */ if (digit == 0xFF) { continue; } if (index <= 3) { index = (index + 5) % 8; if (index == 0) { bytes[offset] |= digit; offset++; if (offset >= bytes.length) break; } else { bytes[offset] |= digit << (8 - index); } } else { index = (index + 5) % 8; bytes[offset] |= (digit >>> index); offset++; if (offset >= bytes.length) { break; } bytes[offset] |= digit << (8 - index); } } return bytes; } /** * Convert a byte array to a String with a hexidecimal format. * * @param data * byte array * @param offset * starting byte (zero based) to convert. * @param length * number of bytes to convert. * * @return the String (with hexidecimal format) form of the byte array */ private static String toHexString(byte[] data, int offset, int length) { if (data == null || data.length == 0) return ""; final StringBuffer s = new StringBuffer(length * 3); for (int i = offset; i < offset + length; i++) { s.append(HEX_TABLE[(data[i] & 0xf0) >>> 4]); s.append(HEX_TABLE[(data[i] & 0x0f)]); if ((i - offset) % 2 == 1) s.append(" "); } return s.toString(); } /** * Generates a new random secret key. * * @return secret key suitable for selected HMac Algorithm */ private byte[] generateNewKey() { byte[] result = new byte[HMAC_BYTE_COUNT[chgHmacAlgorithm.getSelectedIndex()]]; for (int i = 0, len = result.length; i < len;) for (int rnd = rand.nextInt(), n = Math.min(len - i, 4); n-- > 0; rnd >>= 8) result[i++] = (byte) rnd; return result; } /** * Zero-left-padding for integer values. If a length of given integer * converted to string is smaller than len, then zeroes are filled on the * left side so the resulting string has lenght=len. * * @param value * @param len * @return */ private static String zeroLeftPad(int value, int len) { final String strValue = String.valueOf(value); if (strValue.length() >= len) return strValue; final StringBuffer sb = new StringBuffer(len); for (int i = strValue.length(); i < len; i++) { sb.append("0"); } return sb.append(strValue).toString(); } /** * Sorts profiles by the name alphabetically. It updates both listProfiles * and recordIds member variables. */ private void reorderProfiles() { final int n = listProfiles.size(); if (n < 2) return; debug("Sorting profiles alphabetically."); final int selectedIndex = listProfiles.getSelectedIndex(); final int selectedRecordId = selectedIndex >= 0 ? recordIds[selectedIndex] : -1; for (int i = n - 1; i > 0; i--) { for (int j = 0; j < i; j++) { final String thisStr = listProfiles.getString(j); final String nextStr = listProfiles.getString(j + 1); if (thisStr.compareTo(nextStr) > 1) { listProfiles.set(j, nextStr, null); listProfiles.set(j + 1, thisStr, null); final int thisId = recordIds[j]; recordIds[j] = recordIds[j + 1]; recordIds[j + 1] = thisId; } } } if (selectedRecordId >= 0) { for (int i = 0; i < recordIds.length; i++) { if (selectedRecordId == recordIds[i]) { listProfiles.setSelectedIndex(i, true); break; } } } } // Embedded classes ------------------------------------------------------ /** * Task for refreshing the token. */ private class RefreshTokenTask extends TimerTask { public final void run() { int timeStep = -1; try { timeStep = Integer.parseInt(tfTimeStep.getString()); } catch (NumberFormatException e) { debugErr(e.getMessage()); } int remainSec = -1; if (timeStep > 0) { long delta = DEFAULT_DELTA; try { delta = Long.parseLong(tfDelta.getString()); } catch (NumberFormatException e) { debugErr(e.getMessage()); } final long currentTimeSec = System.currentTimeMillis() / 1000L + delta; final long newCounter = getCounter(currentTimeSec, timeStep); if (cachedCounter != newCounter) { int digits = -1; try { digits = Integer.parseInt(tfDigits.getString()); } catch (NumberFormatException e) { debugErr(e.getMessage()); } siToken.setText(genToken(newCounter, getHMac(), digits)); cachedCounter = newCounter; } if (timeStep == 1 || "".equals(siToken.getText())) { remainSec = IDLE; } else { remainSec = (int) (timeStep - 1 - currentTimeSec % timeStep); } } else { remainSec = 0; siToken.setText(""); cachedCounter = INVALID_COUNTER; } // set values (and repaint) only if needed if (gauValidity.getValue() != remainSec) { gauValidity.setValue(remainSec); } } } }