com.ntsync.android.sync.client.ClientKeyHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.ntsync.android.sync.client.ClientKeyHelper.java

Source

package com.ntsync.android.sync.client;

/*
 * Copyright (C) 2014 Markus Grieder
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>. 
 */

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.SecureRandom;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.apache.http.auth.AuthenticationException;
import org.spongycastle.crypto.DataLengthException;
import org.spongycastle.crypto.InvalidCipherTextException;
import org.spongycastle.crypto.modes.AEADBlockCipher;
import org.spongycastle.crypto.params.AEADParameters;
import org.spongycastle.crypto.params.KeyParameter;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.NetworkErrorException;
import android.accounts.OperationCanceledException;
import android.annotation.SuppressLint;
import android.content.Context;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;

import com.ntsync.android.sync.shared.Constants;
import com.ntsync.shared.CryptoHelper;
import com.ntsync.shared.KeyGenerator;
import com.ntsync.shared.Pair;
import com.ntsync.shared.PasswortGenerator;
import com.ntsync.shared.SyncDataHelper;

@SuppressLint("TrulyRandom")
public final class ClientKeyHelper {

    private static final int PWD_WORD_LEN = 5;
    private static final int UPC_MULTIPLIER = 3;
    private static final int UPC_MODVAL = 10;
    private static final int UPC_NR_LEN = 11;

    private ClientKeyHelper() {

    }

    private static final String PRIVATE_KEY = "com.myllih.android.sync.key";
    private static final String PRIVATE_PWD = "com.myllih.android.sync.keypwd";
    private static final String PRIVATE_KEYSALT = "com.myllih.android.sync.keysalt";
    private static final String PRIVATE_PWDCHECK = "com.myllih.android.sync.pwdcheck";
    private static final String PRIVATE_KEY_SALTSAVED = "com.myllih.android.sync.keysaltsaved";

    private static final String TAG = "ClientKeyHelper";

    /** Length of Salt for Key in byte (current: 64 bits) */
    public static final int SALT_LENGHT = 8;

    private static final int IV_LENGTH = CryptoHelper.IV_LEN;

    /**
     * 
     * Get Private Key or return null.
     * 
     * @param account
     *            the account we're syncing
     * @return Private Key
     */
    public static SecretKey getPrivateKey(Account account, AccountManager accountManager) {
        // TODO: Redesign more secure location to safe private key?
        String keyString = accountManager.getUserData(account, PRIVATE_KEY);
        SecretKey key = null;
        if (!TextUtils.isEmpty(keyString)) {
            key = new SecretKeySpec(Base64.decode(keyString, Base64.DEFAULT), "AES");
        }
        return key;
    }

    /**
     * 
     * Get Salt for current Private Key generation or return null.
     * 
     * @param account
     *            the account we're syncing
     * @return Salt as Base64 Encoded
     */
    public static String getSalt(Account account, AccountManager accountManager) {
        String saltString = accountManager.getUserData(account, PRIVATE_KEYSALT);
        if (TextUtils.isEmpty(saltString)) {
            saltString = null;
        }
        return saltString;
    }

    /**
     * 
     * Get Password-Check-Value or null
     * 
     * @param account
     * @return Password-Check as Base64 Encoded or null
     */
    public static String getPwdCheck(Account account, AccountManager accountManager) {
        String saltString = accountManager.getUserData(account, PRIVATE_PWDCHECK);
        if (TextUtils.isEmpty(saltString)) {
            saltString = null;
        }
        return saltString;
    }

    /**
     * Separate the Salt and PwdCheck-Values in separate byte-Arrays
     * 
     * @param saltPwdCheck
     *            null is not allowed.
     * @return salt and PwdCheck never null
     */
    public static Pair<byte[], byte[]> splitSaltPwdCheck(byte[] saltPwdCheck) {
        byte[] salt = new byte[ClientKeyHelper.SALT_LENGHT];
        System.arraycopy(saltPwdCheck, 0, salt, 0, salt.length);
        byte[] pwdCheck = new byte[saltPwdCheck.length - salt.length];
        System.arraycopy(saltPwdCheck, salt.length, pwdCheck, 0, pwdCheck.length);
        return new Pair<byte[], byte[]>(salt, pwdCheck);
    }

    /**
     * 
     * Get Private Key or create a new one.
     * 
     * @param account
     *            the account we're syncing
     * @return Private Key
     * @throws InvalidKeyException
     */
    @SuppressLint("TrulyRandom")
    public static SecretKey getOrCreatePrivateKey(Account account, AccountManager accountManager)
            throws IOException, InvalidKeyException {
        SecretKey key = getPrivateKey(account, accountManager);
        if (key == null) {
            Log.i(TAG, "Create new private Key");

            String pwd = PasswortGenerator.createPwd(PWD_WORD_LEN);

            SecureRandom random = new SecureRandom();
            byte[] salt = new byte[SALT_LENGHT];
            random.nextBytes(salt);

            key = createKey(account, accountManager, pwd, salt, false, null);
        }
        return key;
    }

