/* * FreeOTP * * Authors: Nathaniel McCallum <npmccallum@redhat.com> * * Copyright (C) 2013 Nathaniel McCallum, Red Hat * * 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.fedorahosted.freeotp; import java.nio.ByteBuffer; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Locale; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import android.net.Uri; import com.google.android.apps.authenticator.Base32String; import com.google.android.apps.authenticator.Base32String.DecodingException; public class Token { public static class TokenUriInvalidException extends Exception { private static final long serialVersionUID = -1108624734612362345L; } public static enum TokenType { HOTP, TOTP } private String issuerInt; private String issuerExt; private String issuerAlt; private String label; private String labelAlt; private String image; private String imageAlt; private TokenType type; private String algo; private byte[] secret; private int digits; private long counter; private int period; private Token(Uri uri, boolean internal) throws TokenUriInvalidException { if (!uri.getScheme().equals("otpauth")) throw new TokenUriInvalidException(); if (uri.getAuthority().equals("totp")) type = TokenType.TOTP; else if (uri.getAuthority().equals("hotp")) type = TokenType.HOTP; else throw new TokenUriInvalidException(); String path = uri.getPath(); if (path == null) throw new TokenUriInvalidException(); // Strip the path of its leading '/' for (int i = 0; path.charAt(i) == '/'; i++) path = path.substring(1); if (path.length() == 0) throw new TokenUriInvalidException(); int i = path.indexOf(':'); issuerExt = i < 0 ? "" : path.substring(0, i); issuerInt = uri.getQueryParameter("issuer"); label = path.substring(i >= 0 ? i + 1 : 0); algo = uri.getQueryParameter("algorithm"); if (algo == null) algo = "sha1"; algo = algo.toUpperCase(Locale.US); try { Mac.getInstance("Hmac" + algo); } catch (NoSuchAlgorithmException e1) { throw new TokenUriInvalidException(); } try { String d = uri.getQueryParameter("digits"); if (d == null) d = "6"; digits = Integer.parseInt(d); if (digits != 6 && digits != 8) throw new TokenUriInvalidException(); } catch (NumberFormatException e) { throw new TokenUriInvalidException(); } try { String p = uri.getQueryParameter("period"); if (p == null) p = "30"; period = Integer.parseInt(p); } catch (NumberFormatException e) { throw new TokenUriInvalidException(); } if (type == TokenType.HOTP) { try { String c = uri.getQueryParameter("counter"); if (c == null) c = "0"; counter = Long.parseLong(c); } catch (NumberFormatException e) { throw new TokenUriInvalidException(); } } try { String s = uri.getQueryParameter("secret"); secret = Base32String.decode(s); } catch (DecodingException e) { throw new TokenUriInvalidException(); } catch (NullPointerException e) { throw new TokenUriInvalidException(); } image = uri.getQueryParameter("image"); if (internal) { setIssuer(uri.getQueryParameter("issueralt")); setLabel(uri.getQueryParameter("labelalt")); } } private String getHOTP(long counter) { // Encode counter in network byte order ByteBuffer bb = ByteBuffer.allocate(8); bb.putLong(counter); // Create digits divisor int div = 1; for (int i = digits; i > 0; i--) div *= 10; // Create the HMAC try { Mac mac = Mac.getInstance("Hmac" + algo); mac.init(new SecretKeySpec(secret, "Hmac" + algo)); // Do the hashing byte[] digest = mac.doFinal(bb.array()); // Truncate int binary; int off = digest[digest.length - 1] & 0xf; binary = (digest[off] & 0x7f) << 0x18; binary |= (digest[off + 1] & 0xff) << 0x10; binary |= (digest[off + 2] & 0xff) << 0x08; binary |= (digest[off + 3] & 0xff); binary = binary % div; // Zero pad String hotp = Integer.toString(binary); while (hotp.length() != digits) hotp = "0" + hotp; return hotp; } catch (InvalidKeyException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return ""; } public Token(String uri, boolean internal) throws TokenUriInvalidException { this(Uri.parse(uri), internal); } public Token(Uri uri) throws TokenUriInvalidException { this(uri, false); } public Token(String uri) throws TokenUriInvalidException { this(Uri.parse(uri)); } public String getID() { String id; if (issuerInt != null && !issuerInt.equals("")) id = issuerInt + ":" + label; else if (issuerExt != null && !issuerExt.equals("")) id = issuerExt + ":" + label; else id = label; return id; } // NOTE: This changes internal data. You MUST save the token immediately. public void setIssuer(String issuer) { issuerAlt = (issuer == null || issuer.equals(this.issuerExt)) ? null : issuer; } public String getIssuer() { if (issuerAlt != null) return issuerAlt; return issuerExt != null ? issuerExt : ""; } // NOTE: This changes internal data. You MUST save the token immediately. public void setLabel(String label) { labelAlt = (label == null || label.equals(this.label)) ? null : label; } public String getLabel() { if (labelAlt != null) return labelAlt; return label != null ? label : ""; } public int getDigits() { return digits; } // NOTE: This may change internal data. You MUST save the token immediately. public TokenCode generateCodes() { long cur = System.currentTimeMillis(); switch (type) { case HOTP: return new TokenCode(getHOTP(counter++), cur, cur + (period * 1000)); case TOTP: long counter = cur / 1000 / period; return new TokenCode(getHOTP(counter + 0), (counter + 0) * period * 1000, (counter + 1) * period * 1000, new TokenCode(getHOTP(counter + 1), (counter + 1) * period * 1000, (counter + 2) * period * 1000)); } return null; } public TokenType getType() { return type; } public Uri toUri() { String issuerLabel = !issuerExt.equals("") ? issuerExt + ":" + label : label; Uri.Builder builder = new Uri.Builder().scheme("otpauth").path(issuerLabel) .appendQueryParameter("secret", Base32String.encode(secret)) .appendQueryParameter("issuer", issuerInt == null ? issuerExt : issuerInt) .appendQueryParameter("algorithm", algo) .appendQueryParameter("digits", Integer.toString(digits)) .appendQueryParameter("period", Integer.toString(period)); switch (type) { case HOTP: builder.authority("hotp"); builder.appendQueryParameter("counter", Long.toString(counter + 1)); break; case TOTP: builder.authority("totp"); break; } return builder.build(); } @Override public String toString() { return toUri().toString(); } public void setImage(Uri image) { imageAlt = null; if (image == null) return; if (this.image == null || !Uri.parse(this.image).equals(image)) imageAlt = image.toString(); } public Uri getImage() { if (imageAlt != null) return Uri.parse(imageAlt); if (image != null) return Uri.parse(image); return null; } }