package de.tum.in.tumcampusapp.auxiliary; import android.content.Context; import android.util.Base64; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.UUID; import de.tum.in.tumcampusapp.api.TUMCabeClient; import de.tum.in.tumcampusapp.exceptions.NoPrivateKey; import de.tum.in.tumcampusapp.exceptions.NoPublicKey; import de.tum.in.tumcampusapp.models.tumcabe.ChatMember; import de.tum.in.tumcampusapp.models.tumcabe.DeviceRegister; import de.tum.in.tumcampusapp.models.tumcabe.TUMCabeStatus; import de.tum.in.tumcampusapp.models.tumo.TokenConfirmation; import de.tum.in.tumcampusapp.services.GcmIdentificationService; import de.tum.in.tumcampusapp.tumonline.TUMOnlineConst; import de.tum.in.tumcampusapp.tumonline.TUMOnlineRequest; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; /** * This provides methods to authenticate this app installation with the tumcabe server and other instances requiring a pki */ public class AuthenticationManager { private final static String ALGORITHM = "RSA"; private final static int RSA_KEY_SIZE = 1024; private static String uniqueID; private final Context mContext; public AuthenticationManager(Context c) { mContext = c; } /** * Gets an unique id that identifies this device * should only reset after a reinstall or wiping of the settings * * @return Unique device id */ public static synchronized String getDeviceID(Context context) { if (uniqueID == null) { uniqueID = Utils.getInternalSettingString(context, Const.PREF_UNIQUE_ID, null); if (uniqueID == null) { uniqueID = UUID.randomUUID().toString(); Utils.setInternalSetting(context, Const.PREF_UNIQUE_ID, uniqueID); } } return uniqueID; } public static KeyPairGenerator getKeyPairGeneratorInstance() { try { return KeyPairGenerator.getInstance(ALGORITHM); } catch (NoSuchAlgorithmException e) { // We don't support platforms without RSA throw new AssertionError(e); } } public static KeyFactory getKeyFactoryInstance() { try { return KeyFactory.getInstance(ALGORITHM); } catch (NoSuchAlgorithmException e) { // We don't support platforms without RSA throw new AssertionError(e); } } /** * Get the private key as string * * @return * @throws NoPrivateKey */ private String getPrivateKeyString() throws NoPrivateKey { String key = Utils.getInternalSettingString(mContext, Const.PRIVATE_KEY, ""); if (key.isEmpty()) { throw new NoPrivateKey(); } return key; } /** * Gets the public key as string * * @return * @throws NoPublicKey */ public String getPublicKeyString() throws NoPublicKey { String key = Utils.getInternalSettingString(mContext, Const.PUBLIC_KEY, ""); if (key.isEmpty()) { throw new NoPublicKey(); } return key; } /** * Loads the private key as an object * * @return The private key object */ private PrivateKey getPrivateKey() throws NoPrivateKey { byte[] privateKeyBytes = Base64.decode(this.getPrivateKeyString(), Base64.DEFAULT); try { return getKeyFactoryInstance().generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes)); } catch (InvalidKeySpecException e) { Utils.log(e); } return null; } /** * Sign a message with the currently stored private key * * @param data String to be signed * @return signature used to verify this request * @throws NoPrivateKey */ public String sign(String data) throws NoPrivateKey { RSASigner signer = new RSASigner(this.getPrivateKey()); return signer.sign(data); } /** * Gets private key from preferences or generates one. * * @return true if a private key is present */ public boolean generatePrivateKey(ChatMember member) { // Try to retrieve private key try { //Try to get the private key this.getPrivateKeyString(); //Reupload it in the case it was not yet transmitted to the server this.uploadKey(this.getPublicKeyString(), member); // If we already have one don't create a new one return true; } catch (NoPrivateKey | NoPublicKey e) { //NOPMD //Otherwise catch a not existing private key exception and proceed generation } //Something went wrong, generate a new pair this.clearKeys(); // If the key is not in shared preferences, a new generate key-pair KeyPair keyPair = generateKeyPair(); //In order to store the preferences we need to encode them as base64 string String publicKeyString = keyToBase64(keyPair.getPublic().getEncoded()); String privateKeyString = keyToBase64(keyPair.getPrivate().getEncoded()); this.saveKeys(privateKeyString, publicKeyString); //New keys, need to re-upload this.uploadKey(publicKeyString, member); return true; } /** * Try to upload the public key to the server and remember that state * * @param publicKey */ private void uploadKey(String publicKey, final ChatMember member) { //If we already uploaded it we don't need to redo that if (Utils.getInternalSettingBool(mContext, Const.PUBLIC_KEY_UPLOADED, false)) { this.tryToUploadGcmToken(); return; } try { DeviceRegister dr = new DeviceRegister(mContext, publicKey, member); // Upload public key to the server TUMCabeClient.getInstance(mContext).deviceRegister(dr, new Callback<TUMCabeStatus>() { @Override public void onResponse(Call<TUMCabeStatus> call, Response<TUMCabeStatus> response) { //Remember that we are done, only if we have submitted with the member information if (response.isSuccessful() && "ok".equals(response.body().getStatus())) { if (member != null) { Utils.setInternalSetting(mContext, Const.PUBLIC_KEY_UPLOADED, true); } AuthenticationManager.this.tryToUploadGcmToken(); } } @Override public void onFailure(Call<TUMCabeStatus> call, Throwable t) { Utils.log(t, "Failure uploading public key"); Utils.setInternalSetting(mContext, Const.PUBLIC_KEY_UPLOADED, false); } }); } catch (NoPrivateKey noPrivateKey) { this.clearKeys(); } } public void uploadPublicKey() throws NoPublicKey { final String publicKey = this.getPublicKeyString(); Thread thread = new Thread() { @Override public void run() { //Upload the Private key to the tumo server: we don't need an activated token for that. We want this to happen immediately so that no one else can upload this secret. TUMOnlineRequest<TokenConfirmation> requestSavePublicKey = new TUMOnlineRequest<>(TUMOnlineConst.SECRET_UPLOAD, AuthenticationManager.this.mContext, false); requestSavePublicKey.setParameter("pToken", Utils.getSetting(AuthenticationManager.this.mContext, Const.ACCESS_TOKEN, "")); requestSavePublicKey.setParameterEncoded("pSecret", publicKey); requestSavePublicKey.fetch(); } }; thread.start(); } private void tryToUploadGcmToken() { // Check device for Play Services APK. If check succeeds, proceed with GCM registration. // Can only be done after the public key has been uploaded if (Utils.getInternalSettingBool(mContext, Const.PUBLIC_KEY_UPLOADED, false) && GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(mContext) == ConnectionResult.SUCCESS) { GcmIdentificationService idService = new GcmIdentificationService(mContext); idService.checkSetup(); } } /** * Convert a byte array to a more manageable base64 string to store it in the preferences */ private static String keyToBase64(byte[] key) { return Base64.encodeToString(key, Base64.DEFAULT); } /** * Generates a keypair with the given ALGORITHM & size */ private static KeyPair generateKeyPair() { KeyPairGenerator keyGen = getKeyPairGeneratorInstance(); keyGen.initialize(AuthenticationManager.RSA_KEY_SIZE); return keyGen.generateKeyPair(); } /** * Save private key in shared preferences */ private void saveKeys(String privateKeyString, String publicKeyString) { Utils.setInternalSetting(mContext, Const.PRIVATE_KEY, privateKeyString); Utils.setInternalSetting(mContext, Const.PRIVATE_KEY_ACTIVE, false); //We need to remember this state in order to activate it later Utils.setInternalSetting(mContext, Const.PUBLIC_KEY, publicKeyString); } /** * Reset all keys generated - this should actually never happen other than when a token is reset */ public void clearKeys() { this.saveKeys("", ""); Utils.setInternalSetting(mContext, Const.PUBLIC_KEY_UPLOADED, false); } }