Skip to content
This repository has been archived by the owner on Feb 12, 2025. It is now read-only.

Migrate Google Drive API to the REST V3 API #213

Merged
merged 3 commits into from
Oct 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ android {
packagingOptions {
exclude 'META-INF/LICENSE*'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/DEPENDENCIES'
merge 'reference.conf'
}
externalNativeBuild {
Expand All @@ -92,14 +93,26 @@ dependencies {
exclude group: 'com.android.support', module: 'support-annotations'
})
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.media:media:1.0.1'
implementation 'androidx.media:media:1.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.code.findbugs:jsr305:3.0.2'
implementation 'com.google.android.gms:play-services-drive:17.0.0'
implementation 'com.google.android.gms:play-services-auth:17.0.0'

// gdrive api rest v3 + google auth
def google_drive_rest_version = "v3-rev173-1.25.0"
def google_play_auth_version = "17.0.0"
def google_api_client_version = "1.26.0"
implementation "com.google.apis:google-api-services-drive:$google_drive_rest_version"
implementation("com.google.android.gms:play-services-auth:$google_play_auth_version") {
exclude group: 'org.apache.httpcomponents'
}
implementation "com.google.http-client:google-http-client-gson:$google_api_client_version"
implementation("com.google.api-client:google-api-client-android:$google_api_client_version") {
exclude group: 'org.apache.httpcomponents'
}

implementation('androidx.work:work-runtime:2.0.1') {
exclude group: 'com.google.guava', module: 'listenablefuture'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,23 @@
package fr.acinq.eclair.crypto;

import com.tozny.crypto.android.AesCbcWithIntegrity;
import fr.acinq.bitcoin.DeterministicWallet;
import fr.acinq.eclair.wallet.utils.EncryptedBackup;
import fr.acinq.eclair.wallet.utils.EncryptedData;

import org.junit.Assert;
import org.junit.Test;
import scodec.bits.ByteVector;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;

import fr.acinq.bitcoin.DeterministicWallet;
import fr.acinq.eclair.wallet.utils.EncryptedBackup;
import fr.acinq.eclair.wallet.utils.EncryptedData;
import scodec.bits.ByteVector;

public class BackupEncryptionTest {

@Test
public void encryptWithSeed_v1() throws GeneralSecurityException {
public void encryptWithSeed_v1() throws GeneralSecurityException, IOException {

// create a master key from a random seed
byte[] seed = new byte[16];
Expand All @@ -52,27 +55,46 @@ public void encryptWithSeed_v1() throws GeneralSecurityException {
Assert.assertTrue(AesCbcWithIntegrity.constantTimeEq(plaintext, decrypted));
}

@Test
public void encryptWithSeed_v2() throws GeneralSecurityException {

private AesCbcWithIntegrity.SecretKeys getSecretKeyV2() {
// create a master key from a random seed
byte[] seed = new byte[16];
new SecureRandom().nextBytes(seed);
final DeterministicWallet.ExtendedPrivateKey xpriv = DeterministicWallet.generate(ByteVector.view(seed));

// derive a hardened key from xpriv
// hardened means that, even if the key is compromised, it is not possible to find the parent key
final AesCbcWithIntegrity.SecretKeys key = EncryptedData.secretKeyFromBinaryKey(EncryptedBackup.generateBackupKey_v2(xpriv));
return EncryptedData.secretKeyFromBinaryKey(EncryptedBackup.generateBackupKey_v2(xpriv));
}

@Test
public void encryptWithSeed_v2() throws GeneralSecurityException, IOException {
final AesCbcWithIntegrity.SecretKeys key = getSecretKeyV2();

// data to encrypt
byte[] plaintext = new byte[300];
new SecureRandom().nextBytes(plaintext);

// apply encryption
EncryptedBackup encrypted = EncryptedBackup.encrypt(plaintext, key, EncryptedBackup.BACKUP_VERSION_2);
final EncryptedBackup encrypted = EncryptedBackup.encrypt(plaintext, key, EncryptedBackup.BACKUP_VERSION_2);
byte[] decrypted = encrypted.decrypt(key);

Assert.assertTrue(AesCbcWithIntegrity.constantTimeEq(plaintext, decrypted));

// let's also test that we can still read a version 3 encrypted backup (that is: data is first compressed, then encrypted)
final EncryptedBackup compressedAndEncrypted = EncryptedBackup.encrypt(decrypted, key, EncryptedBackup.BACKUP_VERSION_3);
byte[] decryptedAndUncompressed = compressedAndEncrypted.decrypt(key);

Assert.assertTrue(AesCbcWithIntegrity.constantTimeEq(plaintext, decryptedAndUncompressed));
}

@Test(expected = GeneralSecurityException.class)
public void wrongKeyFails_v2() throws GeneralSecurityException, IOException {
final AesCbcWithIntegrity.SecretKeys goodKey = getSecretKeyV2();
final AesCbcWithIntegrity.SecretKeys badKey = getSecretKeyV2();
byte[] plaintext = new byte[300];
new SecureRandom().nextBytes(plaintext);
final EncryptedBackup encrypted = EncryptedBackup.encrypt(plaintext, goodKey, EncryptedBackup.BACKUP_VERSION_2);
encrypted.decrypt(badKey);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,58 +20,54 @@
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Handler;

import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import androidx.core.app.ActivityCompat;

import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import com.google.android.gms.drive.*;
import com.google.android.gms.drive.query.*;
import com.google.android.gms.tasks.Task;
import fr.acinq.eclair.wallet.models.BackupTypes;
import fr.acinq.eclair.wallet.services.BackupUtils;
import fr.acinq.eclair.wallet.utils.Constants;
import fr.acinq.eclair.wallet.utils.WalletUtils;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.drive.Drive;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import scala.Option;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import fr.acinq.eclair.wallet.models.BackupTypes;
import fr.acinq.eclair.wallet.utils.BackupHelper;
import fr.acinq.eclair.wallet.utils.Constants;
import scala.Option;

public abstract class ChannelsBackupBaseActivity extends EclairActivity {

final static int ACCESS_REQUEST_PING_INTERVAL = 500;
final static int ACCESS_REQUEST_PING_INTERVAL = 1500;
static final int GDRIVE_REQUEST_CODE_SIGN_IN = 0;
private final Logger log = LoggerFactory.getLogger(ChannelsBackupBaseActivity.class);
protected Map<BackupTypes, Option<Boolean>> accessRequestsMap = new HashMap<>();
/**
* Handles high-level drive functions like sync
*/
private DriveClient mDriveClient;

/**
* Handle access to Drive resources/files.
* This maps monitors the access status for each backup sources requested by the user. Access is often
* granted asynchronously and may not be known.
* - If the value is None, the source's access status is pending.
* - If the value is Some(true), access is granted.
* - If the value is Some(false), access is denied.
*
* Value should never be null.
*/
private DriveResourceClient mDriveResourceClient;
protected Map<BackupTypes, Option<Boolean>> accessRequestsMap = new HashMap<>();

/**
* Retrieve a backup file from Drive and returns a Task with its metadata.
* Drive service to manage files using the REST v3 api.
*/
public static Task<MetadataBuffer> retrieveEclairBackupTask(final Task<DriveFolder> appFolderTask,
final DriveResourceClient driveResourceClient,
final String backupFileName) {
return appFolderTask.continueWithTask(appFolder -> {
// retrieve file(s) from drive
final SortOrder sortOrder = new SortOrder.Builder().addSortDescending(SortableField.MODIFIED_DATE).build();
final Query query = new Query.Builder().addFilter(Filters.eq(SearchableField.TITLE, backupFileName))
.setSortOrder(sortOrder).build();
return driveResourceClient.queryChildren(appFolder.getResult(), query);
});
}
protected Drive mDrive;

protected void requestAccess(final boolean checkLocal, final boolean checkGdrive) {
accessRequestsMap.clear();
Expand All @@ -84,20 +80,23 @@ protected void requestAccess(final boolean checkLocal, final boolean checkGdrive
accessRequestsMap.put(BackupTypes.GDRIVE, Option.apply(null));
requestGDriveAccess();
}
new Handler().postDelayed(this::checkAccessRequestIsDone, ACCESS_REQUEST_PING_INTERVAL);
checkAccessRequestIsDone();
} else {
applyAccessRequestDone();
}
}

private Handler accessCheckHandler = new Handler();

private void checkAccessRequestIsDone() {
accessCheckHandler.removeCallbacksAndMessages(null);
if (accessRequestsMap.isEmpty()) {
applyAccessRequestDone();
} else {
if (!accessRequestsMap.containsValue(Option.apply(null))) {
applyAccessRequestDone();
} else {
new Handler().postDelayed(this::checkAccessRequestIsDone, ACCESS_REQUEST_PING_INTERVAL);
accessCheckHandler.postDelayed(this::checkAccessRequestIsDone, ACCESS_REQUEST_PING_INTERVAL);
}
}
}
Expand All @@ -106,16 +105,16 @@ protected void applyAccessRequestDone() {
}

protected void requestLocalAccessOrApply() {
if (!BackupUtils.Local.hasLocalAccess(this)) {
if (!BackupHelper.Local.hasLocalAccess(this)) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, Constants.PERMISSION_EXTERNAL_STORAGE_REQUEST);
} else {
applyLocalAccessGranted();
}
}

protected void requestGDriveAccess() {
final GoogleSignInAccount signInAccount = BackupUtils.GoogleDrive.getSigninAccount(getApplicationContext());
final GoogleSignInClient googleSignInClient = GoogleSignIn.getClient(this, getGoogleSigninOptions());
final GoogleSignInAccount signInAccount = BackupHelper.GoogleDrive.getSigninAccount(getApplicationContext());
final GoogleSignInClient googleSignInClient = GoogleSignIn.getClient(this, BackupHelper.GoogleDrive.getGoogleSigninOptions());
if (signInAccount == null) {
startActivityForResult(googleSignInClient.getSignInIntent(), GDRIVE_REQUEST_CODE_SIGN_IN);
} else {
Expand All @@ -139,15 +138,6 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis
}
}

protected GoogleSignInOptions getGoogleSigninOptions() {
return (new GoogleSignInOptions.Builder()).requestId().requestEmail().requestScopes(Drive.SCOPE_FILE).requestScopes(Drive.SCOPE_APPFOLDER).build();
}

Task<MetadataBuffer> retrieveEclairBackupTask() {
final Task<DriveFolder> appFolderTask = getDriveResourceClient().getAppFolder();
return retrieveEclairBackupTask(appFolderTask, getDriveResourceClient(), WalletUtils.getEclairBackupFileName(app.seedHash.get()));
}

@UiThread
@CallSuper
protected void applyLocalAccessDenied() {
Expand All @@ -169,36 +159,7 @@ protected void applyGdriveAccessDenied() {
@UiThread
@CallSuper
protected void applyGdriveAccessGranted(final GoogleSignInAccount signInAccount) {
mDriveClient = Drive.getDriveClient(getApplicationContext(), signInAccount);
mDriveResourceClient = Drive.getDriveResourceClient(getApplicationContext(), signInAccount);
mDrive = BackupHelper.GoogleDrive.getDriveServiceFromAccount(getApplicationContext(), signInAccount);
accessRequestsMap.put(BackupTypes.GDRIVE, Option.apply(true));
}

@Override
protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == GDRIVE_REQUEST_CODE_SIGN_IN) {
if (resultCode != RESULT_OK) {
log.info("Google Drive sign-in failed with code {}");
applyGdriveAccessDenied();
return;
}
final Task<GoogleSignInAccount> getAccountTask = GoogleSignIn.getSignedInAccountFromIntent(data);
final GoogleSignInAccount signInAccount = getAccountTask.getResult();
if (getAccountTask.isSuccessful() && signInAccount != null) {
applyGdriveAccessGranted(signInAccount);
} else {
log.info("Google Drive sign-in failed, could not get account");
applyGdriveAccessDenied();
}
}
}

protected DriveResourceClient getDriveResourceClient() {
return mDriveResourceClient;
}

protected DriveClient getDriveClient() {
return mDriveClient;
}
}
Loading