    /**
     * 
     * @param account
     * @param accountManager
     * @return true if Salt was saved on the server
     */
    public static boolean isSaltSaved(Account account, AccountManager accountManager) {
        return Boolean.valueOf(accountManager.getUserData(account, PRIVATE_KEY_SALTSAVED));
    }

    /**
     * Set Salt saved to true
     * 
     * @param account
     * @param accountManager
     */
    public static void setSaltSaved(Account account, AccountManager accountManager) {
        accountManager.setUserData(account, PRIVATE_KEY_SALTSAVED, "true");
    }

    /**
     * 
     * @param account
     * @param accountManager
     * @param keyPwd
     *            Password for Key
     * @param salt
     * @param existingSalt
     * @param pwdCheck
     *            null for new Key otherwise used to Check if it is the right
     *            Password.
     * @return
     * @throws InvalidKeyException
     * @throws UnsupportedEncodingException
     */
    public static SecretKey createKey(Account account, AccountManager accountManager, String keyPwd, byte[] salt,
            boolean existingSalt, byte[] pwdCheck) throws InvalidKeyException, UnsupportedEncodingException {

        KeyGenerator keyGen = new KeyGenerator();
        SecretKey skey = keyGen.generateKey(keyPwd, salt);

        byte[] raw = skey.getEncoded();
        String keyValue = Base64.encodeToString(raw, Base64.DEFAULT);
        String saltStr = Base64.encodeToString(salt, Base64.DEFAULT);

        assert (existingSalt ? pwdCheck != null : true);

        byte[] check = pwdCheck;
        if (existingSalt && pwdCheck != null) {
            // Validate new Passwort
            validateKey(check, skey);

        } else if (!existingSalt) {
            check = createPwdCheck(skey);
        }
        String pwdCheckStr = check != null ? Base64.encodeToString(check, Base64.DEFAULT) : null;

        accountManager.setUserData(account, PRIVATE_KEY_SALTSAVED, existingSalt ? "true" : "false");
        accountManager.setUserData(account, PRIVATE_KEYSALT, saltStr);
        accountManager.setUserData(account, PRIVATE_PWDCHECK, pwdCheckStr);
        accountManager.setUserData(account, PRIVATE_PWD, keyPwd);
        accountManager.setUserData(account, PRIVATE_KEY, keyValue);
        return skey;
    }

    private static void validateKey(byte[] check, SecretKey skey) throws InvalidKeyException {
        AEADBlockCipher cipher = CryptoHelper.getCipher();
        try {
            // data, pos, IV_LEN)
            byte[] iv = new byte[CryptoHelper.IV_LEN];
            System.arraycopy(check, 0, iv, 0, CryptoHelper.IV_LEN);
            cipher.init(false, new AEADParameters(new KeyParameter(skey.getEncoded()), CryptoHelper.MAC_SIZE, iv));
            byte[] original = CryptoHelper.cipherData(cipher, check, IV_LENGTH, check.length - IV_LENGTH);

            String orgValue = new String(original, SyncDataHelper.DEFAULT_CHARSET_NAME);
            // Validate Checksum
            int res1 = calcUpcChecksum(orgValue);
            int len = orgValue.length();
            if (orgValue.length() == 0 || res1 != Integer.parseInt(orgValue.substring(len - 1, len))) {
                throw new InvalidKeyException("Invalid Key. Checksum error");
            }
        } catch (NumberFormatException ex) {
            throw new InvalidKeyException("Invalid Key. Data is not number.", ex);
        } catch (DataLengthException e) {
            throw new InvalidKeyException("Invalid Key", e);
        } catch (IllegalStateException e) {
            throw new InvalidKeyException("Invalid Key. Wrong Parameter.", e);
        } catch (InvalidCipherTextException e) {
            throw new InvalidKeyException("Invalid Key.", e);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("No support for UTF-8 available.", e);
        }
    }

    private static byte[] createPwdCheck(SecretKey skey) throws InvalidKeyException, UnsupportedEncodingException {
        byte[] iv = new byte[IV_LENGTH];
        SecureRandom random = new SecureRandom();
        random.nextBytes(iv);
        AEADBlockCipher ecipher = CryptoHelper.getCipher();
        byte[] checkData;
        try {
            ecipher.init(true, new AEADParameters(new KeyParameter(skey.getEncoded()), CryptoHelper.MAC_SIZE, iv));

            // create random integer with checksum (UPC-Format : 12 digits)
            String testValue = String.format("%011d", random.nextInt(Integer.MAX_VALUE)) + "0";
            int res1 = calcUpcChecksum(testValue);

            testValue = testValue.substring(0, UPC_NR_LEN) + res1;

            byte[] pwdCheck = CryptoHelper.cipherData(ecipher,
                    testValue.getBytes(SyncDataHelper.DEFAULT_CHARSET_NAME));

            checkData = new byte[iv.length + pwdCheck.length];
            System.arraycopy(iv, 0, checkData, 0, iv.length);
            System.arraycopy(pwdCheck, 0, checkData, iv.length, pwdCheck.length);
        } catch (DataLengthException e) {
            throw new InvalidKeyException(e.getMessage(), e);
        } catch (IllegalStateException e) {
            throw new InvalidKeyException(e.getMessage(), e);
        } catch (InvalidCipherTextException e) {
            throw new InvalidKeyException(e.getMessage(), e);
        }

        return checkData;
    }

