/* * Nextcloud Android client application * * @author Mario Danic * Copyright (C) 2017-2018 Mario Danic * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package com.owncloud.android.utils; import android.accounts.Account; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.content.Context; import android.text.TextUtils; import android.util.Base64; import android.util.Log; import com.google.gson.Gson; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.authentication.AccountUtils; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.PushConfigurationState; import com.owncloud.android.datamodel.SignatureVerification; import com.owncloud.android.db.PreferenceManager; import com.owncloud.android.lib.common.OwnCloudAccount; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; import com.owncloud.android.lib.common.operations.RemoteOperation; import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.notifications.RegisterAccountDeviceForNotificationsOperation; import com.owncloud.android.lib.resources.notifications.RegisterAccountDeviceForProxyOperation; import com.owncloud.android.lib.resources.notifications.UnregisterAccountDeviceForNotificationsOperation; import com.owncloud.android.lib.resources.notifications.UnregisterAccountDeviceForProxyOperation; import com.owncloud.android.lib.resources.notifications.models.PushResponse; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.security.InvalidKeyException; import java.security.Key; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Locale; public class PushUtils { public static final String KEY_PUSH = "push"; private static final String TAG = "PushUtils"; private static final String KEYPAIR_FOLDER = "nc-keypair"; private static final String KEYPAIR_FILE_NAME = "push_key"; private static final String KEYPAIR_PRIV_EXTENSION = ".priv"; private static final String KEYPAIR_PUB_EXTENSION = ".pub"; private static ArbitraryDataProvider arbitraryDataProvider; public static String generateSHA512Hash(String pushToken) { MessageDigest messageDigest = null; try { messageDigest = MessageDigest.getInstance("SHA-512"); messageDigest.update(pushToken.getBytes()); return bytesToHex(messageDigest.digest()); } catch (NoSuchAlgorithmException e) { Log_OC.d(TAG, "SHA-512 algorithm not supported"); } return ""; } public static String bytesToHex(byte[] bytes) { StringBuilder result = new StringBuilder(); for (byte individualByte : bytes) { result.append(Integer.toString((individualByte & 0xff) + 0x100, 16) .substring(1)); } return result.toString(); } private static int generateRsa2048KeyPair() { migratePushKeys(); String keyPath = MainApp.getAppContext().getFilesDir().getAbsolutePath() + File.separator + MainApp.getDataFolder() + File.separator + KEYPAIR_FOLDER; String privateKeyPath = keyPath + File.separator + KEYPAIR_FILE_NAME + KEYPAIR_PRIV_EXTENSION; String publicKeyPath = keyPath + File.separator + KEYPAIR_FILE_NAME + KEYPAIR_PUB_EXTENSION; File keyPathFile = new File(keyPath); if (!new File(privateKeyPath).exists() && !new File(publicKeyPath).exists()) { try { if (!keyPathFile.exists()) { keyPathFile.mkdir(); } KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); KeyPair pair = keyGen.generateKeyPair(); int statusPrivate = saveKeyToFile(pair.getPrivate(), privateKeyPath); int statusPublic = saveKeyToFile(pair.getPublic(), publicKeyPath); if (statusPrivate == 0 && statusPublic == 0) { // all went well return 0; } else { return -2; } } catch (NoSuchAlgorithmException e) { Log_OC.d(TAG, "RSA algorithm not supported"); } } else { // we already have the key return -1; } // we failed to generate the key return -2; } private static void deleteRegistrationForAccount(Account account) { Context context = MainApp.getAppContext(); OwnCloudAccount ocAccount = null; arbitraryDataProvider = new ArbitraryDataProvider(MainApp.getAppContext().getContentResolver()); try { ocAccount = new OwnCloudAccount(account, context); OwnCloudClient mClient = OwnCloudClientManagerFactory.getDefaultSingleton(). getClientFor(ocAccount, context); RemoteOperation unregisterAccountDeviceForNotificationsOperation = new UnregisterAccountDeviceForNotificationsOperation(); RemoteOperationResult remoteOperationResult = unregisterAccountDeviceForNotificationsOperation. execute(mClient); if (remoteOperationResult.getHttpCode() == HttpStatus.SC_ACCEPTED) { String arbitraryValue; if (!TextUtils.isEmpty(arbitraryValue = arbitraryDataProvider.getValue(account, KEY_PUSH))) { Gson gson = new Gson(); PushConfigurationState pushArbitraryData = gson.fromJson(arbitraryValue, PushConfigurationState.class); RemoteOperation unregisterAccountDeviceForProxyOperation = new UnregisterAccountDeviceForProxyOperation(context.getResources(). getString(R.string.push_server_url), pushArbitraryData.getDeviceIdentifier(), pushArbitraryData.getDeviceIdentifierSignature(), pushArbitraryData.getUserPublicKey()); remoteOperationResult = unregisterAccountDeviceForProxyOperation.execute(mClient); if (remoteOperationResult.isSuccess()) { arbitraryDataProvider.deleteKeyForAccount(account.name, KEY_PUSH); } } } } catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException e) { Log_OC.d(TAG, "Failed to find an account"); } catch (AuthenticatorException e) { Log_OC.d(TAG, "Failed via AuthenticatorException"); } catch (IOException e) { Log_OC.d(TAG, "Failed via IOException"); } catch (OperationCanceledException e) { Log_OC.d(TAG, "Failed via OperationCanceledException"); } } public static void pushRegistrationToServer() { String token = PreferenceManager.getPushToken(MainApp.getAppContext()); arbitraryDataProvider = new ArbitraryDataProvider(MainApp.getAppContext().getContentResolver()); if (!TextUtils.isEmpty(MainApp.getAppContext().getResources().getString(R.string.push_server_url)) && !TextUtils.isEmpty(token)) { PushUtils.generateRsa2048KeyPair(); String pushTokenHash = PushUtils.generateSHA512Hash(token).toLowerCase(Locale.ROOT); PublicKey devicePublicKey = (PublicKey) PushUtils.readKeyFromFile(true); if (devicePublicKey != null) { byte[] publicKeyBytes = Base64.encode(devicePublicKey.getEncoded(), Base64.NO_WRAP); String publicKey = new String(publicKeyBytes); publicKey = publicKey.replaceAll("(.{64})", "$1\n"); publicKey = "-----BEGIN PUBLIC KEY-----\n" + publicKey + "\n-----END PUBLIC KEY-----\n"; Context context = MainApp.getAppContext(); String providerValue; PushConfigurationState accountPushData = null; Gson gson = new Gson(); for (Account account : AccountUtils.getAccounts(context)) { providerValue = arbitraryDataProvider.getValue(account, KEY_PUSH); if (!TextUtils.isEmpty(providerValue)) { accountPushData = gson.fromJson(providerValue, PushConfigurationState.class); } else { accountPushData = null; } if (accountPushData != null && !accountPushData.getPushToken().equals(token) && !accountPushData.isShouldBeDeleted() || TextUtils.isEmpty(providerValue)) { try { OwnCloudAccount ocAccount = new OwnCloudAccount(account, context); OwnCloudClient mClient = OwnCloudClientManagerFactory.getDefaultSingleton(). getClientFor(ocAccount, context); RemoteOperation registerAccountDeviceForNotificationsOperation = new RegisterAccountDeviceForNotificationsOperation(pushTokenHash, publicKey, context.getResources().getString(R.string.push_server_url)); RemoteOperationResult remoteOperationResult = registerAccountDeviceForNotificationsOperation.execute(mClient); if (remoteOperationResult.isSuccess()) { PushResponse pushResponse = remoteOperationResult.getPushResponseData(); RemoteOperation registerAccountDeviceForProxyOperation = new RegisterAccountDeviceForProxyOperation( context.getResources().getString(R.string.push_server_url), token, pushResponse.getDeviceIdentifier(), pushResponse.getSignature(), pushResponse.getPublicKey()); remoteOperationResult = registerAccountDeviceForProxyOperation.execute(mClient); if (remoteOperationResult.isSuccess()) { PushConfigurationState pushArbitraryData = new PushConfigurationState(token, pushResponse.getDeviceIdentifier(), pushResponse.getSignature(), pushResponse.getPublicKey(), false); arbitraryDataProvider.storeOrUpdateKeyValue(account.name, KEY_PUSH, gson.toJson(pushArbitraryData)); } } else if (remoteOperationResult.getCode() == RemoteOperationResult.ResultCode.ACCOUNT_USES_STANDARD_PASSWORD) { arbitraryDataProvider.storeOrUpdateKeyValue(account.name, AccountUtils.ACCOUNT_USES_STANDARD_PASSWORD, "true"); } } catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException e) { Log_OC.d(TAG, "Failed to find an account"); } catch (AuthenticatorException e) { Log_OC.d(TAG, "Failed via AuthenticatorException"); } catch (IOException e) { Log_OC.d(TAG, "Failed via IOException"); } catch (OperationCanceledException e) { Log_OC.d(TAG, "Failed via OperationCanceledException"); } } else if (accountPushData != null && accountPushData.isShouldBeDeleted()) { deleteRegistrationForAccount(account); } } } } } public static Key readKeyFromFile(boolean readPublicKey) { String keyPath = MainApp.getAppContext().getFilesDir().getAbsolutePath() + File.separator + MainApp.getDataFolder() + File.separator + KEYPAIR_FOLDER; String privateKeyPath = keyPath + File.separator + KEYPAIR_FILE_NAME + KEYPAIR_PRIV_EXTENSION; String publicKeyPath = keyPath + File.separator + KEYPAIR_FILE_NAME + KEYPAIR_PUB_EXTENSION; String path; if (readPublicKey) { path = publicKeyPath; } else { path = privateKeyPath; } FileInputStream fileInputStream = null; try { fileInputStream = new FileInputStream(path); byte[] bytes = new byte[fileInputStream.available()]; fileInputStream.read(bytes); fileInputStream.close(); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); if (readPublicKey) { X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes); return keyFactory.generatePublic(keySpec); } else { PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes); return keyFactory.generatePrivate(keySpec); } } catch (FileNotFoundException e) { Log_OC.d(TAG, "Failed to find path while reading the Key"); } catch (IOException e) { Log_OC.d(TAG, "IOException while reading the key"); } catch (InvalidKeySpecException e) { Log_OC.d(TAG, "InvalidKeySpecException while reading the key"); } catch (NoSuchAlgorithmException e) { Log_OC.d(TAG, "RSA algorithm not supported"); } return null; } private static int saveKeyToFile(Key key, String path) { byte[] encoded = key.getEncoded(); FileOutputStream keyFileOutputStream = null; try { if (!new File(path).exists()) { File newFile = new File(path); newFile.getParentFile().mkdirs(); newFile.createNewFile(); } keyFileOutputStream = new FileOutputStream(path); keyFileOutputStream.write(encoded); keyFileOutputStream.close(); return 0; } catch (FileNotFoundException e) { Log_OC.d(TAG, "Failed to save key to file"); } catch (IOException e) { Log_OC.d(TAG, "Failed to save key to file via IOException"); } return -1; } public static void reinitKeys() { Context context = MainApp.getAppContext(); Account[] accounts = AccountUtils.getAccounts(context); for (Account account : accounts) { deleteRegistrationForAccount(account); } String keyPath = context.getDir("nc-keypair", Context.MODE_PRIVATE).getAbsolutePath(); File privateKeyFile = new File(keyPath, "push_key.priv"); File publicKeyFile = new File(keyPath, "push_key.pub"); FileUtils.deleteQuietly(privateKeyFile); FileUtils.deleteQuietly(publicKeyFile); pushRegistrationToServer(); PreferenceManager.setKeysReInit(context); } private static void migratePushKeys() { Context context = MainApp.getAppContext(); if (!PreferenceManager.getKeysMigration(context)) { String oldKeyPath = MainApp.getStoragePath() + File.separator + MainApp.getDataFolder() + File.separator + "nc-keypair"; File oldPrivateKeyFile = new File(oldKeyPath, "push_key.priv"); File oldPublicKeyFile = new File(oldKeyPath, "push_key.pub"); String keyPath = context.getDir("nc-keypair", Context.MODE_PRIVATE).getAbsolutePath(); File privateKeyFile = new File(keyPath, "push_key.priv"); File publicKeyFile = new File(keyPath, "push_key.pub"); if ((privateKeyFile.exists() && publicKeyFile.exists()) || (!oldPrivateKeyFile.exists() && !oldPublicKeyFile.exists())) { PreferenceManager.setKeysMigration(context, true); } else { if (oldPrivateKeyFile.exists()) { try { FileStorageUtils.moveFile(oldPrivateKeyFile, privateKeyFile); } catch (IOException e) { Log.e(TAG, "Failed to move old private key to new location"); } } if (oldPublicKeyFile.exists()) { try { FileStorageUtils.moveFile(oldPublicKeyFile, publicKeyFile); } catch (IOException e) { Log.e(TAG, "Failed to move old public key to new location"); } } if (privateKeyFile.exists() && publicKeyFile.exists()) { PreferenceManager.setKeysMigration(context, true); } } } } public SignatureVerification verifySignature(Context context, byte[] signatureBytes, byte[] subjectBytes) { Signature signature = null; PublicKey publicKey; SignatureVerification signatureVerification = new SignatureVerification(); signatureVerification.setSignatureValid(false); Account[] userEntities = AccountUtils.getAccounts(context); ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(context.getContentResolver()); String arbitraryValue; Gson gson = new Gson(); PushConfigurationState pushArbitraryData; try { signature = Signature.getInstance("SHA512withRSA"); if (userEntities.length > 0) { for (Account userEntity : userEntities) { if (!TextUtils.isEmpty(arbitraryValue = arbitraryDataProvider.getValue(userEntity, KEY_PUSH))) { pushArbitraryData = gson.fromJson(arbitraryValue, PushConfigurationState.class); publicKey = (PublicKey) readKeyFromString(true, pushArbitraryData.getUserPublicKey()); signature.initVerify(publicKey); signature.update(subjectBytes); if (signature.verify(signatureBytes)) { signatureVerification.setSignatureValid(true); signatureVerification.setAccount(userEntity); return signatureVerification; } } } } } catch (NoSuchAlgorithmException e) { Log.d(TAG, "No such algorithm"); } catch (InvalidKeyException e) { Log.d(TAG, "Invalid key while trying to verify"); } catch (SignatureException e) { Log.d(TAG, "Signature exception while trying to verify"); } return signatureVerification; } private Key readKeyFromString(boolean readPublicKey, String keyString) { if (readPublicKey) { keyString = keyString.replaceAll("\\n", "").replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", ""); } else { keyString = keyString.replaceAll("\\n", "").replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", ""); } KeyFactory keyFactory = null; try { keyFactory = KeyFactory.getInstance("RSA"); if (readPublicKey) { X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.decode(keyString, Base64.DEFAULT)); return keyFactory.generatePublic(keySpec); } else { PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(keyString, Base64.DEFAULT)); return keyFactory.generatePrivate(keySpec); } } catch (NoSuchAlgorithmException e) { Log.d("TAG", "No such algorithm while reading key from string"); } catch (InvalidKeySpecException e) { Log.d("TAG", "Invalid key spec while reading key from string"); } return null; } }