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

Commit

Permalink
Migrate Google Drive API to the REST V3 API (#213)
Browse files Browse the repository at this point in the history
* Migrate google drive api to the REST V3 api

The Google Drive Android API that was used to upload/retrieve channels
backup has been deprecated and scheduled for termination at the end of
Dec, 2019. We switch to the REST V3 API which provides almost the same
feature set, albeit without offline syncing. The lack of offline syncing
is not an issue since we already save the channels backup on-device.

see: https://developers.google.com/drive/android/deprecation

* Hidden AppData folder

Since Google mentions that the hidden AppData folder is going to be
deprecated at some point, and since the user has already access to the
actual backup file, there is no reason to hide the backups and the
Google Drive backup files are now stored in a regular Drive folder that
the user can freely access.

Note that the app still checks for old backup that would be in this
hidden AppData folder. Subsequent backups will be uploaded to the public
folder.

* Backup compression

The backup files are now compressed before being encrypted to save
some bandwidth.

Fixes #208
  • Loading branch information
dpad85 authored Oct 2, 2019
1 parent b20edad commit c8d6190
Show file tree
Hide file tree
Showing 16 changed files with 746 additions and 406 deletions.
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

0 comments on commit c8d6190

Please sign in to comment.