    private static int calcUpcChecksum(String testValue) {
        int res1 = 0;
        int res2 = 0;
        for (int i = 0; i < UPC_NR_LEN; i += 2) {
            res1 += Integer.parseInt(testValue.substring(i, i + 1));
        }
        for (int i = 1; i < UPC_NR_LEN; i += 2) {
            res2 += Integer.parseInt(testValue.substring(i, i + 1));
        }

        int checksum = ((res1 * UPC_MULTIPLIER + res2) % UPC_MODVAL);
        if (checksum > 0) {
            checksum = UPC_MODVAL - checksum;
        }
        return checksum;
    }

    public static String getKeyPwd(Account account, AccountManager accountManager) {
        return accountManager.getUserData(account, PRIVATE_PWD);
    }

    /**
     * Clears all saved Data about the Private Key
     * 
     * @param account
     * @param acManager
     */
    public static void clearPrivateKeyData(Account account, AccountManager acManager) {
        acManager.setUserData(account, PRIVATE_KEYSALT, null);
        acManager.setUserData(account, PRIVATE_PWDCHECK, null);
        acManager.setUserData(account, PRIVATE_PWD, null);
        acManager.setUserData(account, PRIVATE_KEY, null);
        acManager.setUserData(account, PRIVATE_KEY_SALTSAVED, null);
    }

    /**
     * Checks if the key-data is ready for sync: Private Key is available for
     * encryption or not another key is already save on the server.
     * 
     * @param context
     * @param account
     * @param am
     * @param authToken
     * @return false is Sync is not ready. Additionally input will be required.
     * @throws AuthenticatorException
     * @throws OperationCanceledException
     */
    public static PrivateKeyState isReadyForSync(Context context, Account account, AccountManager am,
            String authToken) throws OperationCanceledException {
        SecretKey key = getPrivateKey(account, am);
        PrivateKeyState state = PrivateKeyState.READY;
        if (key == null || !ClientKeyHelper.isSaltSaved(account, am)) {
            try {
                // Check ob PwdSalt von einem anderen Client vorhanden
                // ist
                byte[] saltPwdCheck = NetworkUtilities.getKeySalt(context, account.name, authToken);
                if (saltPwdCheck != null && saltPwdCheck.length > SALT_LENGHT) {
                    // Ein Key ist bereits vorhanden -> Key muss eingegeben
                    // werden.
                    state = PrivateKeyState.MISSING_KEY;
                    state.setCurrSalt(saltPwdCheck);
                }
            } catch (AuthenticationException e) {
                Log.w(TAG, "Check for PwdSalt failed with Authentification error", e);
                am.invalidateAuthToken(Constants.ACCOUNT_TYPE, authToken);
                state = PrivateKeyState.AUTH_FAILED;
            } catch (NetworkErrorException e) {
                Log.w(TAG, "Check for PwdSalt failed with NetworkErrorException.", e);
                state = PrivateKeyState.NETWORK_ERROR;
                state.setErrorMsg(e.getLocalizedMessage());
            } catch (ServerException e) {
                Log.w(TAG, "Check for PwdSalt failed with ServerError.", e);
                state = PrivateKeyState.CHECK_FAILED;
            }
        }
        return state;
    }

    public static enum PrivateKeyState {
        /** Private Key is Ready for Sync */
        READY,
        /**
         * Another Key is in use, User has to enter Key-Pwd.
         * {@link #getCurrSalt()} contains the current Key-Salt and PwdCheck
         * from the server
         */
        MISSING_KEY,
        /** Check failed, mostly because of an Server-Error, try later again. */
        CHECK_FAILED, AUTH_FAILED, NETWORK_ERROR;

        private byte[] currSalt;

        private String errorMsg;

        /**
         * @return a KeySalt and PwdCheck for {@link #MISSING_KEY}
         */
        public byte[] getCurrSalt() {
            return currSalt;
        }

        /**
         * @param currSalt
         *            KeySalt and PwdCheck for {@link #MISSING_KEY}
         */
        public void setCurrSalt(byte[] currSalt) {
            this.currSalt = currSalt;
        }

        /**
         * 
         * @return Error-Message for {@link #NETWORK_ERROR}
         */
        public String getErrorMsg() {
            return errorMsg;
        }

        /**
         * @param errorMsg
         *            Error-Message for {@link #NETWORK_ERROR}
         */
        public void setErrorMsg(String errorMsg) {
            this.errorMsg = errorMsg;
        }
    }
}