public interface MainActivityInterface {

    void makeAlertDialog(int titleID, String message);

    void makeAlertDialog(int titleID, int messageID);

    void makeAlertDialog(int titleID, String message, int positiveBtnTextID, boolean cancelable,
                         @Nullable DialogInterface.OnClickListener positiveBtnListener);

    void makeAlertDialog(int titleID, int messageID, int positiveBtnTextID, boolean cancelable,
                         @Nullable DialogInterface.OnClickListener positiveBtnListener);

    void makeDeviceNotSupportedDialog(Exception e);

    void makeToast(String message);

    void makeToast(int resID);

    void setStatusDialogText(String text);

    void setStatusDialogText(int id);

    void cancelStatusDialog();

    void getFirebaseTokenForPushRollout(Token token);

    void firebaseInit(FirebaseInitConfig firebaseInitConfig);

    void removeFirebase();

    void startTimer();

    void stopTimer();

    void resumeTimer();

    SecretKeyWrapper getWrapper();

    PublicKey generatePublicKeyFor(String alias);

    String getStringResource(int id);

    void cancelNotification(int notificationID);
} public interface PresenterInterface {
    /**
     * Initialization which is called directly after instantiation in OnCreate of the Activity.
     */
    void init();

    void scanQRfinished(ScanResult result);

    void addTokenFromIntent(String type, byte[] secret, String serial, int digits, String algorithm, String period, boolean withPIN);

    void cancelAuthentication(Token token);

    /**
     * Add a Push Authentication Request to the corresponding Token.
     * Finds the token by serial and includes a check for duplicates.
     *
     * @param request request that should be added
     */
    void addPushAuthRequest(PushAuthRequest request);

    /**
     * Passing the Lifecycle event, so the presenter can act on it.
     */
    void onResume();

    /**
     * Passing the Lifecycle event, so the presenter can act on it.
     */
    void onPause();

    /**
     * Passing the Lifecycle event, so the presenter can act on it.
     */
    void onStop();

    /**
     * Set the currently selected token to the given position in the list. Actions selected in ActionMode will be done with this token.
     *
     * @param position index position in the list
     */
    void setCurrentSelection(int position);

    void saveTokenlist();

    boolean isCurrentSelectionWithPin();

    boolean isCurrentSelectionPersistent();

    boolean isCurrentSelectionLocked();

    void removeCurrentSelection();

    void removeCurrentAuthRequest(Token token);

    String getCurrentSelectionLabel();

    String getCurrentSelectionOTP();

    Token getCurrentSelection();

    void setCurrentSelectionLabel(String label);

    void changeCurrentSelectionPIN(int pin);

    Token getTokenAtPosition(int position);

    int getTokenCount();

    void addTokenAt(int position, Token token);

    void addToken(Token token);

    Token removeTokenAtPosition(int position);

    void startPushAuthentication(Token token);

    void startPushRolloutForPosition(int position);

    void increaseHOTPCounter(Token token);

    boolean checkPIN(String input, Token token);

    void setPIN(String input, Token token);

    void timerProgress(int progress);

    void firebaseTokenReceived(String fbtoken, Token token);

    /**
     * Remove the Push Authentication Request, because it was finished by the Service while the App was running.
     * The Request is identified by the notificationID and signature.
     *
     * @param notificationID the ID of the notification
     * @param signature      the signature of the Request
     */
    void pushAuthFinishedFor(String serial, int notificationID, String signature, boolean success);

    /**
     * Indicates that a broadcast from the PushAuthService was received to change the state of a token to "authenticating" and updating the UI
     *
     * @param serial         serial of the token
     * @param notificationID notificationID of the notification
     * @param signature      signature of the PushAuthRequest
     */
    void pushAuthStartedFor(String serial, int notificationID, String signature);
} public interface PresenterTaskInterface {
    void updateTaskStatus(int statusCode, Token token);

    void twoStepFinished(String messageToShow);

    PublicKey generatePublicKeyFor(String serial);

    void receivePublicKey(String key, Token token);
} public interface PresenterUtilInterface {
    SecretKeyWrapper getWrapper();
} public interface PushAuthCallbackInterface {
    void authenticationFinished(boolean success, Token token);

    void handleAuthError(int statusCode, Token token);
} public interface TokenListViewInterface {
    void updateProgressbars(int progress);

    void notifyChange();

    void removeProgressbar(int position);
} public class FirebaseInitConfig {
    private String appID;
    private String api_key;
    private String projNumber;
    private String projID;

    public FirebaseInitConfig(String projID, String appID, String api_key, String projNumber) {
        this.projID = projID;
        this.appID = appID;
        this.api_key = api_key;
        this.projNumber = projNumber;
    }

    public String getProjID() {
        return projID;
    }

    public void setProjID(String projID) {
        this.projID = projID;
    }

    public String getAppID() {
        return appID;
    }

    public void setAppID(String appID) {
        this.appID = appID;
    }

    public String getApiKey() {
        return api_key;
    }

    public void setApi_key(String api_key) {
        this.api_key = api_key;
    }

    public String getProjNumber() {
        return projNumber;
    }

    public void setProjNumber(String projNumber) {
        this.projNumber = projNumber;
    }
} public class Model {
    private ArrayList<Token> tokens;
    private Token currentSelection;

    public Model() {
        this.tokens = new ArrayList<>();
    }

    public Model(ArrayList<Token> tokenlist) {
        if (tokenlist == null) {
            this.tokens = new ArrayList<>();
        } else {
            this.tokens = tokenlist;
        }
    }

    public void setCurrentSelection(int position) {
        if (position == -1) this.currentSelection = null;
        else
            this.currentSelection = tokens.get(position);
    }

    /**
     * Checks for Pushtoken whose rollout time has expired.
     *
     * @return String with the expired tokens serials or null if there are none
     */
    public String checkForExpiredTokens() {
        ArrayList<Token> upForDeletion = new ArrayList<>();
        Date now = new Date();
        for (Token t : tokens) {
            if (t.getType().equals(PUSH)) {
                if (t.state.equals(UNFINISHED) && t.rollout_expiration.before(now)) {
                    upForDeletion.add(t);
                }
            }
        }

        if (!upForDeletion.isEmpty()) {
            StringBuilder sb = new StringBuilder();
            for (Token t : upForDeletion) {
                sb.append(t.getSerial()).append("\n");
                tokens.remove(t);
            }
            return sb.toString();
        } else {
            return null;
        }
    }

    public boolean hasPushToken() {
        for (Token t : tokens) {
            if (t.getType().equals(PUSH)) {
                return true;
            }
        }
        return false;
    }

    public Token getCurrentSelection() {
        return currentSelection;
    }

    public Token getTokenBySerial(String serial) {
        for (Token t : tokens) {
            if(t.getSerial().equals(serial)) {
                return t;
            }
        }
        return null;
    }

    public ArrayList<Token> getTokens() {
        return tokens;
    }
} public class PushAuthRequest {
    private String nonce, url, serial, question, title, signature;
    private boolean sslVerify;
    private Date expiration;
    private int notificationID;

    public PushAuthRequest(String nonce, String url, String serial, String question, String title, String signature, int notificationID, boolean sslVerify) {
        this.nonce = nonce;
        this.url = url;
        this.serial = serial;
        this.question = question;
        this.title = title;
        this.signature = signature;
        this.sslVerify = sslVerify;
        // Add expiration time which is +2 Minutes
        Calendar now = Calendar.getInstance();
        now.add(Calendar.MINUTE, 2);
        this.expiration = now.getTime();
        this.notificationID = notificationID;
    }

    public int getNotificationID() {
        return notificationID;
    }

    public Date getExpiration() {
        return expiration;
    }

    public String getNonce() {
        return nonce;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getSerial() {
        return serial;
    }

    public void setSerial(String serial) {
        this.serial = serial;
    }

    public String getQuestion() {
        return question;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getSignature() {
        return signature;
    }

    public boolean isSslVerify() {
        return sslVerify;
    }
} public class ScanResult {
    // BASE
   public String type, serial, label;

    // NORMAL TOKEN
    public String secret, algorithm = "sha1";              // Default
    public int digits = 6, period = 30, counter = 1;       // Defaults
    public boolean pin = false, persistent = false, taptoshow = false;

    // TWO STEP TOKEN (addition)
    public boolean do2Step = false;
    public int phonepartlength = 10;                       // in bytes
    public int iterations = 10000;
    public int output_size = 160;                          // in bits

    // PUSH TOKEN
    public String rollout_url, enrollment_credential;
    public int ttl = 10;                                   // 10 Minutes is the default
    public FirebaseInitConfig firebaseInitConfig;
    public int push_version = 1;
    public boolean sslverify = true;

    public ScanResult(String type, String serial) {
        this.type = type;
        this.serial = serial;
    }
} public class Token {

    private String currentOTP;
    private byte[] secret;
    private String label;
    private String type;
    private int digits;
    private int period;
    private String algorithm = "HmacSHA1"; //default is SHA1
    private int counter;
    private boolean withPIN = false;
    private boolean isLocked = false;
    private String pin = "";
    private boolean withTapToShow = false;
    private boolean tapped = false;
    private boolean persistent = false;
    private String serial;

    public String enrollment_credential;
    public Date rollout_expiration;
    public String rollout_url;
    public boolean sslVerify = true;
    public boolean lastAuthHadError = false;

    private ArrayList<PushAuthRequest> pendingAuths = new ArrayList<>();
    public State state = UNFINISHED;

    public Token(byte[] secret, String serial, String label, String type, int digits) {
        this.secret = secret;
        this.serial = serial;
        this.label = label;
        this.type = type;
        this.digits = digits;
        this.period = this.type.equals(TOTP) ? 30 : 0;
        this.counter = 0;
    }

    public ArrayList<PushAuthRequest> getPendingAuths() {
        return pendingAuths;
    }

    /**
     * Add the request if it is not yet present. Comparison is by notificationID and signature.
     *
     * @param request Request that should be added
     * @return true if successful, false if not (duplicate)
     */
    public boolean addPushAuthRequest(PushAuthRequest request) {
        for (PushAuthRequest req : pendingAuths) {
            if (req.getNotificationID() == request.getNotificationID()
                    && req.getSignature().equals(request.getSignature())) {
                return false;
            }
        }
        pendingAuths.add(request);
        return true;
    }

    public void setPendingAuths(ArrayList<PushAuthRequest> pendingAuths) {
        this.pendingAuths = pendingAuths;
    }

    // A push token only contains the serial and a label
    public Token(String serial, String label) {
        type = PUSH;
        this.serial = serial;
        this.label = label;
    }

    public String getSerial() {
        return serial;
    }

    public void setTapped(boolean tapped) {
        this.tapped = tapped;
    }

    public boolean isTapped() {
        return tapped;
    }

    public boolean isWithTapToShow() {
        return withTapToShow;
    }

    public void setWithTapToShow(boolean withTapToShow) {
        this.withTapToShow = withTapToShow;
    }

    public String getPin() {
        return pin;
    }

    public void setPin(String pin) {
        this.pin = pin;
    }

    public boolean isLocked() {
        return isLocked;
    }

    public void setLocked(boolean locked) {
        isLocked = locked;
    }

    public void setWithPIN(boolean withPIN) {
        this.isLocked = withPIN;
        this.withPIN = withPIN;
    }

    public boolean isWithPIN() {
        return withPIN;
    }

    public void setPeriod(int period) {
        this.period = period;
    }

    public int getCounter() {
        return counter;
    }

    public void setCounter(int counter) {
        this.counter = counter;
    }

    public void setSecret(byte[] secret) {
        this.secret = secret;
    }

    public byte[] getSecret() {
        return secret;
    }

    public void setLabel(String label) {
        this.label = label;
    }

    public String getLabel() {
        return label;
    }

    public String getType() {
        return type;
    }

    public void setAlgorithm(String algorithm) {
        // In the KeyURI the parameter is sha1/sha256/sha512, whereas the Mac instance is HmacSHA1 etc.
        if (algorithm.startsWith("sha") || algorithm.startsWith("SHA")) {
            this.algorithm = "Hmac" + algorithm.toUpperCase();
        } else if (algorithm.startsWith("Hmac")) {
            this.algorithm = algorithm;
        }
    }

    public int getDigits() {
        return digits;
    }

    public int getPeriod() {
        return period;
    }

    public String getAlgorithm() {
        return algorithm;
    }

    public String getCurrentOTP() {
        return currentOTP;
    }

    public void setCurrentOTP(String currentOTP) {
        this.currentOTP = currentOTP;
    }

    public boolean isPersistent() {
        return persistent;
    }

    public void setPersistent(boolean val) {
        persistent = val;
    }
} public class PushAuthTask extends AsyncTask<Void, Integer, Boolean> implements EndpointCallback {

    private String nonce, endpoint_url, serial, question, title, signature;
    private PublicKey piPublicKey;
    private PrivateKey appPrivateKey;
    private boolean sslVerify;
    private PushAuthCallbackInterface pushAuthCallbackInterface;
    private boolean success;
    private Token token;

    public PushAuthTask(Token token, PushAuthRequest pushAuthRequest,
                        PublicKey piPublicKey, PrivateKey appPrivateKey, PushAuthCallbackInterface pushAuthCallbackInterface) {
        this.nonce = pushAuthRequest.getNonce();
        this.endpoint_url = pushAuthRequest.getUrl();
        this.serial = pushAuthRequest.getSerial();
        this.question = pushAuthRequest.getQuestion();
        this.title = pushAuthRequest.getTitle();
        this.signature = pushAuthRequest.getSignature();
        this.appPrivateKey = appPrivateKey;
        this.piPublicKey = piPublicKey;
        this.sslVerify = pushAuthRequest.isSslVerify();
        this.pushAuthCallbackInterface = pushAuthCallbackInterface;
        this.token = token;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        logprint("Push authentication starting...");
    }

    @Override
    protected Boolean doInBackground(Void... voids) {
        // 0. Split data
        String strSSLVerify = sslVerify ? "1" : "0";

        String toVerify = nonce + "|" + endpoint_url + "|" + serial + "|" + question +
                "|" + title + "|" + strSSLVerify;

        // 1. Verify the signature
        boolean validSignature = false;
        try {
            validSignature = Util.verifySignature(piPublicKey, signature, toVerify);
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (SignatureException e) {
            e.printStackTrace();
        }

        if (!validSignature) {
            logprint("Signature invalid.");
            publishProgress(PA_INVALID_SIGNATURE);
            return false;
        }

        // 2. Sign the nonce
        String signature_to_send = null;
        String toSign = nonce + "|" + serial;
        try {
            signature_to_send = Util.sign(appPrivateKey, toSign);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (SignatureException e) {
            e.printStackTrace();
        }

        if (signature_to_send == null) {
            publishProgress(PA_SIGNING_FAILURE);
            return false;
        }

        // 3. Assemble the data and send it to the endpoint
        Map<String, String> map = new LinkedHashMap<>();
        map.put(NONCE, nonce);
        map.put(SERIAL, serial);
        map.put(SIGNATURE, signature_to_send);

        Endpoint endpoint = new Endpoint(sslVerify, endpoint_url, map, this);
        endpoint.connect();
        return true;
    }

    // Progress from THIS task and statusCodes from Endpoint
    @Override
    protected void onProgressUpdate(Integer... values) {
        super.onProgressUpdate(values);
        int code = values[0];
        switch (code) {
            case PA_INVALID_SIGNATURE:
                logprint("authentication failed due to invalid signature :(");
                pushAuthCallbackInterface.handleAuthError(code, token);
                break;
            case PA_SIGNING_FAILURE:
                logprint("authentication failed due to signing failure :(");
                pushAuthCallbackInterface.handleAuthError(code, token);
                break;
            case PA_AUTHENTICATION_FINISHED:
                if (success) {
                    logprint("authentication successful :)");
                    pushAuthCallbackInterface.authenticationFinished(true, token);
                } else {
                    logprint("authentication failed :(");
                    pushAuthCallbackInterface.authenticationFinished(false, token);
                }
                break;
            default: {
                pushAuthCallbackInterface.handleAuthError(code, token);
                break;
            }
        }
    }

    @Override
    public void updateStatus(int statusCode) {
        // Status Codes from the Endpoint are directed to onProgressUpdate to run on UI Thread
        logprint("Statuscode in PushAuthTask from Endpoint: " + statusCode);
        publishProgress(statusCode);
    }

    // Successful request callback from Endpoint
    @Override
    public void responseReceived(String response, int responseCode) {
        if (responseCode == 200) {
            if (!response.equals("")) {
                try {
                    JSONObject jso = new JSONObject(response);
                    JSONObject result = jso.getJSONObject(RESPONSE_RESULT);
                    success = result.getBoolean(RESPONSE_VALUE);
                    publishProgress(PA_AUTHENTICATION_FINISHED);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        }
    }
} public class PushRolloutTask extends AsyncTask<Void, Integer, Boolean> implements EndpointCallback {

    private String serial;
    private String rollout_url;
    private Token token;
    private PresenterTaskInterface presenterTaskInterface;
    private String in_key;
    private String fbtoken;

    public PushRolloutTask(Token t, String fbtoken, PresenterTaskInterface presenterTaskInterface) {
        this.token = t;
        this.serial = t.getSerial();
        this.rollout_url = t.rollout_url;
        this.fbtoken = fbtoken;
        this.presenterTaskInterface = presenterTaskInterface;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        logprint("Starting push rollout...");
        logprint("rollout url: " + rollout_url);
    }

    @Override
    protected Boolean doInBackground(Void... voids) {
        publishProgress(PRO_STATUS_STEP_1);
        // Verify the tokens register ttl
        Date now = new Date();
        if (now.after(token.rollout_expiration)) {
            publishProgress(PRO_STATUS_REGISTRATION_TIME_EXPIRED);
            return false;
        }

        // 1. Generate a new keypair (RSA 4096bit), the private key is stored with the serial as alias
        PublicKey pubkey = presenterTaskInterface.generatePublicKeyFor(serial);

        // 2. Send the pubkey and the firebase token to the rollout URL
        publishProgress(PRO_STATUS_STEP_2);

        String key = null;
        if (pubkey != null) {
            key = new Util().encodeBase64(pubkey.getEncoded());
        }

        // assemble the data to send
        Map<String, String> map = new LinkedHashMap<>();
        map.put(ENROLLMENT_CRED, token.enrollment_credential);
        map.put(SERIAL, token.getSerial());
        map.put(FB_TOKEN, fbtoken);
        if (key == null) return false;
        map.put(PUBKEY, key);

        Endpoint endpoint = new Endpoint(token.sslVerify, token.rollout_url, map, this);
        endpoint.connect();
        return true;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        super.onProgressUpdate(values);
        // Pass the statusCode to the presenter. This must be done via this method
        // because onProgressUpdate runs on the main thread.
        if (values[0] == PRO_STATUS_KEY_RECEIVED) {
            presenterTaskInterface.receivePublicKey(in_key, token);
        } else {
            presenterTaskInterface.updateTaskStatus(values[0], token);
        }
    }

    @Override
    protected void onPostExecute(Boolean aBoolean) {
        super.onPostExecute(aBoolean);
    }

    @Override
    public void updateStatus(int statusCode) {
        // Status from Endpoint
        switch (statusCode) {
            // Special Cases
            case STATUS_ENDPOINT_SENDING_COMPLETE:
                publishProgress(PRO_STATUS_STEP_3);
                break;
            default:
                // Other cases are just passed on
                publishProgress(statusCode);
                break;
        }
    }

    @Override
    public void responseReceived(String response, int responseCode) {
        // Parsing the JSON here
        if (responseCode == 200) {
            if (!response.equals("")) {
                try {
                    JSONObject resp = new JSONObject(response);
                    JSONObject detail = resp.getJSONObject(RESPONSE_DETAIL);
                    in_key = detail.getString(RESPONSE_PUBLIC_KEY);
                    logprint("in_key:" + in_key);
                    publishProgress(PRO_STATUS_DONE);
                    publishProgress(PRO_STATUS_KEY_RECEIVED);
                } catch (JSONException e) {
                    logprint("Malformed JSON in response");
                    publishProgress(PRO_STATUS_MALFORMED_JSON);
                    e.printStackTrace();
                }
            }
        } else {
            logprint("response not ok");
            publishProgress(PRO_STATUS_RESPONSE_NOT_OK);
        }
    }
} public class TwoStepRolloutTask extends AsyncTask<Void, Void, Boolean> {

    private final Token token;
    private int iterations;
    private int output_size_bit;
    private byte[] phonepartBytes;
    private PresenterTaskInterface presenterTaskInterface;

    public TwoStepRolloutTask(Token t, int phonepartlength, int iterations, int output_size, PresenterTaskInterface presenterTaskInterface) {
        this.token = t;
        this.iterations = iterations;
        this.output_size_bit = output_size;
        this.phonepartBytes = new byte[phonepartlength];
        this.presenterTaskInterface = presenterTaskInterface;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        logprint("Starting 2step rollout...");
        presenterTaskInterface.updateTaskStatus(STATUS_TWO_STEP_ROLLOUT, token);
    }

    @Override
    protected Boolean doInBackground(Void... params) {
        // 1. Generate random bytes for the phonepartBytes
        SecureRandom sr = new SecureRandom();
        sr.nextBytes(phonepartBytes);

        String server_secret_hex = byteArrayToHexString(token.getSecret());
        char[] ch = server_secret_hex.toCharArray();
        byte[] completesecretBytes = new byte[(output_size_bit / 8)];

        // 2. PBKDF2 with the specified parameters
        try {
            completesecretBytes = OTPGenerator.generatePBKDFKey(ch, phonepartBytes, iterations, output_size_bit);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            e.printStackTrace();
        }
        token.setSecret(completesecretBytes);
        return true;
    }

    private String buildResultMessage() {
        /* 3. Build the result to show to the user
            We use the first 4 characters of the sha1 hash of the client(phone) part as checksum.
            client_part being the binary random value, that the client(phone) generated:
            b32encode(sha1(client_part)[0:3] + client_part)*/
        String result;
        byte[] digest = new byte[20];
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            digest = md.digest(phonepartBytes);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        byte[] checksumBytes = new byte[4];
        System.arraycopy(digest, 0, checksumBytes, 0, 4);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            outputStream.write(checksumBytes);
            outputStream.write(phonepartBytes);
        } catch (IOException e) {
            e.printStackTrace();
        }

        byte[] completeOutputBytes = outputStream.toByteArray();
        result = Util.insertPeriodically(new Base32().encodeAsString(completeOutputBytes), 4);
        result = result.replaceAll("=", "");
        return result;
    }

    @Override
    protected void onPostExecute(final Boolean success) {
        // 4. Display the phone-part of the secret and first OTP to verify
        presenterTaskInterface.updateTaskStatus(STATUS_TWO_STEP_ROLLOUT_DONE, token);
        presenterTaskInterface.twoStepFinished(buildResultMessage());
        logprint("2step rollout finished.");
    }
} public class SecretKeyWrapper {
    private final Cipher mCipher;
    private final KeyPair mPair;

    /**
     * Create a wrapper using the public/private key pair with the given alias.
     * If no pair with that alias exists, it will be generated.
     */
    @SuppressLint("GetInstance")
    public SecretKeyWrapper(Context context)
            throws GeneralSecurityException, IOException {
        mCipher = Cipher.getInstance(KEY_WRAP_ALGORITHM);

        final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
        keyStore.load(null);

        if (!keyStore.containsAlias("settings")) {
            generateKeyPair("settings", context);
        }

        // Even if we just generated the key, always read it back to ensure we
        // can read it successfully.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            //Following code is for API 28+
            PrivateKey privateKey = (PrivateKey) keyStore.getKey("settings", null);
            Certificate certificate = keyStore.getCertificate("settings");
            KeyStore.PrivateKeyEntry entry = new KeyStore.PrivateKeyEntry(privateKey, new Certificate[]{certificate});
            mPair = new KeyPair(entry.getCertificate().getPublicKey(), entry.getPrivateKey());
        } else {
            final KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry("settings", null);
            mPair = new KeyPair(entry.getCertificate().getPublicKey(), entry.getPrivateKey());
        }

        /*final PrivateKey privateKey = (PrivateKey) keyStore.getKey("alias", null);
        final PublicKey publicKey = privateKey != null ? keyStore.getCertificate("alias").getPublicKey() : null;
        mPair = new KeyPair(publicKey, privateKey);*/
    }

    /**
     * Wrap a {@link SecretKey} using the public key assigned to this wrapper.
     * Use {@link #unwrap(byte[])} to later recover the original
     * {@link SecretKey}.
     *
     * @return a wrapped push_version of the given {@link SecretKey} that can be
     * safely stored on untrusted storage.
     */
    public byte[] wrap(SecretKey key) throws GeneralSecurityException {
        mCipher.init(Cipher.WRAP_MODE, mPair.getPublic());
        return mCipher.wrap(key);
    }

    /**
     * Unwrap a {@link SecretKey} using the private key assigned to this
     * wrapper.
     *
     * @param blob a wrapped {@link SecretKey} as previously returned by
     *             {@link #wrap(SecretKey)}.
     */
    public SecretKey unwrap(byte[] blob) throws GeneralSecurityException {
        mCipher.init(Cipher.UNWRAP_MODE, mPair.getPrivate());

        return (SecretKey) mCipher.unwrap(blob, "AES", Cipher.SECRET_KEY);
    }

    /**
     * Generate a KeyPair and store it with the given alias in the KeyStore.
     * Return the PublicKey
     *
     * @param alias   the alias to store the key with
     * @param context needed for KeyPairGeneratorSpec
     * @return the PublicKey of the just generated KeyPair
     */
    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    public static PublicKey generateKeyPair(String alias, Context context) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException,
            NoSuchProviderException, InvalidAlgorithmParameterException, UnrecoverableEntryException {
        final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
        keyStore.load(null);
        logprint("generateKeyPair for alias: " + alias);
        if (keyStore.containsAlias(alias)) {
            // TODO double entry_normal -> overwrite?
        }
        final Calendar start = new GregorianCalendar();
        final Calendar end = new GregorianCalendar();
        end.add(Calendar.YEAR, 100);
        final KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
        final KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context)
                .setAlias(alias)
                .setSubject(new X500Principal("CN=" + alias))
                .setSerialNumber(BigInteger.ONE)
                .setStartDate(start.getTime())
                .setEndDate(end.getTime())
                .setKeySize(4096)
                .build();
        gen.initialize(spec);
        gen.generateKeyPair();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            //Following code is for API 28+
            PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, null);
            Certificate certificate = keyStore.getCertificate(alias);
            return new KeyStore.PrivateKeyEntry(privateKey, new Certificate[]{certificate}).getCertificate().getPublicKey();
        } else {
            final KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null);
            return entry.getCertificate().getPublicKey();
        }
    }

    /**
     * Load the PrivateKey for the given alias/serial
     *
     * @param alias the alias to load the key for
     * @return the PrivateKey
     */
    public PrivateKey getPrivateKeyFor(String alias) throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException, UnrecoverableEntryException {
        final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
        keyStore.load(null);
        if (!keyStore.containsAlias(alias)) {
            // TODO key not found
            return null;
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            //Following code is for API 28+
            PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, null);
            Certificate certificate = keyStore.getCertificate(alias);
            return new KeyStore.PrivateKeyEntry(privateKey, new Certificate[]{certificate}).getPrivateKey();
        } else {
            final KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null);
            return entry.getPrivateKey();
        }
    }

    /**
     * Remove the privateKey from the Keystore for the given alias/serial
     *
     * @param alias the serial is the alias of the privateKey in the Keystore
     */
    public void removePrivateKeyFor(String alias) {
        try {
            final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
            keyStore.load(null);
            if (!keyStore.containsAlias(alias)) {
                logprint("key for alias " + alias + " was not found for deletion!");
                return;
            }
            keyStore.deleteEntry(alias);
            logprint("key for alias " + alias + " was deleted from keystore!");
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

} public class Util {

    private String baseFilePath;
    private SecretKeyWrapper secretKeyWrapper;
    private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public Util(SecretKeyWrapper secretKeyWrapper, String baseFilePath) {
        this.baseFilePath = baseFilePath;
        this.secretKeyWrapper = secretKeyWrapper;
    }

    public Util() {
    }

    /**
     * This Method loads the encrypted saved tokens, in the progress the Secret Key is unwrapped
     * and used to decrypt the saved tokens
     *
     * @return An ArrayList of Tokens
     */
    public ArrayList<Token> loadTokens() throws IOException, GeneralSecurityException {
        logprint("LOADING TOKEN");
        ArrayList<Token> tokens = new ArrayList<>();
        try {
            byte[] data = loadDataFromFile(DATAFILE);
            if (data == null) {
                return null;
            }
            JSONArray a = new JSONArray(new String(data));
            for (int i = 0; i < a.length(); i++) {
                tokens.add(makeTokenFromJSON(a.getJSONObject(i)));
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return tokens;
    }

    /**
     * Encrypt and save the ArrayList of tokens with a Secret Key, which is wrapped by a Public Key
     * that is stored in the Keystore
     *
     * @param tokens ArrayList of tokens to save
     */
    public void saveTokens(ArrayList<Token> tokens) throws GeneralSecurityException, IOException {
        if (tokens == null) {
            return;
        }
        JSONArray tmp = new JSONArray();
        for (Token t : tokens) {
            try {
                tmp.put(makeJSONfromToken(t));
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }

        if (saveToFile(DATAFILE, tmp.toString().getBytes())) {
            logprint("Tokenlist saved.");
        }
    }

    private Token makeTokenFromJSON(JSONObject o) throws JSONException {
        //Log.d("LOAD TOKEN FROM: ", o.toString());

        // when no serial is found (for "old" data) it is set to the label
        String serial;
        String label = o.getString(LABEL);
        try {
            serial = o.getString(SERIAL);
        } catch (JSONException e) {
            serial = label;
        }
        String type = o.getString(TYPE);

        if (type.equals(PUSH)) {
            Token t = new Token(serial, label);
            t.state = State.valueOf(o.getString(ROLLOUT_STATE));
            if (t.state.equals(UNFINISHED)) {
                t.rollout_url = o.getString(URL);
                t.enrollment_credential = o.getString(ENROLLMENT_CRED);
                t.sslVerify = o.getBoolean(SSL_VERIFY);
                try {
                    t.rollout_expiration = dateFormat.parse(o.getString(ROLLOUT_EXPIRATION));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
            // Check for pending Authentication Requests
            try {
                String pendingAuths = o.getString(PENDING_AUTHS);
                t.setPendingAuths(new Gson().fromJson(pendingAuths, new TypeToken<ArrayList<PushAuthRequest>>() {
                }.getType()));
            } catch (JSONException e) {
                // there were none and that's ok
            }
            return t;
        }

        Token tmp = new Token(new Base32().decode(o.getString(SECRET)), serial, label,
                type, o.getInt(DIGITS));

        tmp.setAlgorithm(o.getString(ALGORITHM));
        if (o.getString(TYPE).equals(HOTP)) {
            tmp.setCounter(o.getInt(COUNTER));
        }
        if (o.getString(TYPE).equals(TOTP)) {
            tmp.setPeriod(o.getInt(PERIOD));
        }
        if (o.optBoolean(WITHPIN, false)) {
            tmp.setWithPIN(true);
            tmp.setPin(o.getString(PIN));
            tmp.setLocked(true);
        }
        if (o.optBoolean(TAPTOSHOW, false)) {
            tmp.setWithTapToShow(true);
        }
        if (o.optBoolean(PERSISTENT)) {
            tmp.setPersistent(true);
        }

        return tmp;
    }

    private JSONObject makeJSONfromToken(Token t) throws JSONException {
        JSONObject o = new JSONObject();

        o.put(SERIAL, t.getSerial());
        o.put(LABEL, t.getLabel());
        o.put(TYPE, t.getType());

        if (t.getType().equals(PUSH)) {
            State state = t.state;
            if (state.equals(AUTHENTICATING)) {
                // Don't save authenticating state, has to be finished
                // Unfinished token cannot authenticate
                state = FINISHED;
            }
            o.put(ROLLOUT_STATE, state);
            // If the rollout is not finished yet, save the data necessary to complete it
            if (t.state.equals(UNFINISHED)) {
                o.put(URL, t.rollout_url);
                o.put(ROLLOUT_EXPIRATION, dateFormat.format(t.rollout_expiration));
                o.put(ENROLLMENT_CRED, t.enrollment_credential);
                o.put(SSL_VERIFY, t.sslVerify);
            }

            // Check for pending Authentication Requests
            if (!t.getPendingAuths().isEmpty()) {
                String pendingAuths = new Gson().toJson(t.getPendingAuths());
                o.put(PENDING_AUTHS, pendingAuths);
            }
            return o;
        }

        o.put(SECRET, new String(new Base32().encode(t.getSecret())));
        o.put(DIGITS, t.getDigits());
        o.put(ALGORITHM, t.getAlgorithm());

        if (t.getType().equals(HOTP)) {
            o.put(COUNTER, t.getCounter());
        }
        if (t.getType().equals(TOTP)) {
            o.put(PERIOD, t.getPeriod());
        }
        if (t.isWithPIN()) {
            o.put(WITHPIN, true);
            o.put(PIN, t.getPin());
        } else {
            o.put(WITHPIN, false);
        }
        if (t.isWithTapToShow()) {
            o.put(TAPTOSHOW, true);
        }
        if (t.isPersistent()) {
            o.put(PERSISTENT, true);
        }
        return o;
    }

    public void storePIPubkey(String key, String serial) throws GeneralSecurityException, IllegalArgumentException, IOException {
        byte[] keybytes = decodeBase64(key);

        PublicKey pubkey = PKCS1ToSubjectPublicKeyInfo.decodePKCS1PublicKey(keybytes);
        if (saveToFile(serial + "_" + PUBKEYFILE, pubkey.getEncoded())) {
            logprint("pubkey for " + serial + " saved.");
        }

        // this code is expecting pkcs8
        /*X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keybytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        PublicKey pubkey = kf.generatePublic(keySpec); */
    }

    public PublicKey getPIPubkey(String serial) throws GeneralSecurityException, IOException {
        if (baseFilePath == null) return null;
        return getPIPubkey(baseFilePath, serial);
    }

    PublicKey getPIPubkey(String filepath, String serial) throws GeneralSecurityException, IOException {
        byte[] keybytes = loadDataFromFile(serial + "_" + PUBKEYFILE, filepath);
        // build pubkey
        if (keybytes == null) return null;
        X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(keybytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePublic(X509publicKey);
    }

    /**
     * Load the data from an encrypted file. The baseFilePath of Util will be used if set.
     * baseFilePath + "/" + fileName
     *
     * @param fileName Name of the file to load
     * @return raw data as byte array, null if no baseFilePath is set or there is no file
     */
    private byte[] loadDataFromFile(String fileName) throws IOException, GeneralSecurityException {
        if (baseFilePath == null) return null;
        return loadDataFromFile(fileName, baseFilePath);
    }

    /**
     * Load the data from an encrypted file, using the specified baseFilePath (from context).
     * baseFilePath + "/" + fileName
     *
     * @param fileName     Name of the file to load
     * @param baseFilePath baseFilePath of the Context
     * @return raw data as byte array, null if there is no file
     */
    private byte[] loadDataFromFile(String fileName, String baseFilePath) throws IOException, GeneralSecurityException {
        byte[] encryptedData = readFile(new File(baseFilePath + "/" + fileName));
        // decrypt
        SecretKey encryptionKey = getSecretKey(new File(baseFilePath + "/" + KEYFILE));
        if (encryptedData == null) {
            return null;
        }
        return decrypt(encryptionKey, encryptedData);
    }

    private void writeFile(File file, byte[] data) throws IOException {
        try (OutputStream out = new FileOutputStream(file)) {
            out.write(data);
        }
    }

    private byte[] readFile(File file) throws IOException {
        try (InputStream in = new FileInputStream(file)) {
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int count;
            while ((count = in.read(buffer)) != -1) {
                bytes.write(buffer, 0, count);
            }
            return bytes.toByteArray();
        } catch (FileNotFoundException e) {
            logprint("File: " + file.getAbsolutePath() + " not found");
            return null;
        }
    }

    public void removePubkeyFor(String serial) {
        File f = new File(baseFilePath + "/" + serial + "_" + PUBKEYFILE);
        boolean res;
        if (f.exists()) {
            res = f.delete();
            if (res) {
                logprint("pubkey file of " + serial + " was found and deleted!");
            } else {
                logprint("pubkey file of " + serial + " was not deleted!");
            }
        } else {
            logprint("pubkey file of " + serial + " was not found!");
        }
    }

    public void storeFirebaseConfig(FirebaseInitConfig firebaseInitConfig) throws GeneralSecurityException, IOException {
        logprint("Storing Firebase config...");
        JSONObject o = new JSONObject();
        try {
            o.put(PROJECT_ID, firebaseInitConfig.getProjID());
            o.put(APP_ID, firebaseInitConfig.getAppID());
            o.put(API_KEY, firebaseInitConfig.getApiKey());
            o.put(PROJECT_NUMBER, firebaseInitConfig.getProjNumber());
        } catch (JSONException e) {
            e.printStackTrace();
        }
        if (saveToFile(FB_CONFIG_FILE, o.toString().getBytes())) {
            logprint("Firebase config stored.");
        }
    }

    /**
     * @return FirebaseInitConfig object or null if there is no config / error
     */
    public FirebaseInitConfig loadFirebaseConfig() throws IOException, GeneralSecurityException, JSONException {
        logprint("Loading Firebase config...");
        byte[] data = loadDataFromFile(FB_CONFIG_FILE);
        if (data == null) {
            logprint("Firebase config not found!");
            return null;
        }

        JSONObject o = new JSONObject(new String(data));
        String projID = o.getString(PROJECT_ID);
        String appID = o.getString(APP_ID);
        String api_key = o.getString(API_KEY);
        String projNumber = o.getString(PROJECT_NUMBER);

        //logprint("Firebase config loaded.");
        return new FirebaseInitConfig(projID, appID, api_key, projNumber);
    }

    public void removeFirebaseConfig() {
        File f = new File(baseFilePath + "/" + FB_CONFIG_FILE);
        if (f.exists()) {
            if (f.delete()) {
                logprint("Firebase config removed.");
            }
        }
    }

    /**
     * Encrypt and save the given data in the specified file and baseFilePath.
     * baseFilePath + "/" + fileName
     *
     * @param fileName     Name of the file to save to
     * @param baseFilePath Path to the app's data storage
     * @param data         Data to save
     * @return true if successful, false if error
     */
    private boolean saveToFile(String fileName, String baseFilePath, byte[] data) throws GeneralSecurityException, IOException {
        SecretKey key = getSecretKey(new File(baseFilePath + "/" + KEYFILE));
        data = encrypt(key, data);
        writeFile(new File(baseFilePath + "/" + fileName), data);
        return true;
    }

    /**
     * Encrypt and save the given data in the specified file.
     * baseFilePath + "/" + fileName
     *
     * @param fileName Name of the file to save to
     * @param data     Data to save
     * @return true if successful, false if error
     */
    public boolean saveToFile(String fileName, byte[] data) throws GeneralSecurityException, IOException {
        if (baseFilePath == null) return false;
        return saveToFile(fileName, baseFilePath, data);
    }

    private static byte[] encrypt(SecretKey secretKey, GCMParameterSpec iv, byte[] plainText)
            throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
            InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        Cipher cipher = Cipher.getInstance(CRYPT_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
        return cipher.doFinal(plainText);
    }

    private static byte[] decrypt(SecretKey secretKey, GCMParameterSpec iv, byte[] cipherText)
            throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
            InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        Cipher cipher = Cipher.getInstance(CRYPT_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
        return cipher.doFinal(cipherText);
    }

    static byte[] encrypt(SecretKey secretKey, byte[] plaintext)
            throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException,
            IllegalBlockSizeException, InvalidAlgorithmParameterException {
        final byte[] iv = new byte[IV_LENGTH];
        new SecureRandom().nextBytes(iv);
        GCMParameterSpec params = new GCMParameterSpec(128, iv, 0, 12);
        byte[] cipherText = encrypt(secretKey, params, plaintext);
        byte[] combined = new byte[iv.length + cipherText.length];
        System.arraycopy(iv, 0, combined, 0, iv.length);
        System.arraycopy(cipherText, 0, combined, iv.length, cipherText.length);
        return combined;
    }

    static byte[] decrypt(SecretKey secretKey, byte[] cipherText)
            throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException,
            BadPaddingException, InvalidAlgorithmParameterException {
        //byte[] iv = Arrays.copyOfRange(cipherText, 0, IV_LENGTH);
        GCMParameterSpec params = new GCMParameterSpec(128, cipherText, 0, 12);
        byte[] cipher = Arrays.copyOfRange(cipherText, IV_LENGTH, cipherText.length);
        return decrypt(secretKey, params, cipher);
    }

    /**
     * Load our symmetric secret key.
     * The symmetric secret key is stored securely on disk by wrapping
     * it with a public/private key pair, possibly backed by hardware.
     */
    public SecretKey getSecretKey(File keyFile)
            throws GeneralSecurityException, IOException {
        if (secretKeyWrapper == null) {
            throw new GeneralSecurityException("No SecretKeyWrapper available!");
        }
        // Generate secret key if none exists
        if (!keyFile.exists()) {
            final byte[] raw = new byte[KEY_LENGTH];
            new SecureRandom().nextBytes(raw);
            final SecretKey key = new SecretKeySpec(raw, "AES");
            final byte[] wrapped = secretKeyWrapper.wrap(key);
            writeFile(keyFile, wrapped);
        }
        // Even if we just generated the key, always read it back to ensure we
        // can read it successfully.
        final byte[] wrapped = readFile(keyFile);
        if (wrapped == null) return null;
        return secretKeyWrapper.unwrap(wrapped);
    }

    /**
     * @param privateKey privateKey to sign the message with
     * @param message    message to sign
     * @return Base32 formatted signature
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @throws SignatureException
     */
    public static String sign(PrivateKey privateKey, String message) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
        byte[] bMessage = message.getBytes(StandardCharsets.UTF_8);

        Signature s = Signature.getInstance(SIGNING_ALGORITHM);
        s.initSign(privateKey);
        s.update(bMessage);

        byte[] signature = s.sign();
        return new Base32().encodeAsString(signature);
    }

    /**
     * @param publicKey publicKey to verify the signature with
     * @param signature signature to verify, !!formatted in Base32!!
     * @param payload   payload that was signed
     * @return true if the signature is valid, false otherwise
     * @throws InvalidKeyException
     * @throws NoSuchAlgorithmException
     * @throws SignatureException
     */
    public static boolean verifySignature(PublicKey publicKey, String signature, String payload) throws InvalidKeyException,
            NoSuchAlgorithmException, SignatureException {
        if (!new Base32().isInAlphabet(signature)) {
            logprint("verifySignature: The given signature is not Base32 encoded!");
            return false;
        }

        byte[] message = payload.getBytes(StandardCharsets.UTF_8);
        byte[] bSignature = new Base32().decode(signature);
        Signature sig = Signature.getInstance(SIGNING_ALGORITHM);

        sig.initVerify(publicKey);
        sig.update(message);
        return sig.verify(bSignature);
    }

    public byte[] decodeBase64(String key) {
        return Base64.decode(key, Base64.DEFAULT);
    }

    public String encodeBase64(byte[] data) {
        return Base64.encodeToString(data, Base64.URL_SAFE);
    }

    /**
     * Converts a byte array to a Hex String
     *
     * @param ba byte array to convert
     * @return the Hex as String
     */
    public static String byteArrayToHexString(byte[] ba) {
        StringBuilder str = new StringBuilder();
        for (int i = 0; i < ba.length; i++)
            str.append(String.format("%02x", ba[i]));
        return str.toString();
    }

    /**
     * Converts a Hex string to a byte array
     *
     * @param hex: the Hex string to convert
     * @return a byte array
     */
    public static byte[] hexStringToByteArray(String hex) {
        // Adding one byte to get the right conversion
        // Values starting with "0" can be converted
        byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();

        // Copy all the REAL bytes, not the "first"
        byte[] ret = new byte[bArray.length - 1];
        for (int i = 0; i < ret.length; i++)
            ret[i] = bArray[i + 1];
        return ret;
    }

    public static void logprint(String msg) {
        if (BuildConfig.DEBUG) {
            if (msg == null)
                return;
            Log.e(TAG, msg);
        }
    }

    public static String insertPeriodically(String text, int stepSize) {
        StringBuilder builder = new StringBuilder(text.length() + " ".length() * (text.length() / stepSize) + 1);
        int index = 0;
        String prefix = "";
        while (index < text.length()) {
            builder.append(prefix);
            prefix = " ";
            builder.append(text.substring(index,
                    Math.min(index + stepSize, text.length())));
            index += stepSize;
        }
        return builder.toString();
    }
} public static final String HOTP = "hotp"; public static final int PA_INVALID_SIGNATURE = 2001; public static final int PA_SIGNING_FAILURE = 2002; public static final int PRO_STATUS_BAD_BASE64 = 1013; public static final int PRO_STATUS_DONE = 1004; public static final int PRO_STATUS_MALFORMED_JSON = 1015; public static final int PRO_STATUS_REGISTRATION_TIME_EXPIRED = 1010; public static final int PRO_STATUS_RESPONSE_NOT_OK = 1016; public static final int PRO_STATUS_RESPONSE_NO_KEY = 1011; public static final int PRO_STATUS_STEP_1 = 1001; public static final int PRO_STATUS_STEP_2 = 1002; public static final int PRO_STATUS_STEP_3 = 1003; public static final String PUSH = "pipush"; public static final int STATUS_ENDPOINT_ERROR = 4005; public static final int STATUS_ENDPOINT_MALFORMED_URL = 4003; public static final int STATUS_ENDPOINT_SSL_ERROR = 4006; public static final int STATUS_ENDPOINT_UNKNOWN_HOST = 4002; public static final int STATUS_INIT_FIREBASE = 3101; public static final int STATUS_INIT_FIREBASE_DONE = 3102; public static final int STATUS_STANDARD_ROLLOUT_DONE = 3105; public static final int STATUS_TWO_STEP_ROLLOUT = 3103; public static final int STATUS_TWO_STEP_ROLLOUT_DONE = 3104; public static final String TOTP = "totp"; public static String generateOTP(Token token) {
    if (token.getType().equals(HOTP) || token.getType().equals(TOTP)) {
        String secretAsHEX = byteArrayToHexString(token.getSecret());
        String digits = String.valueOf(token.getDigits());
        if (token.getType().equals(TOTP)) {
            return String.format("%0" + token.getDigits() + "d", generateTOTP(secretAsHEX,
                    (System.currentTimeMillis() / 1000), digits, token.getPeriod(), token.getAlgorithm()));
        } else {
            return String.format("%0" + token.getDigits() + "d", generateHOTP(secretAsHEX,
                    String.valueOf(token.getCounter()), digits, token.getAlgorithm()));
        }
    } else return "";
} public static String hashPIN(int pin, Token token) {
    byte[] secretBytes = new Base32().decode(token.getSecret());
    if (secretBytes.length == 0) {
        secretBytes = token.getSecret();
    }
    byte[] pinToHash = hexStringToByteArray(Integer.toHexString(pin));
    byte[] temp = hmac_sha("HmacSHA512", secretBytes, pinToHash);
    return byteArrayToHexString(temp);
} public static void logprint(String msg) {
    if (BuildConfig.DEBUG) {
        if (msg == null)
            return;
        Log.e(TAG, msg);
    }
}
import android.util.Pair;
import org.apache.commons.codec.binary.Base32;
import org.json.JSONException;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.UnrecoverableEntryException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import it.netknights.piauthenticator.R;
import it.netknights.piauthenticator.interfaces.MainActivityInterface;
import it.netknights.piauthenticator.interfaces.PresenterInterface;
import it.netknights.piauthenticator.interfaces.PresenterTaskInterface;
import it.netknights.piauthenticator.interfaces.PresenterUtilInterface;
import it.netknights.piauthenticator.interfaces.PushAuthCallbackInterface;
import it.netknights.piauthenticator.interfaces.TokenListViewInterface;
import it.netknights.piauthenticator.model.FirebaseInitConfig;
import it.netknights.piauthenticator.model.Model;
import it.netknights.piauthenticator.model.PushAuthRequest;
import it.netknights.piauthenticator.model.ScanResult;
import it.netknights.piauthenticator.model.Token;
import it.netknights.piauthenticator.tasks.PushAuthTask;
import it.netknights.piauthenticator.tasks.PushRolloutTask;
import it.netknights.piauthenticator.tasks.TwoStepRolloutTask;
import it.netknights.piauthenticator.utils.SecretKeyWrapper;
import it.netknights.piauthenticator.utils.Util;
import static it.netknights.piauthenticator.utils.AppConstants.HOTP;
import static it.netknights.piauthenticator.utils.AppConstants.PA_INVALID_SIGNATURE;
import static it.netknights.piauthenticator.utils.AppConstants.PA_SIGNING_FAILURE;
import static it.netknights.piauthenticator.utils.AppConstants.PRO_STATUS_BAD_BASE64;
import static it.netknights.piauthenticator.utils.AppConstants.PRO_STATUS_DONE;
import static it.netknights.piauthenticator.utils.AppConstants.PRO_STATUS_MALFORMED_JSON;
import static it.netknights.piauthenticator.utils.AppConstants.PRO_STATUS_REGISTRATION_TIME_EXPIRED;
import static it.netknights.piauthenticator.utils.AppConstants.PRO_STATUS_RESPONSE_NOT_OK;
import static it.netknights.piauthenticator.utils.AppConstants.PRO_STATUS_RESPONSE_NO_KEY;
import static it.netknights.piauthenticator.utils.AppConstants.PRO_STATUS_STEP_1;
import static it.netknights.piauthenticator.utils.AppConstants.PRO_STATUS_STEP_2;
import static it.netknights.piauthenticator.utils.AppConstants.PRO_STATUS_STEP_3;
import static it.netknights.piauthenticator.utils.AppConstants.PUSH;
import static it.netknights.piauthenticator.utils.AppConstants.STATUS_ENDPOINT_ERROR;
import static it.netknights.piauthenticator.utils.AppConstants.STATUS_ENDPOINT_MALFORMED_URL;
import static it.netknights.piauthenticator.utils.AppConstants.STATUS_ENDPOINT_SSL_ERROR;
import static it.netknights.piauthenticator.utils.AppConstants.STATUS_ENDPOINT_UNKNOWN_HOST;
import static it.netknights.piauthenticator.utils.AppConstants.STATUS_INIT_FIREBASE;
import static it.netknights.piauthenticator.utils.AppConstants.STATUS_INIT_FIREBASE_DONE;
import static it.netknights.piauthenticator.utils.AppConstants.STATUS_STANDARD_ROLLOUT_DONE;
import static it.netknights.piauthenticator.utils.AppConstants.STATUS_TWO_STEP_ROLLOUT;
import static it.netknights.piauthenticator.utils.AppConstants.STATUS_TWO_STEP_ROLLOUT_DONE;
import static it.netknights.piauthenticator.utils.AppConstants.State.AUTHENTICATING;
import static it.netknights.piauthenticator.utils.AppConstants.State.FINISHED;
import static it.netknights.piauthenticator.utils.AppConstants.State.ROLLING_OUT;
import static it.netknights.piauthenticator.utils.AppConstants.State.UNFINISHED;
import static it.netknights.piauthenticator.utils.AppConstants.TOTP;
import static it.netknights.piauthenticator.utils.OTPGenerator.generateOTP;
import static it.netknights.piauthenticator.utils.OTPGenerator.hashPIN;
import static it.netknights.piauthenticator.utils.Util.logprint;
    }

    private void doTwoStepRollout(Token token, int phonepartlength, int iterations, int output_size) {
        new TwoStepRolloutTask(token, phonepartlength, iterations, output_size, this).execute();
    }

    private void rolloutFinished(Token token) {
        if (token == null) return;
        if (!token.getType().equals(PUSH)) {
            token.setCurrentOTP(generateOTP(token));
        } else {
            // Do not add token twice
            for (Token t : model.getTokens()) {
                if (t.getSerial().equals(token.getSerial()) && t.getType().equals(PUSH)) {
                    saveTokenlist();
                    tokenListInterface.notifyChange();
                    return;
                }
            }
        }
        model.getTokens().add(token);
        saveTokenlist();
        tokenListInterface.notifyChange();
    }

    @Override
    public PublicKey generatePublicKeyFor(String serial) {
        return mainActivityInterface.generatePublicKeyFor(serial);
    }

    /**
     * Store the received key with the serial as alias. If there is an error when storing the key,
     * the Token state will stay marked as unfinished and can be repeated.
     *
     * @param key   the received key as raw String
     * @param token token to save the key for
     */
    @Override
    public void receivePublicKey(String key, Token token) {
        try {
            try {
                util.storePIPubkey(key, token.getSerial());
            } catch (IOException e) {
                e.printStackTrace();            // TODO handle e
            }
        } catch (GeneralSecurityException e) {
            // this means the "key" field was empty or the DECODED data is not a key
            updateTaskStatus(PRO_STATUS_RESPONSE_NO_KEY, token);
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            updateTaskStatus(PRO_STATUS_BAD_BASE64, token);
        }
    }

    /**
     * Updates the UI according to the statusCode. This includes opening/closing dialogs and choosing the dialog type.
     *
     * @param statusCode statusCode
     */
    @Override
    public void updateTaskStatus(int statusCode, Token token) {
        switch (statusCode) {
            //----------- STANDARD ROLLOUT -----------
            case STATUS_STANDARD_ROLLOUT_DONE:
                rolloutFinished(token);
                break;
            //----------- INIT FIREBASE -----------
            case STATUS_INIT_FIREBASE:
                //mainActivityInterface.setStatusDialogText(R.string.InitFirebaseStatus);
                break;
            case STATUS_INIT_FIREBASE_DONE:
                mainActivityInterface.cancelStatusDialog();
                break;
            //----------- TWO STEP ROLLOUT -----------
            case STATUS_TWO_STEP_ROLLOUT:
                mainActivityInterface.setStatusDialogText(R.string.WaitWhileSecretIsGenerated);
                break;
            case STATUS_TWO_STEP_ROLLOUT_DONE:
                rolloutFinished(token);
                mainActivityInterface.cancelStatusDialog();
                break;
            //----------- PUSH ROLLOUT -----------
            case PRO_STATUS_STEP_1:
                //mainActivityInterface.setStatusDialogText(R.string.PushRolloutStep1Status);
                break;
            case PRO_STATUS_STEP_2:
                //mainActivityInterface.setStatusDialogText(R.string.PushRolloutStep2Status);
                break;
            case PRO_STATUS_STEP_3:
                //mainActivityInterface.setStatusDialogText(R.string.PushRolloutStep3Status);
                break;
            case PRO_STATUS_DONE:
                token.state = FINISHED;
                rolloutFinished(token);
                mainActivityInterface.cancelStatusDialog();
                break;
            //----------- PUSH ROLLOUT ERRORS -----------
            case PRO_STATUS_BAD_BASE64:
                token.state = UNFINISHED;
                rolloutFinished(token);
                mainActivityInterface.makeAlertDialog(R.string.Error, R.string.KeyFromServerWrongFormat);
                break;
            case PRO_STATUS_MALFORMED_JSON:
                token.state = UNFINISHED;
                rolloutFinished(token);
                mainActivityInterface.makeAlertDialog(R.string.Error, R.string.ResponseNotParsed);
                break;
            case PRO_STATUS_RESPONSE_NO_KEY:
                token.state = UNFINISHED;
                rolloutFinished(token);
                mainActivityInterface.makeAlertDialog((R.string.Error), (R.string.ResponseNoKey));
                break;
            case PRO_STATUS_REGISTRATION_TIME_EXPIRED:
                mainActivityInterface.makeAlertDialog(R.string.Error, R.string.RegistrationTimeExpired);
                removeToken(token);
                break;
            case STATUS_ENDPOINT_MALFORMED_URL:
                mainActivityInterface.makeAlertDialog(R.string.Error, R.string.RolloutURLinvalid);
                break;
            case STATUS_ENDPOINT_UNKNOWN_HOST: