com.google.authenticator.blackberry.AuthenticatorScreen.java Source code

Java tutorial

Introduction

Here is the source code for com.google.authenticator.blackberry.AuthenticatorScreen.java

Source

/*-
 * Copyright 2010 Google Inc.
 *
 * 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 com.google.authenticator.blackberry;

import net.rim.blackberry.api.browser.Browser;
import net.rim.blackberry.api.browser.BrowserSession;
import net.rim.device.api.i18n.ResourceBundle;
import net.rim.device.api.system.Alert;
import net.rim.device.api.system.Application;
import net.rim.device.api.system.ApplicationDescriptor;
import net.rim.device.api.ui.MenuItem;
import net.rim.device.api.ui.Screen;
import net.rim.device.api.ui.UiApplication;
import net.rim.device.api.ui.component.LabelField;
import net.rim.device.api.ui.component.Menu;
import net.rim.device.api.ui.component.RichTextField;
import net.rim.device.api.ui.container.MainScreen;

import org.bouncycastle.crypto.Mac;
import org.bouncycastle.crypto.digests.SHA1Digest;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;

import com.google.authenticator.blackberry.AccountDb.OtpType;
import com.google.authenticator.blackberry.Base32String.DecodingException;
import com.google.authenticator.blackberry.resource.AuthenticatorResource;

/**
 * BlackBerry port of {@code AuthenticatorActivity}.
 */
public class AuthenticatorScreen extends MainScreen implements UpdateCallback, AuthenticatorResource, Runnable {

    private static ResourceBundle sResources = ResourceBundle.getBundle(BUNDLE_ID, BUNDLE_NAME);

    private static final int VIBRATE_DURATION = 200;

    private static final long REFRESH_INTERVAL = 30 * 1000;

    private static final boolean AUTO_REFRESH = true;

    private static final String TERMS_URL = "http://www.google.com/accounts/TOS";

    private static final String PRIVACY_URL = "http://www.google.com/mobile/privacy.html";

    /**
     * Computes the one-time PIN given the secret key.
     * 
     * @param secret
     *          the secret key
     * @return the PIN
     * @throws GeneralSecurityException
     * @throws DecodingException
     *           If the key string is improperly encoded.
     */
    public static String computePin(String secret, Long counter) {
        try {
            final byte[] keyBytes = Base32String.decode(secret);
            Mac mac = new HMac(new SHA1Digest());
            mac.init(new KeyParameter(keyBytes));
            PasscodeGenerator pcg = new PasscodeGenerator(mac);
            if (counter == null) { // time-based totp
                return pcg.generateTimeoutCode();
            } else { // counter-based hotp
                return pcg.generateResponseCode(counter.longValue());
            }
        } catch (RuntimeException e) {
            return "General security exception";
        } catch (DecodingException e) {
            return "Decoding exception";
        }
    }

    /**
     * Parses a secret value from a URI. The format will be:
     * 
     * <pre>
     * https://www.google.com/accounts/KeyProv?user=username#secret 
     *   OR
     * totp://username@domain#secret  
     * otpauth://totp/user@example.com?secret=FFF...
     * otpauth://hotp/user@example.com?secret=FFF...&amp;counter=123
     * </pre>
     * 
     * @param uri The URI containing the secret key
     */
    void parseSecret(Uri uri) {
        String scheme = uri.getScheme().toLowerCase();
        String path = uri.getPath();
        String authority = uri.getAuthority();
        String user = DEFAULT_USER;
        String secret;
        AccountDb.OtpType type = AccountDb.OtpType.TOTP;
        Integer counter = new Integer(0); // only interesting for HOTP
        if (OTP_SCHEME.equals(scheme)) {
            if (authority != null && authority.equals(TOTP)) {
                type = AccountDb.OtpType.TOTP;
            } else if (authority != null && authority.equals(HOTP)) {
                type = AccountDb.OtpType.HOTP;
                String counterParameter = uri.getQueryParameter(COUNTER_PARAM);
                if (counterParameter != null) {
                    counter = Integer.valueOf(counterParameter);
                }
            }

            if (path != null && path.length() > 1) {
                user = path.substring(1); // path is "/user", so remove leading /
            }

            secret = uri.getQueryParameter(SECRET_PARAM);
            // TODO: remove TOTP scheme
        } else if (TOTP.equals(scheme)) {
            if (authority != null) {
                user = authority;
            }
            secret = uri.getFragment();
        } else { // https://www.google.com... URI format
            String userParam = uri.getQueryParameter(USER_PARAM);
            if (userParam != null) {
                user = userParam;
            }
            secret = uri.getFragment();
        }

        if (secret == null) {
            // Secret key not found in URI
            return;
        }

        // TODO: April 2010 - remove version parameter handling.
        String version = uri.getQueryParameter(VERSION_PARAM);
        if (version == null) { // version is null for legacy URIs
            try {
                secret = Base32String.encode(Base32Legacy.decode(secret));
            } catch (DecodingException e) {
                // Error decoding legacy key from URI
                e.printStackTrace();
            }
        }

        if (!secret.equals(getSecret(user)) || counter != AccountDb.getCounter(user)
                || type != AccountDb.getType(user)) {
            saveSecret(user, secret, null, type);
            mStatusText.setText(sResources.getString(SECRET_SAVED));
        }
    }

    static String getSecret(String user) {
        return AccountDb.getSecret(user);
    }

    static void saveSecret(String user, String secret, String originalUser, AccountDb.OtpType type) {
        if (originalUser == null) {
            originalUser = user;
        }
        if (secret != null) {
            AccountDb.update(user, secret, originalUser, type);
            Alert.startVibrate(VIBRATE_DURATION);
        }
    }

    private LabelField mVersionText;
    private LabelField mStatusText;
    private RichTextField mEnterPinTextView;
    private PinListField mUserList;
    private PinListFieldCallback mUserAdapter;
    private PinInfo[] mUsers = {};
    private boolean mUpdateAvailable;
    private int mTimer = -1;

    static final String DEFAULT_USER = "Default account";
    private static final String OTP_SCHEME = "otpauth";
    private static final String TOTP = "totp"; // time-based
    private static final String HOTP = "hotp"; // counter-based
    private static final String USER_PARAM = "user";
    private static final String SECRET_PARAM = "secret";
    private static final String VERSION_PARAM = "v";
    private static final String COUNTER_PARAM = "counter";

    public AuthenticatorScreen() {
        setTitle(sResources.getString(APP_NAME));
        // LabelField cannot scroll content that is bigger than the screen,
        // so use RichTextField instead.
        mEnterPinTextView = new RichTextField(sResources.getString(ENTER_PIN));

        mUserList = new PinListField();
        mUserAdapter = new PinListFieldCallback(mUsers);
        setAdapter();

        ApplicationDescriptor applicationDescriptor = ApplicationDescriptor.currentApplicationDescriptor();
        String version = applicationDescriptor.getVersion();
        mVersionText = new LabelField(version, FIELD_RIGHT | FIELD_BOTTOM);
        mStatusText = new LabelField("", FIELD_HCENTER | FIELD_BOTTOM);

        add(mEnterPinTextView);
        add(mUserList);
        add(new LabelField(" ")); // One-line spacer
        add(mStatusText);
        add(mVersionText);

        FieldUtils.setVisible(mEnterPinTextView, false);

        UpdateCallback callback = this;
        new UpdateTask(callback).start();
    }

    private void setAdapter() {
        int lastIndex = mUserList.getSelectedIndex();
        mUserList.setCallback(mUserAdapter);
        mUserList.setSize(mUsers.length);
        mUserList.setRowHeight(mUserAdapter.getRowHeight());
        mUserList.setSelectedIndex(lastIndex);
    }

    /**
     * {@inheritDoc}
     */
    protected void onDisplay() {
        super.onDisplay();
        onResume();
    }

    /**
     * {@inheritDoc}
     */
    protected void onExposed() {
        super.onExposed();
        onResume();
    }

    /**
     * {@inheritDoc}
     */
    protected void onObscured() {
        onPause();
        super.onObscured();
    }

    private void onResume() {
        refreshUserList();
        if (AUTO_REFRESH) {
            startTimer();
        }
    }

    private void onPause() {
        if (isTimerSet()) {
            stopTimer();
        }
    }

    private boolean isTimerSet() {
        return mTimer != -1;
    }

    private void startTimer() {
        if (isTimerSet()) {
            stopTimer();
        }
        Application application = getApplication();
        Runnable runnable = this;
        boolean repeat = true;
        mTimer = application.invokeLater(runnable, REFRESH_INTERVAL, repeat);
    }

    private void stopTimer() {
        if (isTimerSet()) {
            Application application = getApplication();
            application.cancelInvokeLater(mTimer);
            mTimer = -1;
        }
    }

    /**
     * {@inheritDoc}
     */
    public void run() {
        refreshUserList();
    }

