public interface EndpointCallback {
    void updateStatus(int statusCode);

    void responseReceived(String response, int responseCode);
} public interface PresenterTaskInterface {
    void updateTaskStatus(int statusCode, Token token);

    void twoStepFinished(String messageToShow);

    PublicKey generatePublicKeyFor(String serial);

    void receivePublicKey(String key, Token token);
} 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 Endpoint {

    private boolean sslVerify;
    private String url;
    private Map<String, String> data;
    private EndpointCallback callback;

    public Endpoint(boolean sslVerify, String url, Map<String, String> data, EndpointCallback callback) {
        this.sslVerify = sslVerify;
        this.url = url;
        this.data = data;
        this.callback = callback;
    }

    /**
     * Establishes a connection to the URL specified in the Constructor.
     * The data is sent as POST Parameters.
     *
     * @return true if the request could be sent, false if not
     */
    public boolean connect() {
        logprint("Setting up connection to " + url);
        URL url;
        try {
            url = new URL(this.url);
        } catch (MalformedURLException e) {
            callback.updateStatus(STATUS_ENDPOINT_MALFORMED_URL);
            e.printStackTrace();
            return false;
        }
        HttpURLConnection con;
        try {
            if (url.getProtocol().equals("https")) {
                con = (HttpsURLConnection) url.openConnection();
            } else {
                con = (HttpURLConnection) url.openConnection();
            }
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        con.setDoOutput(true);
        con.setDoInput(true);
        try {
            con.setRequestMethod("POST");
        } catch (ProtocolException e) {
            e.printStackTrace();
        }
        con.setReadTimeout(READ_TIMEOUT);
        con.setConnectTimeout(CONNECT_TIMEOUT);

        if (!sslVerify && (con instanceof HttpsURLConnection)) {
            con = turnOffSSLVerification((HttpsURLConnection) con);
        }
        logprint("Sending...");
        OutputStream os;
        try {
            os = con.getOutputStream();
        } catch (SSLHandshakeException e) {
            e.printStackTrace();
            callback.updateStatus(STATUS_ENDPOINT_SSL_ERROR);
            return false;
        } catch (IOException e) {
            e.printStackTrace();
            callback.updateStatus(STATUS_ENDPOINT_UNKNOWN_HOST);
            return false;
        }

        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8));
        // Sending the map's k:v pairs separated with & after the first
        try {
            String toSend = "";
            logprint("Data:");
            for (String key : data.keySet()) {
                toSend += key + "=" + data.get(key);
                logprint(toSend);
                writer.write(toSend);
                toSend = "&";
            }
            writer.flush();
            writer.close();
            os.close();
            con.connect();
        } catch (IOException e) {
            e.printStackTrace();
        }

        logprint("Getting response...");
        int responsecode = 0;
        try {
            responsecode = con.getResponseCode();
        } catch (IOException e) {
            e.printStackTrace();
        }
        logprint("response code: " + responsecode);
        BufferedReader br;
        String line;
        StringBuilder response = new StringBuilder();
        try {
            br = new BufferedReader(new InputStreamReader(con.getInputStream()));
            while ((line = br.readLine()) != null) {
                response.append(line);
            }
            logprint("response: " + response.toString());
            callback.responseReceived(response.toString(), responsecode);
        } catch (IOException e) {
            callback.updateStatus(STATUS_ENDPOINT_ERROR);
            e.printStackTrace();
        }
        con.disconnect();
        return true;
    }

    private HttpsURLConnection turnOffSSLVerification(HttpsURLConnection con) {
        final TrustManager[] trustAllCerts = new TrustManager[]{
                new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
                    }

                    @Override
                    public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
                    }

                    @Override
                    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                        return new java.security.cert.X509Certificate[]{};
                    }
                }
        };
        SSLContext sslContext = null;
        try {
            sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        }
        final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
        con.setSSLSocketFactory(sslSocketFactory);
        con.setHostnameVerifier((hostname, session) -> true);
        logprint("SSL Verification is off.");
        return con;
    }
} 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 ENROLLMENT_CRED = "enrollment_credential"; public static final String FB_TOKEN = "fbtoken"; public static final int PRO_STATUS_DONE = 1004; public static final int PRO_STATUS_KEY_RECEIVED = 1005; 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_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 PUBKEY = "pubkey"; public static final String RESPONSE_DETAIL = "detail"; public static final String RESPONSE_PUBLIC_KEY = "public_key"; public static final String SERIAL = "serial"; public static final int STATUS_ENDPOINT_SENDING_COMPLETE = 4001; public static void logprint(String msg) {
    if (BuildConfig.DEBUG) {
        if (msg == null)
            return;
        Log.e(TAG, msg);
    }
}
import android.os.AsyncTask;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.PublicKey;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import it.netknights.piauthenticator.interfaces.EndpointCallback;
import it.netknights.piauthenticator.interfaces.PresenterTaskInterface;
import it.netknights.piauthenticator.model.Token;
import it.netknights.piauthenticator.utils.Endpoint;
import it.netknights.piauthenticator.utils.Util;
import static it.netknights.piauthenticator.utils.AppConstants.ENROLLMENT_CRED;
import static it.netknights.piauthenticator.utils.AppConstants.FB_TOKEN;
import static it.netknights.piauthenticator.utils.AppConstants.PRO_STATUS_DONE;
import static it.netknights.piauthenticator.utils.AppConstants.PRO_STATUS_KEY_RECEIVED;
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_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.PUBKEY;
import static it.netknights.piauthenticator.utils.AppConstants.RESPONSE_DETAIL;
import static it.netknights.piauthenticator.utils.AppConstants.RESPONSE_PUBLIC_KEY;
import static it.netknights.piauthenticator.utils.AppConstants.SERIAL;
import static it.netknights.piauthenticator.utils.AppConstants.STATUS_ENDPOINT_SENDING_COMPLETE;
import static it.netknights.piauthenticator.utils.Util.logprint;
/*
  privacyIDEA Authenticator

  Authors: Nils Behlen <nils.behlen@netknights.it>

  Copyright (c) 2017-2019 NetKnights GmbH

  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 it.netknights.piauthenticator.tasks;






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);