com.ledger.android.u2f.bridge.MainActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.ledger.android.u2f.bridge.MainActivity.java

Source

/*
*******************************************************************************
*   Android U2F USB BridgE 
*   (c) 2016 Ledger
*
*  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.ledger.android.u2f.bridge;

import java.util.Iterator;
import java.util.Set;
import java.util.Vector;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.security.MessageDigest;

import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;

import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.ViewGroup;
import android.app.Activity;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import android.util.Log;
import android.util.Base64;

@SuppressLint("NewApi")
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "LedgerU2FBridge";

    private static final String ACTION_GOOGLE = "com.google.android.apps.authenticator.AUTHENTICATE";
    private static final String ACTION_LEDGER = "com.ledger.android.u2f.bridge.AUTHENTICATE";
    private static final String TAG_REQUEST = "request";
    private static final String TAG_RESULT_DATA = "resultData";
    private static final String TAG_JSON_TYPE = "type";
    private static final String TAG_JSON_APPID = "appId";
    private static final String TAG_JSON_CHALLENGE = "challenge";
    private static final String TAG_JSON_REGISTERED_KEYS = "registeredKeys";
    private static final String TAG_JSON_REGISTER_REQUESTS = "registerRequests";
    private static final String TAG_JSON_KEYHANDLE = "keyHandle";
    private static final String TAG_JSON_VERSION = "version";
    private static final String TAG_JSON_REQUESTID = "requestId";
    private static final String TAG_JSON_RESPONSEDATA = "responseData";
    private static final String TAG_JSON_CLIENTDATA = "clientData";
    private static final String TAG_JSON_SIGNATUREDATA = "signatureData";
    private static final String TAG_JSON_REGISTRATIONDATA = "registrationData";
    private static final String TAG_JSON_TYP = "typ";
    private static final String TAG_JSON_ORIGIN = "origin";
    private static final String TAG_JSON_CID_PUBKEY = "cid_pubkey";

    private static final String SIGN_REQUEST_TYPE = "u2f_sign_request";
    private static final String SIGN_RESPONSE_TYPE = "u2f_sign_response";
    private static final String SIGN_RESPONSE_TYP = "navigator.id.getAssertion";
    private static final String REGISTER_REQUEST_TYPE = "u2f_register_request";
    private static final String REGISTER_RESPONSE_TYPE = "u2f_register_response";
    private static final String REGISTER_RESPONSE_TYP = "navigator.id.finishEnrollment";
    private static final String CID_UNAVAILABLE = "unavailable";

    private static final String VERSION_U2F_V2 = "U2F_V2";

    private static final int SW_OK = 0x9000;
    private static final int SW_USER_PRESENCE_REQUIRED = 0x6985;

    private class U2FContext {

        public U2FContext(String appId, byte[] challenge, Vector<byte[]> keyHandles, int requestId, boolean sign) {
            this.appId = appId;
            this.challenge = challenge;
            this.keyHandles = keyHandles;
            this.requestId = requestId;
            this.sign = sign;
        }

        public String getAppId() {
            return appId;
        }

        public byte[] getChallenge() {
            return challenge;
        }

        public Vector<byte[]> getKeyHandles() {
            return keyHandles;
        }

        public void setChosenKeyHandle(byte[] chosenKeyHandle) {
            this.chosenKeyHandle = chosenKeyHandle;
        }

        public byte[] getChosenKeyHandle() {
            return chosenKeyHandle;
        }

        public int getRequestId() {
            return requestId;
        }

        public boolean isSign() {
            return sign;
        }

        private String appId;
        private byte[] challenge;
        private Vector<byte[]> keyHandles;
        private byte[] chosenKeyHandle;
        private int requestId;
        private boolean sign;
    }

    private class U2FAuthRunner extends Thread implements U2FTransportFactoryCallback {

        //private static final int PAUSE = 50;
        private static final int PAUSE = 300;

        private static final int FIDO_CLA = 0x00;
        private static final int FIDO_INS_AUTH = 0x02;
        private static final int FIDO_INS_REGISTER = 0x01;
        private static final int FIDO_P1_SIGN = 0x03;

        private U2FContext context;
        private U2FTransportAndroid transportBuilder;
        private boolean stopped;

        public U2FAuthRunner(U2FContext context) {
            this.context = context;
            transportBuilder = new U2FTransportAndroid(MainActivity.this);
        }

        public void markStopped() {
            stopped = true;
            transportBuilder.markStopped();
        }

        private boolean isResponseOK(byte[] response) {
            if ((response == null) || (response.length < 2)) {
                return false;
            }
            int sw = ((response[response.length - 2] & 0xff) << 8) | (response[response.length - 1] & 0xff);
            return sw == SW_OK;
        }

        private boolean isResponseBusy(byte[] response) {
            if ((response == null) || (response.length < 2)) {
                return false;
            }
            int sw = ((response[response.length - 2] & 0xff) << 8) | (response[response.length - 1] & 0xff);
            return sw == SW_USER_PRESENCE_REQUIRED;
        }

        private byte[] processSign(U2FTransportAndroidHID transport) throws Exception {
            byte[] response = null;
            choiceLoop: for (byte[] keyHandle : context.getKeyHandles()) {
                if (stopped) {
                    break;
                }
                for (;;) {
                    if (stopped) {
                        break;
                    }
                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
                    int msgLength = 32 + 32 + 1 + keyHandle.length;
                    bos.write(FIDO_CLA);
                    bos.write(FIDO_INS_AUTH);
                    bos.write(FIDO_P1_SIGN);
                    bos.write(0x00); // p2
                    bos.write(0x00); // extended length
                    bos.write(msgLength >> 8);
                    bos.write(msgLength & 0xff);
                    MessageDigest digest = MessageDigest.getInstance("SHA-256");
                    bos.write(digest.digest(MainActivity.this.createClientData(context).getBytes("UTF-8")));
                    bos.write(digest.digest(context.getAppId().getBytes("UTF-8")));
                    bos.write(keyHandle.length);
                    bos.write(keyHandle);
                    bos.write(0x00);
                    bos.write(0x00);
                    byte[] authApdu = bos.toByteArray();
                    response = transport.exchange(authApdu);
                    if (isResponseOK(response)) {
                        context.setChosenKeyHandle(keyHandle);
                        break choiceLoop;
                    }
                    if (!isResponseBusy(response)) {
                        break;
                    } else {
                        response = null;
                        Thread.sleep(PAUSE);
                    }
                }
            }
            return response;
        }

        private byte[] processRegister(U2FTransportAndroidHID transport) throws Exception {
            byte[] response = null;
            for (;;) {
                if (stopped) {
                    break;
                }
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                int msgLength = 32 + 32;
                bos.write(FIDO_CLA);
                bos.write(FIDO_INS_REGISTER);
                bos.write(0x00); // p1
                bos.write(0x00); // p2
                bos.write(0x00); // extended length
                bos.write(msgLength >> 8);
                bos.write(msgLength & 0xff);
                MessageDigest digest = MessageDigest.getInstance("SHA-256");
                bos.write(digest.digest(MainActivity.this.createClientData(context).getBytes("UTF-8")));
                bos.write(digest.digest(context.getAppId().getBytes("UTF-8")));
                bos.write(0x00);
                bos.write(0x00);
                byte[] authApdu = bos.toByteArray();
                response = transport.exchange(authApdu);
                if (isResponseOK(response)) {
                    break;
                }
                if (isResponseBusy(response)) {
                    response = null;
                    Thread.sleep(200);
                } else {
                    response = null;
                    break;
                }
            }
            return response;
        }

        public void onConnected(boolean success) {
            byte[] response = null;
            if (success) {
                U2FTransportAndroidHID transport = transportBuilder.getTransport();
                try {
                    transport.setDebug(true);
                    transport.init();
                    if (context.isSign()) {
                        response = processSign(transport);
                    } else {
                        response = processRegister(transport);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    response = null;
                }
                try {
                    transport.close();
                } catch (IOException e) {
                }
            } else {
            }
            final byte[] sentResponse = (isResponseOK(response) ? response : null);
            MainActivity.this.runOnUiThread(new Runnable() {
                public void run() {
                    MainActivity.this.postResponse(sentResponse);
                }
            });
        }

        public void run() {
            while (!transportBuilder.isPluggedIn() && !stopped) {
                try {
                    Thread.sleep(PAUSE);
                } catch (InterruptedException e) {
                }
            }
            if (stopped) {
                return;
            }
            transportBuilder.connect(MainActivity.this, this);
        }
    }

    private Button mCancelButton;
    private U2FContext mU2FContext;
    private U2FAuthRunner mAuthThread;

    private U2FContext parseU2FContext(String data) {
        try {
            JSONObject json = new JSONObject(data);
            String requestType = json.getString(TAG_JSON_TYPE);
            if (requestType.equals(SIGN_REQUEST_TYPE)) {
                return parseU2FContextSign(json);
            } else if (requestType.equals(REGISTER_REQUEST_TYPE)) {
                return parseU2FContextRegister(json);
            } else {
                Log.e(TAG, "Invalid request type");
                return null;
            }
        } catch (JSONException e) {
            Log.e(TAG, "Error decoding request");
            return null;
        }

    }

    private U2FContext parseU2FContextSign(JSONObject json) {
        try {
            String appId = json.getString(TAG_JSON_APPID);
            byte[] challenge = Base64.decode(json.getString(TAG_JSON_CHALLENGE), Base64.URL_SAFE);
            int requestId = json.getInt(TAG_JSON_REQUESTID);
            JSONArray array = json.getJSONArray(TAG_JSON_REGISTERED_KEYS);
            Vector<byte[]> keyHandles = new Vector<byte[]>();
            for (int i = 0; i < array.length(); i++) {
                JSONObject keyHandleItem = array.getJSONObject(i);
                if (!keyHandleItem.getString(TAG_JSON_VERSION).equals(VERSION_U2F_V2)) {
                    Log.e(TAG, "Invalid handle version");
                    return null;
                }
                byte[] keyHandle = Base64.decode(keyHandleItem.getString(TAG_JSON_KEYHANDLE), Base64.URL_SAFE);
                keyHandles.add(keyHandle);
            }
            return new U2FContext(appId, challenge, keyHandles, requestId, true);
        } catch (JSONException e) {
            Log.e(TAG, "Error decoding request");
            return null;
        }
    }

    private U2FContext parseU2FContextRegister(JSONObject json) {
        try {
            byte[] challenge = null;
            String appId = json.getString(TAG_JSON_APPID);
            int requestId = json.getInt(TAG_JSON_REQUESTID);
            JSONArray array = json.getJSONArray(TAG_JSON_REGISTER_REQUESTS);
            for (int i = 0; i < array.length(); i++) {
                // TODO : only handle USB transport if several are present
                JSONObject registerItem = array.getJSONObject(i);
                if (!registerItem.getString(TAG_JSON_VERSION).equals(VERSION_U2F_V2)) {
                    Log.e(TAG, "Invalid register version");
                    return null;
                }
                challenge = Base64.decode(registerItem.getString(TAG_JSON_CHALLENGE), Base64.URL_SAFE);
            }
            return new U2FContext(appId, challenge, null, requestId, false);
        } catch (JSONException e) {
            Log.e(TAG, "Error decoding request");
            return null;
        }
    }

    private String createU2FResponse(U2FContext context, byte[] data) {
        if (context.isSign()) {
            return createU2FResponseSign(context, data);
        } else {
            return createU2FResponseRegister(context, data);
        }
    }

    private String createClientData(U2FContext context) {
        try {
            JSONObject clientData = new JSONObject();
            clientData.put(TAG_JSON_TYP, (context.isSign() ? SIGN_RESPONSE_TYP : REGISTER_RESPONSE_TYP));
            clientData.put(TAG_JSON_CHALLENGE, Base64.encodeToString(context.getChallenge(),
                    Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING));
            clientData.put(TAG_JSON_ORIGIN, context.getAppId());
            clientData.put(TAG_JSON_CID_PUBKEY, CID_UNAVAILABLE);
            return clientData.toString();
        } catch (Exception e) {
            Log.e(TAG, "Error encoding client data");
            return null;
        }
    }

    private String createU2FResponseSign(U2FContext context, byte[] signature) {
        try {
            JSONObject response = new JSONObject();
            response.put(TAG_JSON_TYPE, SIGN_RESPONSE_TYPE);
            response.put(TAG_JSON_REQUESTID, context.getRequestId());
            JSONObject responseData = new JSONObject();
            responseData.put(TAG_JSON_KEYHANDLE, Base64.encodeToString(context.getChosenKeyHandle(),
                    Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING));
            responseData.put(TAG_JSON_SIGNATUREDATA, Base64.encodeToString(signature, 0, signature.length - 2,
                    Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING));
            String clientData = createClientData(context);
            responseData.put(TAG_JSON_CLIENTDATA, Base64.encodeToString(clientData.getBytes("UTF-8"),
                    Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING));
            response.put(TAG_JSON_RESPONSEDATA, responseData);
            return response.toString();
        } catch (Exception e) {
            Log.e(TAG, "Error encoding request");
            return null;
        }
    }

    private String createU2FResponseRegister(U2FContext context, byte[] registerResponse) {
        try {
            JSONObject response = new JSONObject();
            response.put(TAG_JSON_TYPE, REGISTER_RESPONSE_TYPE);
            response.put(TAG_JSON_REQUESTID, context.getRequestId());
            JSONObject responseData = new JSONObject();
            responseData.put(TAG_JSON_REGISTRATIONDATA, Base64.encodeToString(registerResponse, 0,
                    registerResponse.length - 2, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING));
            responseData.put(TAG_JSON_VERSION, VERSION_U2F_V2);
            String clientData = createClientData(context);
            responseData.put(TAG_JSON_CLIENTDATA, Base64.encodeToString(clientData.getBytes("UTF-8"),
                    Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING));
            response.put(TAG_JSON_RESPONSEDATA, responseData);
            return response.toString();
        } catch (Exception e) {
            Log.e(TAG, "Error encoding request");
            return null;
        }
    }

    public void postResponse(byte[] responseData) {
        if (responseData == null) {
            finish();
            return;
        }
        String response = createU2FResponse(mU2FContext, responseData);
        if (response == null) {
            finish();
            return;
        }
        Intent intent = getIntent();
        intent.putExtra(TAG_RESULT_DATA, response);
        setResult(Activity.RESULT_OK, intent);
        finish();
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (mAuthThread != null) {
            mAuthThread.markStopped();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        mCancelButton = (Button) findViewById(R.id.cancel_button);
        mCancelButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mAuthThread != null) {
                    mAuthThread.markStopped();
                }
                finish();
            }
        });

        TypefaceHelper.applyFonts((ViewGroup) getWindow().getDecorView());

        Intent intent = getIntent();

        if (!intent.getAction().equals(ACTION_GOOGLE) && !intent.getAction().equals(ACTION_LEDGER)) {
            Toast.makeText(MainActivity.this, R.string.unsupported_intent, Toast.LENGTH_LONG).show();
            finish();
            return;
        }

        String request = intent.getStringExtra(TAG_REQUEST);
        if (request == null) {
            Log.e(TAG, "Request missing");
            finish();
            return;
        }

        mU2FContext = parseU2FContext(request);
        if (mU2FContext == null) {
            finish();
            return;
        }

        if (mAuthThread != null) {
            mAuthThread.markStopped();
        }
        mAuthThread = new U2FAuthRunner(mU2FContext);
        mAuthThread.start();
    }

}