    void refreshUserList() {
        String[] cursor = AccountDb.getNames();
        if (cursor.length > 0) {
            if (mUsers.length != cursor.length) {
                mUsers = new PinInfo[cursor.length];
            }
            for (int i = 0; i < cursor.length; i++) {
                String user = cursor[i];
                computeAndDisplayPin(user, i, false);
            }
            mUserAdapter = new PinListFieldCallback(mUsers);
            setAdapter(); // force refresh of display

            if (!FieldUtils.isVisible(mUserList)) {
                mEnterPinTextView.setText(sResources.getString(ENTER_PIN));
                FieldUtils.setVisible(mEnterPinTextView, true);
                FieldUtils.setVisible(mUserList, true);
            }
        } else {
            // If the user started up this app but there is no secret key yet,
            // then tell the user to visit a web page to get the secret key.
            mUsers = new PinInfo[0]; // clear any existing user PIN state
            tellUserToGetSecretKey();
        }
    }

    /**
     * Tells the user to visit a web page to get a secret key.
     */
    private void tellUserToGetSecretKey() {
        // TODO: fill this in with code to send our phone number to the server
        String notInitialized = sResources.getString(NOT_INITIALIZED);
        mEnterPinTextView.setText(notInitialized);
        FieldUtils.setVisible(mEnterPinTextView, true);
        FieldUtils.setVisible(mUserList, false);
    }

    /**
     * Computes the PIN and saves it in mUsers. This currently runs in the UI
     * thread so it should not take more than a second or so. If necessary, we can
     * move the computation to a background thread.
     * 
     * @param user the user email to display with the PIN
     * @param position the index for the screen of this user and PIN
     * @param computeHotp true if we should increment counter and display new hotp
     * 
     * @return the generated PIN
     */
    String computeAndDisplayPin(String user, int position, boolean computeHotp) {
        OtpType type = AccountDb.getType(user);
        String secret = getSecret(user);
        PinInfo currentPin;
        if (mUsers[position] != null) {
            currentPin = mUsers[position]; // existing PinInfo, so we'll update it
        } else {
            currentPin = new PinInfo();
            currentPin.mPin = sResources.getString(EMPTY_PIN);
        }

        currentPin.mUser = user;
        if (type == OtpType.TOTP) {
            currentPin.mPin = computePin(secret, null);
        } else if (type == OtpType.HOTP) {
            currentPin.mIsHotp = true;
            if (computeHotp) {
                AccountDb.incrementCounter(user);
                Integer counter = AccountDb.getCounter(user);
                currentPin.mPin = computePin(secret, new Long(counter.longValue()));
            }
        }
        mUsers[position] = currentPin;
        return currentPin.mPin;
    }

    private void pushScreen(Screen screen) {
        UiApplication app = (UiApplication) getApplication();
        app.pushScreen(screen);
    }

    /**
     * {@inheritDoc}
     */
    public Menu getMenu(int instance) {
        if (instance == Menu.INSTANCE_CONTEXT) {
            // Show the full menu instead of the context menu 
            return super.getMenu(Menu.INSTANCE_DEFAULT);
        } else {
            return super.getMenu(instance);
        }
    }

    /**
     * {@inheritDoc}
     */
    protected void makeMenu(Menu menu, int instance) {
        super.makeMenu(menu, instance);
        MenuItem enterKeyItem = new MenuItem(sResources, ENTER_KEY_MENU_ITEM, 0, 0) {
            public void run() {
                pushScreen(new EnterKeyScreen());
            }
        };
        MenuItem termsItem = new MenuItem(sResources, TERMS_MENU_ITEM, 0, 0) {
            public void run() {
                BrowserSession session = Browser.getDefaultSession();
                session.displayPage(TERMS_URL);
            }
        };
        MenuItem privacyItem = new MenuItem(sResources, PRIVACY_MENU_ITEM, 0, 0) {
            public void run() {
                BrowserSession session = Browser.getDefaultSession();
                session.displayPage(PRIVACY_URL);
            }
        };
        menu.add(enterKeyItem);
        if (!isTimerSet()) {
            MenuItem refreshItem = new MenuItem(sResources, REFRESH_MENU_ITEM, 0, 0) {
                public void run() {
                    refreshUserList();
                }
            };
            menu.add(refreshItem);
        }
        if (mUpdateAvailable) {
            MenuItem updateItem = new MenuItem(sResources, UPDATE_NOW, 0, 0) {
                public void run() {
                    BrowserSession session = Browser.getDefaultSession();
                    session.displayPage(Build.DOWNLOAD_URL);
                    mStatusText.setText("");
                }
            };
            menu.add(updateItem);
        }
        menu.add(termsItem);
        menu.add(privacyItem);
    }

    /**
     * {@inheritDoc}
     */
    public void onUpdate(String version) {
        String status = sResources.getString(UPDATE_AVAILABLE) + ": " + version;
        mStatusText.setText(status);
        mUpdateAvailable = true;
    }
}