Merge remote-tracking branch 'origin/master' into dev

This commit is contained in:
Tobias Kaminsky 2024-01-17 03:36:50 +01:00
commit 892d4f199c
71 changed files with 6370 additions and 1617 deletions

View file

@ -1,6 +1,9 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="ktlint" />
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.io.PrintStream,printf,java.nio.channels.FileChannel,position" />
</inspection_tool>
<inspection_tool class="KotlinUnusedImport" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantSemicolon" enabled="true" level="ERROR" enabled_by_default="true" />
</profile>

View file

@ -266,7 +266,7 @@ dependencies {
implementation 'org.greenrobot:eventbus:3.3.1'
implementation 'com.googlecode.ez-vcard:ez-vcard:0.12.0'
implementation 'org.lukhnos:nnio:0.2'
implementation 'org.bouncycastle:bcpkix-jdk15to18:1.72'
implementation 'org.bouncycastle:bcpkix-jdk18on:1.75'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.github.nextcloud-deps:sectioned-recyclerview:0.6.1'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'

View file

@ -48,6 +48,7 @@
<issue id="TrustAllX509TrustManager">
<ignore path="**/bouncycastle/est/jcajce/*.class" />
<ignore path="**/bcpkix-jdk15to18-1.72.jar" />
<ignore path="**/bcpkix-jdk18on-1.75.jar" />
</issue>
<issue id="RestrictedApi" severity="error">

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,32 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.client;
public enum EndToEndAction {
CREATE_FOLDER,
GO_INTO_FOLDER,
GO_UP,
UPLOAD_FILE,
DOWNLOAD_FILE,
DELETE_FILE,
}

View file

@ -1,730 +0,0 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2020 Tobias Kaminsky
* Copyright (C) 2020 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.client;
import android.accounts.AccountManager;
import com.nextcloud.test.RandomStringGenerator;
import com.nextcloud.test.RetryTestRule;
import com.owncloud.android.AbstractOnServerIT;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.db.OCUpload;
import com.owncloud.android.files.services.FileUploader;
import com.owncloud.android.lib.common.accounts.AccountUtils;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.lib.ocs.responses.PrivateKey;
import com.owncloud.android.lib.resources.e2ee.ToggleEncryptionRemoteOperation;
import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation;
import com.owncloud.android.lib.resources.status.OCCapability;
import com.owncloud.android.lib.resources.status.OwnCloudVersion;
import com.owncloud.android.lib.resources.users.DeletePrivateKeyOperation;
import com.owncloud.android.lib.resources.users.DeletePublicKeyOperation;
import com.owncloud.android.lib.resources.users.GetPrivateKeyOperation;
import com.owncloud.android.lib.resources.users.GetPublicKeyOperation;
import com.owncloud.android.lib.resources.users.SendCSROperation;
import com.owncloud.android.lib.resources.users.StorePrivateKeyOperation;
import com.owncloud.android.operations.DownloadFileOperation;
import com.owncloud.android.operations.GetCapabilitiesOperation;
import com.owncloud.android.operations.RemoveFileOperation;
import com.owncloud.android.utils.CsrHelper;
import com.owncloud.android.utils.EncryptionUtils;
import com.owncloud.android.utils.FileStorageUtils;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import static com.owncloud.android.lib.resources.status.OwnCloudVersion.nextcloud_19;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNotNull;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assume.assumeTrue;
@RunWith(AndroidJUnit4.class)
public class EndToEndRandomIT extends AbstractOnServerIT {
public enum Action {
CREATE_FOLDER,
GO_INTO_FOLDER,
GO_UP,
UPLOAD_FILE,
DOWNLOAD_FILE,
DELETE_FILE,
}
private static ArbitraryDataProvider arbitraryDataProvider;
private OCFile currentFolder;
private int actionCount = 20;
private String rootEncFolder = "/e/";
@Rule
public RetryTestRule retryTestRule = new RetryTestRule();
@BeforeClass
public static void initClass() throws Exception {
arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
createKeys();
}
@Before
public void before() throws IOException {
OCCapability capability = getStorageManager().getCapability(account.name);
if (capability.getVersion().equals(new OwnCloudVersion("0.0.0"))) {
// fetch new one
assertTrue(new GetCapabilitiesOperation(getStorageManager())
.execute(client)
.isSuccess());
}
// tests only for NC19+
assumeTrue(getStorageManager()
.getCapability(account.name)
.getVersion()
.isNewerOrEqual(nextcloud_19)
);
// make sure that every file is available, even after tests that remove source file
createDummyFiles();
}
@Test
public void run() throws Exception {
init();
for (int i = 0; i < actionCount; i++) {
Action nextAction = Action.values()[new Random().nextInt(Action.values().length)];
switch (nextAction) {
case CREATE_FOLDER:
createFolder(i);
break;
case GO_INTO_FOLDER:
goIntoFolder(i);
break;
case GO_UP:
goUp(i);
break;
case UPLOAD_FILE:
uploadFile(i);
break;
case DOWNLOAD_FILE:
downloadFile(i);
break;
case DELETE_FILE:
deleteFile(i);
break;
default:
Log_OC.d(this, "[" + i + "/" + actionCount + "]" + " Unknown action: " + nextAction);
break;
}
}
}
@Test
public void uploadOneFile() throws Exception {
init();
uploadFile(0);
}
@Test
public void createFolder() throws Exception {
init();
currentFolder = createFolder(0);
assertNotNull(currentFolder);
}
@Test
public void createSubFolders() throws Exception {
init();
currentFolder = createFolder(0);
assertNotNull(currentFolder);
currentFolder = createFolder(1);
assertNotNull(currentFolder);
currentFolder = createFolder(2);
assertNotNull(currentFolder);
}
@Test
public void createSubFoldersWithFiles() throws Exception {
init();
currentFolder = createFolder(0);
assertNotNull(currentFolder);
uploadFile(1);
uploadFile(1);
uploadFile(2);
currentFolder = createFolder(1);
assertNotNull(currentFolder);
uploadFile(11);
uploadFile(12);
uploadFile(13);
currentFolder = createFolder(2);
assertNotNull(currentFolder);
uploadFile(21);
uploadFile(22);
uploadFile(23);
}
@Test
public void pseudoRandom() throws Exception {
init();
uploadFile(1);
createFolder(2);
goIntoFolder(3);
goUp(4);
createFolder(5);
uploadFile(6);
goUp(7);
goIntoFolder(8);
goIntoFolder(9);
uploadFile(10);
}
@Test
public void deleteFile() throws Exception {
init();
uploadFile(1);
deleteFile(1);
}
@Test
public void deleteFolder() throws Exception {
init();
// create folder, go into it
OCFile createdFolder = createFolder(0);
assertNotNull(createdFolder);
currentFolder = createdFolder;
uploadFile(1);
goUp(1);
// delete folder
assertTrue(new RemoveFileOperation(createdFolder,
false,
user,
false,
targetContext,
getStorageManager())
.execute(client)
.isSuccess());
}
@Test
public void downloadFile() throws Exception {
init();
uploadFile(1);
downloadFile(1);
}
private void init() throws Exception {
// create folder
createFolder(rootEncFolder);
OCFile encFolder = createFolder(rootEncFolder + RandomStringGenerator.make(5) + "/");
// encrypt it
assertTrue(new ToggleEncryptionRemoteOperation(encFolder.getLocalId(),
encFolder.getRemotePath(),
true)
.execute(client).isSuccess());
encFolder.setEncrypted(true);
getStorageManager().saveFolder(encFolder, new ArrayList<>(), new ArrayList<>());
useExistingKeys();
rootEncFolder = encFolder.getDecryptedRemotePath();
currentFolder = encFolder;
}
private OCFile createFolder(int i) {
String path = currentFolder.getDecryptedRemotePath() + RandomStringGenerator.make(5) + "/";
Log_OC.d(this, "[" + i + "/" + actionCount + "] " + "Create folder: " + path);
return createFolder(path);
}
private void goIntoFolder(int i) {
ArrayList<OCFile> folders = new ArrayList<>();
for (OCFile file : getStorageManager().getFolderContent(currentFolder, false)) {
if (file.isFolder()) {
folders.add(file);
}
}
if (folders.isEmpty()) {
Log_OC.d(this, "[" + i + "/" + actionCount + "] " + "Go into folder: No folders");
return;
}
currentFolder = folders.get(new Random().nextInt(folders.size()));
Log_OC.d(this,
"[" + i + "/" + actionCount + "] " + "Go into folder: " + currentFolder.getDecryptedRemotePath());
}
private void goUp(int i) {
if (currentFolder.getRemotePath().equals(rootEncFolder)) {
Log_OC.d(this,
"[" + i + "/" + actionCount + "] " + "Go up to folder: " + currentFolder.getDecryptedRemotePath());
return;
}
currentFolder = getStorageManager().getFileById(currentFolder.getParentId());
if (currentFolder == null) {
throw new RuntimeException("Current folder is null");
}
Log_OC.d(this,
"[" + i + "/" + actionCount + "] " + "Go up to folder: " + currentFolder.getDecryptedRemotePath());
}
private void uploadFile(int i) throws IOException {
String fileName = RandomStringGenerator.make(5) + ".txt";
File file;
if (new Random().nextBoolean()) {
file = createFile(fileName, new Random().nextInt(50000));
} else {
file = createFile(fileName, 500000 + new Random().nextInt(50000));
}
String remotePath = currentFolder.getRemotePath() + fileName;
Log_OC.d(this,
"[" + i + "/" + actionCount + "] " +
"Upload file to: " + currentFolder.getDecryptedRemotePath() + fileName);
OCUpload ocUpload = new OCUpload(file.getAbsolutePath(),
remotePath,
account.name);
uploadOCUpload(ocUpload);
shortSleep();
OCFile parentFolder = getStorageManager()
.getFileByEncryptedRemotePath(new File(ocUpload.getRemotePath()).getParent() + "/");
String uploadedFileName = new File(ocUpload.getRemotePath()).getName();
String decryptedPath = parentFolder.getDecryptedRemotePath() + uploadedFileName;
OCFile uploadedFile = getStorageManager().getFileByDecryptedRemotePath(decryptedPath);
verifyStoragePath(uploadedFile);
// verify storage path
refreshFolder(currentFolder.getRemotePath());
uploadedFile = getStorageManager().getFileByDecryptedRemotePath(decryptedPath);
verifyStoragePath(uploadedFile);
// verify that encrypted file is on server
assertTrue(new ReadFileRemoteOperation(currentFolder.getRemotePath() + uploadedFile.getEncryptedFileName())
.execute(client)
.isSuccess());
// verify that unencrypted file is not on server
assertFalse(new ReadFileRemoteOperation(currentFolder.getDecryptedRemotePath() + fileName)
.execute(client)
.isSuccess());
}
private void downloadFile(int i) {
ArrayList<OCFile> files = new ArrayList<>();
for (OCFile file : getStorageManager().getFolderContent(currentFolder, false)) {
if (!file.isFolder()) {
files.add(file);
}
}
if (files.isEmpty()) {
Log_OC.d(this, "[" + i + "/" + actionCount + "] No files in: " + currentFolder.getDecryptedRemotePath());
return;
}
OCFile fileToDownload = files.get(new Random().nextInt(files.size()));
assertNotNull(fileToDownload.getRemoteId());
Log_OC.d(this,
"[" + i + "/" + actionCount + "] " + "Download file: " +
currentFolder.getDecryptedRemotePath() + fileToDownload.getDecryptedFileName());
assertTrue(new DownloadFileOperation(user, fileToDownload, targetContext)
.execute(client)
.isSuccess());
assertTrue(new File(fileToDownload.getStoragePath()).exists());
verifyStoragePath(fileToDownload);
}
@Test
public void testUploadWithCopy() throws Exception {
init();
OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
currentFolder.getRemotePath() + "nonEmpty.txt",
account.name);
uploadOCUpload(ocUpload, FileUploader.LOCAL_BEHAVIOUR_COPY);
File originalFile = new File(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt");
OCFile uploadedFile = fileDataStorageManager.getFileByDecryptedRemotePath(currentFolder.getRemotePath() +
"nonEmpty.txt");
assertTrue(originalFile.exists());
assertTrue(new File(uploadedFile.getStoragePath()).exists());
}
@Test
public void testUploadWithMove() throws Exception {
init();
OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
currentFolder.getRemotePath() + "nonEmpty.txt",
account.name);
uploadOCUpload(ocUpload, FileUploader.LOCAL_BEHAVIOUR_MOVE);
File originalFile = new File(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt");
OCFile uploadedFile = fileDataStorageManager.getFileByDecryptedRemotePath(currentFolder.getRemotePath() +
"nonEmpty.txt");
assertFalse(originalFile.exists());
assertTrue(new File(uploadedFile.getStoragePath()).exists());
}
@Test
public void testUploadWithForget() throws Exception {
init();
OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
currentFolder.getRemotePath() + "nonEmpty.txt",
account.name);
uploadOCUpload(ocUpload, FileUploader.LOCAL_BEHAVIOUR_FORGET);
File originalFile = new File(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt");
OCFile uploadedFile = fileDataStorageManager.getFileByDecryptedRemotePath(currentFolder.getRemotePath() +
"nonEmpty.txt");
assertTrue(originalFile.exists());
assertFalse(new File(uploadedFile.getStoragePath()).exists());
}
@Test
public void testUploadWithDelete() throws Exception {
init();
OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
currentFolder.getRemotePath() + "nonEmpty.txt",
account.name);
uploadOCUpload(ocUpload, FileUploader.LOCAL_BEHAVIOUR_DELETE);
File originalFile = new File(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt");
OCFile uploadedFile = fileDataStorageManager.getFileByDecryptedRemotePath(currentFolder.getRemotePath() +
"nonEmpty.txt");
assertFalse(originalFile.exists());
assertFalse(new File(uploadedFile.getStoragePath()).exists());
}
@Test
public void testCheckCSR() throws Exception {
deleteKeys();
// Create public/private key pair
KeyPair keyPair = EncryptionUtils.generateKeyPair();
// create CSR
AccountManager accountManager = AccountManager.get(targetContext);
String userId = accountManager.getUserData(account, AccountUtils.Constants.KEY_USER_ID);
String urlEncoded = CsrHelper.generateCsrPemEncodedString(keyPair, userId);
SendCSROperation operation = new SendCSROperation(urlEncoded);
RemoteOperationResult result = operation.execute(account, targetContext);
assertTrue(result.isSuccess());
String publicKeyString = (String) result.getData().get(0);
// check key
RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyPair.getPrivate();
RSAPublicKey publicKey = EncryptionUtils.convertPublicKeyFromString(publicKeyString);
BigInteger modulusPublic = publicKey.getModulus();
BigInteger modulusPrivate = privateKey.getModulus();
assertEquals(modulusPrivate, modulusPublic);
createKeys();
}
private void deleteFile(int i) {
ArrayList<OCFile> files = new ArrayList<>();
for (OCFile file : getStorageManager().getFolderContent(currentFolder, false)) {
if (!file.isFolder()) {
files.add(file);
}
}
if (files.isEmpty()) {
Log_OC.d(this, "[" + i + "/" + actionCount + "] No files in: " + currentFolder.getDecryptedRemotePath());
return;
}
OCFile fileToDelete = files.get(new Random().nextInt(files.size()));
assertNotNull(fileToDelete.getRemoteId());
Log_OC.d(this,
"[" + i + "/" + actionCount + "] " +
"Delete file: " + currentFolder.getDecryptedRemotePath() + fileToDelete.getDecryptedFileName());
assertTrue(new RemoveFileOperation(fileToDelete,
false,
user,
false,
targetContext,
getStorageManager())
.execute(client)
.isSuccess());
}
@Test
public void reInit() throws Exception {
// create folder
OCFile encFolder = createFolder(rootEncFolder);
// encrypt it
assertTrue(new ToggleEncryptionRemoteOperation(encFolder.getLocalId(),
encFolder.getRemotePath(),
true)
.execute(client).isSuccess());
encFolder.setEncrypted(true);
getStorageManager().saveFolder(encFolder, new ArrayList<>(), new ArrayList<>());
// delete keys
arbitraryDataProvider.deleteKeyForAccount(account.name, EncryptionUtils.PRIVATE_KEY);
arbitraryDataProvider.deleteKeyForAccount(account.name, EncryptionUtils.PUBLIC_KEY);
arbitraryDataProvider.deleteKeyForAccount(account.name, EncryptionUtils.MNEMONIC);
useExistingKeys();
}
private void useExistingKeys() throws Exception {
// download them from server
GetPublicKeyOperation publicKeyOperation = new GetPublicKeyOperation();
RemoteOperationResult<String> publicKeyResult = publicKeyOperation.execute(account, targetContext);
assertTrue("Result code:" + publicKeyResult.getHttpCode(), publicKeyResult.isSuccess());
String publicKeyFromServer = publicKeyResult.getResultData();
arbitraryDataProvider.storeOrUpdateKeyValue(account.name,
EncryptionUtils.PUBLIC_KEY,
publicKeyFromServer);
RemoteOperationResult<PrivateKey> privateKeyResult = new GetPrivateKeyOperation().execute(account,
targetContext);
assertTrue(privateKeyResult.isSuccess());
PrivateKey privateKey = privateKeyResult.getResultData();
String mnemonic = generateMnemonicString();
String decryptedPrivateKey = EncryptionUtils.decryptPrivateKey(privateKey.getKey(), mnemonic);
arbitraryDataProvider.storeOrUpdateKeyValue(account.name,
EncryptionUtils.PRIVATE_KEY, decryptedPrivateKey);
Log_OC.d(this, "Private key successfully decrypted and stored");
arbitraryDataProvider.storeOrUpdateKeyValue(account.name, EncryptionUtils.MNEMONIC, mnemonic);
}
/*
TODO do not c&p code
*/
private static void createKeys() throws Exception {
deleteKeys();
String publicKeyString;
// Create public/private key pair
KeyPair keyPair = EncryptionUtils.generateKeyPair();
// create CSR
AccountManager accountManager = AccountManager.get(targetContext);
String userId = accountManager.getUserData(account, AccountUtils.Constants.KEY_USER_ID);
String urlEncoded = CsrHelper.generateCsrPemEncodedString(keyPair, userId);
SendCSROperation operation = new SendCSROperation(urlEncoded);
RemoteOperationResult result = operation.execute(account, targetContext);
if (result.isSuccess()) {
publicKeyString = (String) result.getData().get(0);
// check key
RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyPair.getPrivate();
RSAPublicKey publicKey = EncryptionUtils.convertPublicKeyFromString(publicKeyString);
BigInteger modulusPublic = publicKey.getModulus();
BigInteger modulusPrivate = privateKey.getModulus();
if (modulusPrivate.compareTo(modulusPublic) != 0) {
throw new RuntimeException("Wrong CSR returned");
}
} else {
throw new Exception("failed to send CSR", result.getException());
}
java.security.PrivateKey privateKey = keyPair.getPrivate();
String privateKeyString = EncryptionUtils.encodeBytesToBase64String(privateKey.getEncoded());
String privatePemKeyString = EncryptionUtils.privateKeyToPEM(privateKey);
String encryptedPrivateKey = EncryptionUtils.encryptPrivateKey(privatePemKeyString,
generateMnemonicString());
// upload encryptedPrivateKey
StorePrivateKeyOperation storePrivateKeyOperation = new StorePrivateKeyOperation(encryptedPrivateKey);
RemoteOperationResult storePrivateKeyResult = storePrivateKeyOperation.execute(account, targetContext);
if (storePrivateKeyResult.isSuccess()) {
arbitraryDataProvider.storeOrUpdateKeyValue(account.name, EncryptionUtils.PRIVATE_KEY,
privateKeyString);
arbitraryDataProvider.storeOrUpdateKeyValue(account.name, EncryptionUtils.PUBLIC_KEY, publicKeyString);
arbitraryDataProvider.storeOrUpdateKeyValue(account.name, EncryptionUtils.MNEMONIC,
generateMnemonicString());
} else {
throw new RuntimeException("Error uploading private key!");
}
}
private static void deleteKeys() {
RemoteOperationResult<PrivateKey> privateKeyRemoteOperationResult = new GetPrivateKeyOperation().execute(client);
RemoteOperationResult<String> publicKeyRemoteOperationResult = new GetPublicKeyOperation().execute(client);
if (privateKeyRemoteOperationResult.isSuccess() || publicKeyRemoteOperationResult.isSuccess()) {
// delete keys
assertTrue(new DeletePrivateKeyOperation().execute(client).isSuccess());
assertTrue(new DeletePublicKeyOperation().execute(client).isSuccess());
arbitraryDataProvider.deleteKeyForAccount(account.name, EncryptionUtils.PRIVATE_KEY);
arbitraryDataProvider.deleteKeyForAccount(account.name, EncryptionUtils.PUBLIC_KEY);
arbitraryDataProvider.deleteKeyForAccount(account.name, EncryptionUtils.MNEMONIC);
}
}
private static String generateMnemonicString() {
return "1 2 3 4 5 6";
}
public void after() {
// remove all encrypted files
OCFile root = fileDataStorageManager.getFileByDecryptedRemotePath("/");
removeFolder(root);
// List<OCFile> files = fileDataStorageManager.getFolderContent(root, false);
//
// for (OCFile child : files) {
// removeFolder(child);
// }
assertEquals(0, fileDataStorageManager.getFolderContent(root, false).size());
super.after();
}
private void removeFolder(OCFile folder) {
Log_OC.d(this, "Start removing content of folder: " + folder.getDecryptedRemotePath());
List<OCFile> children = fileDataStorageManager.getFolderContent(folder, false);
// remove children
for (OCFile child : children) {
if (child.isFolder()) {
removeFolder(child);
// remove folder
Log_OC.d(this, "Remove folder: " + child.getDecryptedRemotePath());
if (!folder.isEncrypted() && child.isEncrypted()) {
assertTrue(new ToggleEncryptionRemoteOperation(child.getLocalId(),
child.getRemotePath(),
false)
.execute(client)
.isSuccess());
OCFile f = getStorageManager().getFileByEncryptedRemotePath(child.getRemotePath());
f.setEncrypted(false);
getStorageManager().saveFile(f);
child.setEncrypted(false);
}
} else {
Log_OC.d(this, "Remove file: " + child.getDecryptedRemotePath());
}
assertTrue(new RemoveFileOperation(child, false, user, false, targetContext, getStorageManager())
.execute(client)
.isSuccess()
);
}
Log_OC.d(this, "Finished removing content of folder: " + folder.getDecryptedRemotePath());
}
private void verifyStoragePath(OCFile file) {
assertEquals(FileStorageUtils.getSavePath(account.name) +
currentFolder.getDecryptedRemotePath() +
file.getDecryptedFileName(),
file.getStoragePath());
}
}

View file

@ -28,6 +28,9 @@ import com.nextcloud.client.preferences.DarkMode;
import com.nextcloud.common.NextcloudClient;
import com.nextcloud.java.util.Optional;
import com.nextcloud.test.GrantStoragePermissionRule;
import com.nextcloud.test.RandomStringGenerator;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.UploadsStorageManager;
@ -38,6 +41,7 @@ import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.OwnCloudClientFactory;
import com.owncloud.android.lib.common.accounts.AccountUtils;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation;
import com.owncloud.android.lib.resources.status.CapabilityBooleanType;
import com.owncloud.android.lib.resources.status.GetCapabilitiesRemoteOperation;
import com.owncloud.android.lib.resources.status.OCCapability;
@ -46,8 +50,6 @@ import com.owncloud.android.operations.CreateFolderOperation;
import com.owncloud.android.operations.UploadFileOperation;
import com.owncloud.android.utils.FileStorageUtils;
import junit.framework.TestCase;
import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.Before;
@ -100,6 +102,8 @@ public abstract class AbstractIT {
protected FileDataStorageManager fileDataStorageManager =
new FileDataStorageManager(user, targetContext.getContentResolver());
protected ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
@BeforeClass
public static void beforeAll() {
try {
@ -118,14 +122,11 @@ public abstract class AbstractIT {
client = OwnCloudClientFactory.createOwnCloudClient(account, targetContext);
nextcloudClient = OwnCloudClientFactory.createNextcloudClient(user, targetContext);
} catch (OperationCanceledException e) {
e.printStackTrace();
} catch (AuthenticatorException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (AccountUtils.AccountNotFoundException e) {
e.printStackTrace();
} catch (OperationCanceledException |
IOException |
AccountUtils.AccountNotFoundException |
AuthenticatorException e) {
throw new RuntimeException("Error setting up clients", e);
}
Bundle arguments = androidx.test.platform.app.InstrumentationRegistry.getArguments();
@ -334,11 +335,15 @@ public abstract class AbstractIT {
}
public OCFile createFolder(String remotePath) {
TestCase.assertTrue(new CreateFolderOperation(remotePath, user, targetContext, getStorageManager())
RemoteOperationResult check = new ExistenceCheckRemoteOperation(remotePath, false).execute(client);
if (!check.isSuccess()) {
assertTrue(new CreateFolderOperation(remotePath, user, targetContext, getStorageManager())
.execute(client)
.isSuccess());
}
return getStorageManager().getFileByDecryptedRemotePath(remotePath);
return getStorageManager().getFileByDecryptedRemotePath(remotePath.endsWith("/") ? remotePath : remotePath + "/");
}
public void uploadFile(File file, String remotePath) {
@ -473,6 +478,14 @@ public abstract class AbstractIT {
return AccountManager.get(targetContext).getUserData(user.toPlatformAccount(), KEY_USER_ID);
}
public String getRandomName() {
return getRandomName(5);
}
public String getRandomName(int length) {
return RandomStringGenerator.make(length);
}
protected static User getUser(Account account) {
Optional<User> optionalUser = UserAccountManagerImpl.fromContext(targetContext).getUser(account.name);
return optionalUser.orElseThrow(IllegalAccessError::new);

View file

@ -94,21 +94,19 @@ public abstract class AbstractOnServerIT extends AbstractIT {
user = optionalUser.orElseThrow(IllegalAccessError::new);
client = OwnCloudClientFactory.createOwnCloudClient(account, targetContext);
nextcloudClient = OwnCloudClientFactory.createNextcloudClient(user, targetContext);
createDummyFiles();
waitForServer(client, baseUrl);
deleteAllFilesOnServer(); // makes sure that no file/folder is in root
// deleteAllFilesOnServer(); // makes sure that no file/folder is in root
} catch (OperationCanceledException e) {
e.printStackTrace();
} catch (AuthenticatorException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (AccountUtils.AccountNotFoundException e) {
e.printStackTrace();
} catch (OperationCanceledException |
IOException |
AccountUtils.AccountNotFoundException |
AuthenticatorException e) {
throw new RuntimeException("Error setting up clients", e);
}
}

View file

@ -26,12 +26,11 @@ import org.junit.Assert.assertEquals
import org.junit.Test
class ArbitraryDataProviderIT : AbstractIT() {
private val arbitraryDataProvider = ArbitraryDataProviderImpl(targetContext)
@Test
fun testNull() {
fun testEmpty() {
val key = "DUMMY_KEY"
arbitraryDataProvider.storeOrUpdateKeyValue(user.accountName, key, null)
arbitraryDataProvider.storeOrUpdateKeyValue(user.accountName, key, "")
assertEquals("", arbitraryDataProvider.getValue(user.accountName, key))
}

View file

@ -84,6 +84,7 @@ class FileDetailSharingFragmentIT : AbstractIT() {
remoteId = "00000001"
parentId = activity.storageManager.getFileByEncryptedRemotePath("/").fileId
permissions = OCFile.PERMISSION_CAN_RESHARE
fileDataStorageManager.saveFile(this)
}
folder = OCFile("/test").apply {

View file

@ -31,16 +31,20 @@ import com.nextcloud.test.RetryTestRule;
import com.owncloud.android.AbstractIT;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
import com.owncloud.android.datamodel.DecryptedFolderMetadata;
import com.owncloud.android.datamodel.EncryptedFolderMetadata;
import com.owncloud.android.datamodel.e2e.v1.decrypted.Data;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedMetadata;
import com.owncloud.android.datamodel.e2e.v1.decrypted.Encrypted;
import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFile;
import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFolderMetadataFileV1;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.utils.CsrHelper;
import com.owncloud.android.lib.resources.e2ee.CsrHelper;
import com.owncloud.android.utils.EncryptionUtils;
import org.apache.commons.codec.binary.Hex;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.FileInputStream;
@ -63,9 +67,6 @@ import java.util.Set;
import javax.crypto.BadPaddingException;
import androidx.test.runner.AndroidJUnit4;
import static com.owncloud.android.utils.EncryptionUtils.EncryptedFile;
import static com.owncloud.android.utils.EncryptionUtils.decodeStringToBase64Bytes;
import static com.owncloud.android.utils.EncryptionUtils.decryptFile;
import static com.owncloud.android.utils.EncryptionUtils.decryptFolderMetaData;
@ -93,11 +94,12 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
@RunWith(AndroidJUnit4.class)
public class EncryptionTestIT extends AbstractIT {
@Rule public RetryTestRule retryTestRule = new RetryTestRule();
private String privateKey = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAo" +
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
public static final String privateKey = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAo" +
"IBAQDsn0JKS/THu328z1IgN0VzYU53HjSX03WJIgWkmyTaxbiKpoJaKbksXmfSpgzV" +
"GzKFvGfZ03fwFrN7Q8P8R2e8SNiell7mh1TDw9/0P7Bt/ER8PJrXORo+GviKHxaLr7" +
"Y0BJX9i/nW/L0L/VaE8CZTAqYBdcSJGgHJjY4UMf892ZPTa9T2Dl3ggdMZ7BQ2kiCi" +
@ -123,7 +125,7 @@ public class EncryptionTestIT extends AbstractIT {
"JLecOFu3ZlQl/RStQb69QKb5MNOIMmQhg8WOxZxHcpmIDbkDAm/J/ovJXFSoBdOr5o" +
"uQsYsDZhsWW97zvLMzg5pH9/3/1BNz5q3Vu4HgfBSwWGt4E2NENj+XA+QAVmGA==";
private String cert = "-----BEGIN CERTIFICATE-----\n" +
public static final String publicKey = "-----BEGIN CERTIFICATE-----\n" +
"MIIDpzCCAo+gAwIBAgIBADANBgkqhkiG9w0BAQUFADBuMRowGAYDVQQDDBF3d3cu\n" +
"bmV4dGNsb3VkLmNvbTESMBAGA1UECgwJTmV4dGNsb3VkMRIwEAYDVQQHDAlTdHV0\n" +
"dGdhcnQxGzAZBgNVBAgMEkJhZGVuLVd1ZXJ0dGVtYmVyZzELMAkGA1UEBhMCREUw\n" +
@ -151,7 +153,7 @@ public class EncryptionTestIT extends AbstractIT {
byte[] key1 = generateKey();
String base64encodedKey = encodeBytesToBase64String(key1);
String encryptedString = EncryptionUtils.encryptStringAsymmetric(base64encodedKey, cert);
String encryptedString = EncryptionUtils.encryptStringAsymmetric(base64encodedKey, publicKey);
String decryptedString = decryptStringAsymmetric(encryptedString, privateKey);
byte[] key2 = decodeStringToBase64Bytes(decryptedString);
@ -207,9 +209,9 @@ public class EncryptionTestIT extends AbstractIT {
String encryptedString;
if (new Random().nextBoolean()) {
encryptedString = EncryptionUtils.encryptStringSymmetric(privateKey, key);
encryptedString = EncryptionUtils.encryptStringSymmetricAsString(privateKey, key);
} else {
encryptedString = EncryptionUtils.encryptStringSymmetricOld(privateKey, key);
encryptedString = EncryptionUtils.encryptStringSymmetricAsStringOld(privateKey, key);
if (encryptedString.indexOf(ivDelimiterOld) != encryptedString.lastIndexOf(ivDelimiterOld)) {
Log_OC.d("EncryptionTestIT", "skip due to duplicated iv (old system) -> ignoring");
@ -230,7 +232,7 @@ public class EncryptionTestIT extends AbstractIT {
for (int i = 0; i < max; i++) {
Log_OC.d("EncryptionTestIT", i + " of " + max);
String encryptedString = EncryptionUtils.encryptStringSymmetric(privateKey, key);
String encryptedString = EncryptionUtils.encryptStringSymmetricAsString(privateKey, key);
int delimiterPosition = encryptedString.indexOf(ivDelimiter);
if (delimiterPosition == -1) {
@ -286,43 +288,42 @@ public class EncryptionTestIT extends AbstractIT {
keyGen.initialize(2048, new SecureRandom());
KeyPair keyPair = keyGen.generateKeyPair();
assertFalse(CsrHelper.generateCsrPemEncodedString(keyPair, "").isEmpty());
assertFalse(new CsrHelper().generateCsrPemEncodedString(keyPair, "").isEmpty());
assertFalse(encodeBytesToBase64String(keyPair.getPublic().getEncoded()).isEmpty());
}
/**
* DecryptedFolderMetadata -> EncryptedFolderMetadata -> JSON -> encrypt -> decrypt -> JSON ->
* EncryptedFolderMetadata -> DecryptedFolderMetadata
* DecryptedFolderMetadataFile -> EncryptedFolderMetadataFile -> JSON -> encrypt -> decrypt -> JSON ->
* EncryptedFolderMetadataFile -> DecryptedFolderMetadataFile
*/
@Test
public void encryptionMetadata() throws Exception {
DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata();
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
long folderID = 1;
public void encryptionMetadataV1() throws Exception {
DecryptedFolderMetadataFileV1 decryptedFolderMetadata1 = generateFolderMetadataV1_1();
// encrypt
EncryptedFolderMetadata encryptedFolderMetadata1 = encryptFolderMetadata(
EncryptedFolderMetadataFileV1 encryptedFolderMetadata1 = encryptFolderMetadata(
decryptedFolderMetadata1,
cert,
arbitraryDataProvider,
publicKey,
1,
user,
folderID);
arbitraryDataProvider);
// serialize
String encryptedJson = serializeJSON(encryptedFolderMetadata1);
// de-serialize
EncryptedFolderMetadata encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
new TypeToken<EncryptedFolderMetadata>() {
EncryptedFolderMetadataFileV1 encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
new TypeToken<>() {
});
// decrypt
DecryptedFolderMetadata decryptedFolderMetadata2 = decryptFolderMetaData(
DecryptedFolderMetadataFileV1 decryptedFolderMetadata2 = decryptFolderMetaData(
encryptedFolderMetadata2,
privateKey,
arbitraryDataProvider,
user,
folderID);
1);
// compare
assertTrue(compareJsonStrings(serializeJSON(decryptedFolderMetadata1),
@ -331,29 +332,28 @@ public class EncryptionTestIT extends AbstractIT {
@Test
public void testChangedMetadataKey() throws Exception {
DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata();
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
DecryptedFolderMetadataFileV1 decryptedFolderMetadata1 = generateFolderMetadataV1_1();
long folderID = 1;
// encrypt
EncryptedFolderMetadata encryptedFolderMetadata1 = encryptFolderMetadata(
EncryptedFolderMetadataFileV1 encryptedFolderMetadata1 = encryptFolderMetadata(
decryptedFolderMetadata1,
cert,
arbitraryDataProvider,
publicKey,
folderID,
user,
folderID);
arbitraryDataProvider);
// store metadata key
String oldMetadataKey = encryptedFolderMetadata1.getMetadata().getMetadataKey();
// do it again
// encrypt
EncryptedFolderMetadata encryptedFolderMetadata2 = encryptFolderMetadata(
EncryptedFolderMetadataFileV1 encryptedFolderMetadata2 = encryptFolderMetadata(
decryptedFolderMetadata1,
cert,
arbitraryDataProvider,
publicKey,
folderID,
user,
folderID);
arbitraryDataProvider);
String newMetadataKey = encryptedFolderMetadata2.getMetadata().getMetadataKey();
@ -362,17 +362,16 @@ public class EncryptionTestIT extends AbstractIT {
@Test
public void testMigrateMetadataKey() throws Exception {
DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata();
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
DecryptedFolderMetadataFileV1 decryptedFolderMetadata1 = generateFolderMetadataV1_1();
long folderID = 1;
// encrypt
EncryptedFolderMetadata encryptedFolderMetadata1 = encryptFolderMetadata(
EncryptedFolderMetadataFileV1 encryptedFolderMetadata1 = encryptFolderMetadata(
decryptedFolderMetadata1,
cert,
arbitraryDataProvider,
publicKey,
folderID,
user,
folderID);
arbitraryDataProvider);
// reset new metadata key, to mimic old version
encryptedFolderMetadata1.getMetadata().setMetadataKey(null);
@ -380,12 +379,12 @@ public class EncryptionTestIT extends AbstractIT {
// do it again
// encrypt
EncryptedFolderMetadata encryptedFolderMetadata2 = encryptFolderMetadata(
EncryptedFolderMetadataFileV1 encryptedFolderMetadata2 = encryptFolderMetadata(
decryptedFolderMetadata1,
cert,
arbitraryDataProvider,
publicKey,
folderID,
user,
folderID);
arbitraryDataProvider);
String newMetadataKey = encryptedFolderMetadata2.getMetadata().getMetadataKey();
@ -403,7 +402,7 @@ public class EncryptionTestIT extends AbstractIT {
@Test
public void cryptFileWithMetadata() throws Exception {
DecryptedFolderMetadata metadata = generateFolderMetadata();
DecryptedFolderMetadataFileV1 metadata = generateFolderMetadataV1_1();
// n9WXAIXO2wRY4R8nXwmo
assertTrue(cryptFile("ia7OEEEyXMoRa1QWQk8r",
@ -428,28 +427,27 @@ public class EncryptionTestIT extends AbstractIT {
@Test
public void bigMetadata() throws Exception {
DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata();
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
DecryptedFolderMetadataFileV1 decryptedFolderMetadata1 = generateFolderMetadataV1_1();
long folderID = 1;
// encrypt
EncryptedFolderMetadata encryptedFolderMetadata1 = encryptFolderMetadata(
EncryptedFolderMetadataFileV1 encryptedFolderMetadata1 = encryptFolderMetadata(
decryptedFolderMetadata1,
cert,
arbitraryDataProvider,
publicKey,
folderID,
user,
folderID);
arbitraryDataProvider);
// serialize
String encryptedJson = serializeJSON(encryptedFolderMetadata1);
// de-serialize
EncryptedFolderMetadata encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
new TypeToken<EncryptedFolderMetadata>() {
EncryptedFolderMetadataFileV1 encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
new TypeToken<>() {
});
// decrypt
DecryptedFolderMetadata decryptedFolderMetadata2 = decryptFolderMetaData(
DecryptedFolderMetadataFileV1 decryptedFolderMetadata2 = decryptFolderMetaData(
encryptedFolderMetadata2,
privateKey,
arbitraryDataProvider,
@ -473,17 +471,17 @@ public class EncryptionTestIT extends AbstractIT {
// encrypt
encryptedFolderMetadata1 = encryptFolderMetadata(decryptedFolderMetadata1,
cert,
arbitraryDataProvider,
publicKey,
folderID,
user,
folderID);
arbitraryDataProvider);
// serialize
encryptedJson = serializeJSON(encryptedFolderMetadata1);
// de-serialize
encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
new TypeToken<EncryptedFolderMetadata>() {
new TypeToken<>() {
});
// decrypt
@ -502,21 +500,97 @@ public class EncryptionTestIT extends AbstractIT {
}
}
@Test
public void bigMetadata2() throws Exception {
long folderID = 1;
DecryptedFolderMetadataFileV1 decryptedFolderMetadata1 = generateFolderMetadataV1_1();
// encrypt
EncryptedFolderMetadataFileV1 encryptedFolderMetadata1 = encryptFolderMetadata(
decryptedFolderMetadata1,
publicKey,
folderID,
user,
arbitraryDataProvider);
// serialize
String encryptedJson = serializeJSON(encryptedFolderMetadata1);
// de-serialize
EncryptedFolderMetadataFileV1 encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
new TypeToken<EncryptedFolderMetadataFileV1>() {
});
// decrypt
DecryptedFolderMetadataFileV1 decryptedFolderMetadata2 = decryptFolderMetaData(
encryptedFolderMetadata2,
privateKey,
arbitraryDataProvider,
user,
folderID);
// compare
assertTrue(compareJsonStrings(serializeJSON(decryptedFolderMetadata1),
serializeJSON(decryptedFolderMetadata2)));
// prefill with 500
for (int i = 0; i < 500; i++) {
addFile(decryptedFolderMetadata1, i);
}
int max = 505;
for (int i = 500; i < max; i++) {
Log_OC.d(this, "Big metadata: " + i + " of " + max);
addFile(decryptedFolderMetadata1, i);
// encrypt
encryptedFolderMetadata1 = encryptFolderMetadata(
decryptedFolderMetadata1,
publicKey,
folderID,
user,
arbitraryDataProvider);
// serialize
encryptedJson = serializeJSON(encryptedFolderMetadata1);
// de-serialize
encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
new TypeToken<>() {
});
// decrypt
decryptedFolderMetadata2 = decryptFolderMetaData(
encryptedFolderMetadata2,
privateKey,
arbitraryDataProvider,
user,
folderID);
// compare
assertTrue(compareJsonStrings(serializeJSON(decryptedFolderMetadata1),
serializeJSON(decryptedFolderMetadata2)));
assertEquals(i + 3, decryptedFolderMetadata1.getFiles().size());
assertEquals(i + 3, decryptedFolderMetadata2.getFiles().size());
}
}
@Test
public void filedrop() throws Exception {
DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata();
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
DecryptedFolderMetadataFileV1 decryptedFolderMetadata1 = generateFolderMetadataV1_1();
long folderID = 1;
// add filedrop
Map<String, DecryptedFolderMetadata.DecryptedFile> filesdrop = new HashMap<>();
Map<String, DecryptedFile> filesdrop = new HashMap<>();
DecryptedFolderMetadata.Data data = new DecryptedFolderMetadata.Data();
Data data = new Data();
data.setKey("9dfzbIYDt28zTyZfbcll+g==");
data.setFilename("test2.txt");
data.setVersion(1);
DecryptedFolderMetadata.DecryptedFile file = new DecryptedFolderMetadata.DecryptedFile();
DecryptedFile file = new DecryptedFile();
file.setInitializationVector("hnJLF8uhDvDoFK4ajuvwrg==");
file.setEncrypted(data);
file.setMetadataKey(0);
@ -527,24 +601,24 @@ public class EncryptionTestIT extends AbstractIT {
decryptedFolderMetadata1.setFiledrop(filesdrop);
// encrypt
EncryptedFolderMetadata encryptedFolderMetadata1 = encryptFolderMetadata(
EncryptedFolderMetadataFileV1 encryptedFolderMetadata1 = encryptFolderMetadata(
decryptedFolderMetadata1,
cert,
arbitraryDataProvider,
publicKey,
folderID,
user,
folderID);
EncryptionUtils.encryptFileDropFiles(decryptedFolderMetadata1, encryptedFolderMetadata1, cert);
arbitraryDataProvider);
EncryptionUtils.encryptFileDropFiles(decryptedFolderMetadata1, encryptedFolderMetadata1, publicKey);
// serialize
String encryptedJson = serializeJSON(encryptedFolderMetadata1);
// de-serialize
EncryptedFolderMetadata encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
new TypeToken<EncryptedFolderMetadata>() {
EncryptedFolderMetadataFileV1 encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
new TypeToken<>() {
});
// decrypt
DecryptedFolderMetadata decryptedFolderMetadata2 = decryptFolderMetaData(
DecryptedFolderMetadataFileV1 decryptedFolderMetadata2 = decryptFolderMetaData(
encryptedFolderMetadata2,
privateKey,
arbitraryDataProvider,
@ -562,19 +636,19 @@ public class EncryptionTestIT extends AbstractIT {
assertNull(decryptedFolderMetadata2.getFiledrop());
}
private void addFile(DecryptedFolderMetadata decryptedFolderMetadata, int counter) {
private void addFile(DecryptedFolderMetadataFileV1 decryptedFolderMetadata, int counter) {
// Add new file
// Always generate new
byte[] key = generateKey();
byte[] iv = randomBytes(ivLength);
byte[] authTag = randomBytes((128 / 8));
DecryptedFolderMetadata.Data data = new DecryptedFolderMetadata.Data();
Data data = new Data();
data.setKey(EncryptionUtils.encodeBytesToBase64String(key));
data.setFilename(counter + ".txt");
data.setVersion(1);
DecryptedFolderMetadata.DecryptedFile file = new DecryptedFolderMetadata.DecryptedFile();
DecryptedFile file = new DecryptedFile();
file.setInitializationVector(EncryptionUtils.encodeBytesToBase64String(iv));
file.setEncrypted(data);
file.setMetadataKey(0);
@ -636,7 +710,7 @@ public class EncryptionTestIT extends AbstractIT {
@Test
public void testExcludeGSON() throws Exception {
DecryptedFolderMetadata metadata = generateFolderMetadata();
DecryptedFolderMetadataFileV1 metadata = generateFolderMetadataV1_1();
String jsonWithKeys = serializeJSON(metadata);
String jsonWithoutKeys = serializeJSON(metadata, true);
@ -645,13 +719,27 @@ public class EncryptionTestIT extends AbstractIT {
assertFalse(jsonWithoutKeys.contains("metadataKeys"));
}
@Test
public void testEqualsSign() {
assertEquals("\"===\"", serializeJSON("==="));
}
@Test
public void testBase64() {
String originalString = "randomstring123";
String encodedString = EncryptionUtils.encodeStringToBase64String(originalString);
String compare = EncryptionUtils.decodeBase64StringToString(encodedString);
assertEquals(originalString, compare);
}
@Test
public void testChecksum() throws Exception {
DecryptedFolderMetadata metadata = new DecryptedFolderMetadata();
DecryptedFolderMetadataFileV1 metadata = new DecryptedFolderMetadataFileV1();
String mnemonic = "chimney potato joke science ridge trophy result estate spare vapor much room";
metadata.getFiles().put("n9WXAIXO2wRY4R8nXwmo", new DecryptedFolderMetadata.DecryptedFile());
metadata.getFiles().put("ia7OEEEyXMoRa1QWQk8r", new DecryptedFolderMetadata.DecryptedFile());
metadata.getFiles().put("n9WXAIXO2wRY4R8nXwmo", new DecryptedFile());
metadata.getFiles().put("ia7OEEEyXMoRa1QWQk8r", new DecryptedFile());
String encryptedMetadataKey = "GuFPAULudgD49S4+VDFck3LiqQ8sx4zmbrBtdpCSGcT+T0W0z4F5gYQYPlzTG6WOkdW5LJZK/";
metadata.getMetadata().setMetadataKey(encryptedMetadataKey);
@ -667,7 +755,7 @@ public class EncryptionTestIT extends AbstractIT {
String newChecksum = generateChecksum(metadata, newMnemonic);
assertNotEquals(expectedChecksum, newChecksum);
metadata.getFiles().put("aeb34yXMoRa1QWQk8r", new DecryptedFolderMetadata.DecryptedFile());
metadata.getFiles().put("aeb34yXMoRa1QWQk8r", new DecryptedFile());
newChecksum = generateChecksum(metadata, mnemonic);
assertNotEquals(expectedChecksum, newChecksum);
@ -675,8 +763,6 @@ public class EncryptionTestIT extends AbstractIT {
@Test
public void testAddIdToMigratedIds() {
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
// delete ids
arbitraryDataProvider.deleteKeyForAccount(user.getAccountName(), EncryptionUtils.MIGRATED_FOLDER_IDS);
@ -686,9 +772,21 @@ public class EncryptionTestIT extends AbstractIT {
assertTrue(isFolderMigrated(id, user, arbitraryDataProvider));
}
// TODO E2E: more tests
// more tests
// migrate v1 -> v2
// migrate v1 -> v2 with filedrop
// migrate v1 -> v1.1
// migrate v1 -> v1.1 with filedrop
// migrate v1.1 -> v2
// migrate v1.1 -> v2 with filedrop
// Helper
private boolean compareJsonStrings(String expected, String actual) {
public static boolean compareJsonStrings(String expected, String actual) {
JsonParser parser = new JsonParser();
JsonElement o1 = parser.parse(expected);
JsonElement o2 = parser.parse(actual);
@ -702,29 +800,29 @@ public class EncryptionTestIT extends AbstractIT {
}
}
private DecryptedFolderMetadata generateFolderMetadata() throws Exception {
private DecryptedFolderMetadataFileV1 generateFolderMetadataV1_1() throws Exception {
String metadataKey0 = encodeBytesToBase64String(generateKey());
String metadataKey1 = encodeBytesToBase64String(generateKey());
String metadataKey2 = encodeBytesToBase64String(generateKey());
HashMap<Integer, String> metadataKeys = new HashMap<>();
metadataKeys.put(0, EncryptionUtils.encryptStringAsymmetric(metadataKey0, cert));
metadataKeys.put(1, EncryptionUtils.encryptStringAsymmetric(metadataKey1, cert));
metadataKeys.put(2, EncryptionUtils.encryptStringAsymmetric(metadataKey2, cert));
DecryptedFolderMetadata.Encrypted encrypted = new DecryptedFolderMetadata.Encrypted();
metadataKeys.put(0, EncryptionUtils.encryptStringAsymmetric(metadataKey0, publicKey));
metadataKeys.put(1, EncryptionUtils.encryptStringAsymmetric(metadataKey1, publicKey));
metadataKeys.put(2, EncryptionUtils.encryptStringAsymmetric(metadataKey2, publicKey));
Encrypted encrypted = new Encrypted();
encrypted.setMetadataKeys(metadataKeys);
DecryptedFolderMetadata.Metadata metadata1 = new DecryptedFolderMetadata.Metadata();
DecryptedMetadata metadata1 = new DecryptedMetadata();
metadata1.setMetadataKeys(metadataKeys);
metadata1.setVersion(1.1);
metadata1.setVersion(1);
HashMap<String, DecryptedFolderMetadata.DecryptedFile> files = new HashMap<>();
HashMap<String, DecryptedFile> files = new HashMap<>();
DecryptedFolderMetadata.Data data1 = new DecryptedFolderMetadata.Data();
Data data1 = new Data();
data1.setKey("WANM0gRv+DhaexIsI0T3Lg==");
data1.setFilename("test.txt");
data1.setVersion(1);
DecryptedFolderMetadata.DecryptedFile file1 = new DecryptedFolderMetadata.DecryptedFile();
DecryptedFile file1 = new DecryptedFile();
file1.setInitializationVector("gKm3n+mJzeY26q4OfuZEqg==");
file1.setEncrypted(data1);
file1.setMetadataKey(0);
@ -732,12 +830,12 @@ public class EncryptionTestIT extends AbstractIT {
files.put("ia7OEEEyXMoRa1QWQk8r", file1);
DecryptedFolderMetadata.Data data2 = new DecryptedFolderMetadata.Data();
Data data2 = new Data();
data2.setKey("9dfzbIYDt28zTyZfbcll+g==");
data2.setFilename("test2.txt");
data2.setVersion(1);
DecryptedFolderMetadata.DecryptedFile file2 = new DecryptedFolderMetadata.DecryptedFile();
DecryptedFile file2 = new DecryptedFile();
file2.setInitializationVector("hnJLF8uhDvDoFK4ajuvwrg==");
file2.setEncrypted(data2);
file2.setMetadataKey(0);
@ -745,9 +843,10 @@ public class EncryptionTestIT extends AbstractIT {
files.put("n9WXAIXO2wRY4R8nXwmo", file2);
return new DecryptedFolderMetadata(metadata1, files);
return new DecryptedFolderMetadataFileV1(metadata1, files);
}
private boolean cryptFile(String fileName, String md5, byte[] key, byte[] iv, byte[] expectedAuthTag)
throws Exception {
File file = getFile(fileName);
@ -757,10 +856,10 @@ public class EncryptionTestIT extends AbstractIT {
File encryptedTempFile = File.createTempFile("file", "tmp");
FileOutputStream fileOutputStream = new FileOutputStream(encryptedTempFile);
fileOutputStream.write(encryptedFile.encryptedBytes);
fileOutputStream.write(encryptedFile.getEncryptedBytes());
fileOutputStream.close();
byte[] authenticationTag = decodeStringToBase64Bytes(encryptedFile.authenticationTag);
byte[] authenticationTag = decodeStringToBase64Bytes(encryptedFile.getAuthenticationTag());
// verify authentication tag
assertTrue(Arrays.equals(expectedAuthTag, authenticationTag));

View file

@ -0,0 +1,157 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.utils
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedMetadata
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedUser
import com.owncloud.android.lib.resources.status.E2EVersion
class EncryptionTestUtils {
val t1PrivateKey =
"MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQC1p8eYMFwGoi7geYzEwNbePRLL5LRhorAecFG3zkpLBwSi/QHkU4" +
"u4uSegEbHgOfe73eKVOFdfFpw8wd5cvtY+4CzbX8bu+yrC+tFGcJ25/4VQ78Bl4MI0SvOmxDwuZNrg9SWgs9RwialKOsfCEyz0" +
"SS8RstGNt5KZKn1e8z7V9X/eORPmOQ5KcIXHlMbAY3m4erBSKvhRZdqy+Dbnc0rZeZaKkoIMJH1OYfVto/ek12iIKF2YStPVzo" +
"TgNsFelPDxeA/lltgf6qDVRD+ELydEncPIJwcv52D8ZitoEyEOfjDZW+rvvE02g1ZD1xPkDLpwltAsFCglCKvKBAWuhthFAgMB" +
"AAECgf8BN1MLcq+6m8C1tzNvN/UDd9c0rUpexM6D5eC4O+6B7YGidEqIhHVIzUj0e2HUgpRBbURxsvF1FWdIT2gu7dnULtOGWQ" +
"xNujJ0kGwXfAnqxh/rACDFb5TS3sJawEExC5yJw14bCEbE/0uBF5uiTU/U9AV7PKHlqAKsS2RtcwPNceB8zDu0hh/Mb/uS7274" +
"TsxUllx0WzGZrozO1K6AlOete9rXmmpghpFTNVhxgf0pxe3hrK+tZGSL9di+Wft9eCvSbdG/FzeXgwVqmGtWU7kSB7FqstEEJO" +
"4VpOSyEfcXGHTHwdZjrhBUuAcjWE8E0mCKa8htRE52czb3C0f7ZYkCgYEA5eH3vmHEgQjXzSSEtbmDLRq9X9SB7pIAIXHj2UuE" +
"OTkLUJ/7xLTHqt82jqZaZzns1RZIJXKZjH85CswQp/py2/qD240KvA/N+ELZaciaV+Wg+m4+iHdi0DyPkaKaBtFG1nsR2GbVWO" +
"1OsaTUZTG4D7RCUErU6XVmNPQKSk5uRA0CgYEAykskpX3KKuWq5nxV4vwgPmxz+uAfCtaGhcPEUg764SR+n0ODAvGiEJU7B0Q2" +
"oX621pDOQeRfFufiMWfD8ByhErs1HFCmW69YPlR8qamfc8tHG5UM+r3bb49sDEYU4qr1Ji5Zzs4XgfmToKLbWdzzhaW6YxqO7N" +
"ntIIh2169PPxkCgYBF2TAWl8xGTLKNcYAlW1XBObO6z24fWBtUDi/mEWz+mheXCtVMAoX8pFAGbgNgBBiy8k8/mZ+QMgPaBQE2" +
"mQGXV3oDFsrhM4go298Fpl9HP8126lJz0pqinRQecyKL2cDFYKWedDh1Cb30ehnTGZVMqD/R97rTqMlCY7hQtZ4JbQKBgEXpLD" +
"QJQeoLT0GybJgyXA5WuspT1EaRlxH5cwqM5MUUMLJnyYol6cVjXXAIcfzj5tpGVxHMk9Q9tR0v6DY+HqhzjEpJ0QRUl+GKnz6f" +
"QVzqPpvYqhCptoFahpPDUIp5XJmiYSUoclVX5F4aikYHJx3kBYMkdYqDUgDxSGkHzBJZAoGAHV44xgTW02dgeB5GfDJVWCJKAU" +
"GsYOFuUehKUBXSJ0929hdP0sjOQDJN3DEDISzmgdWX5NyLJxEYgFWNivpePjWCWzOzyua3nPSpvxPIUB7xh27gjT91glj1hEmy" +
"sCd7+9yoMPiCXR7iigRycxegI/Krd39QzISSk9O0finfytU="
val t1PublicKey = """-----BEGIN CERTIFICATE-----
MIIC6DCCAdCgAwIBAgIBADANBgkqhkiG9w0BAQUFADANMQswCQYDVQQDDAJ0MTAe
Fw0yMzA3MjUwNzU3MTJaFw00MzA3MjAwNzU3MTJaMA0xCzAJBgNVBAMMAnQxMIIB
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtafHmDBcBqIu4HmMxMDW3j0S
y+S0YaKwHnBRt85KSwcEov0B5FOLuLknoBGx4Dn3u93ilThXXxacPMHeXL7WPuAs
21/G7vsqwvrRRnCduf+FUO/AZeDCNErzpsQ8LmTa4PUloLPUcImpSjrHwhMs9Ekv
EbLRjbeSmSp9XvM+1fV/3jkT5jkOSnCFx5TGwGN5uHqwUir4UWXasvg253NK2XmW
ipKCDCR9TmH1baP3pNdoiChdmErT1c6E4DbBXpTw8XgP5ZbYH+qg1UQ/hC8nRJ3D
yCcHL+dg/GYraBMhDn4w2Vvq77xNNoNWQ9cT5Ay6cJbQLBQoJQirygQFrobYRQID
AQABo1MwUTAdBgNVHQ4EFgQUE9zCeA9/QMAtVgLxD23X6ZcodhMwHwYDVR0jBBgw
FoAUE9zCeA9/QMAtVgLxD23X6ZcodhMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG
9w0BAQUFAAOCAQEAZdy/YjJlvnz3FQwxp6oVtMJccpdxveEPfLzgaverhtd/vP8O
AvDzOLgQJHmrDS91SG503eU4cYGyuNKwd77OyTnqMg+GUEmJhGfPpSVrEIdh65jv
q61T4oqBdehevVmBq54rGiwL0DGv1DlXQlwiJZP4qni2KnOEFcnvL3gVtRnQjXQ+
kHvlMshkK6w021EMV5NfjG2zg67wC65rLaej5f6Ssp2S7g2VtmE4aXq1bjAuEbqk
4TiyZHLDdsJuqzyGyyOpMV7i9ucXDoaZt9cGS9hT2vRxTrSH63vKR8Xeig9+stLw
t9ONcUqCKP7hd8rajtxM4JIIRExwD8OkgARWGg==
-----END CERTIFICATE-----"""
val johnPrivateKey =
"""MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDuPcSvlhqElPQsCdJEuGmptGj4TUBWe33yu+ncOYR8Ec3M0H4NL0gE
|ORJJcz9i18ByLpNzDy6NUGOtlf9YSat/zKdAfFiZJolKc/y4BPfTr8xx5ml2mu4Rz39LXRru+nnhluV3g1h2Z9LvWhUVUqAztz9W2H
|H6uC7jx+7HNtYC9VgsVzHjuHPQMlOePPZlr9Hry5enF/Psn24RdiKqwCz8WhsOwtmW5PdHLLBVHAoF53URnFR4sgmLLGlS2GEZ8hvx
|vdV/2NmhRWLebmCZziyklAe9gCR9lgfN32tqzyMG7VptBHFy7YJidWjpjSZPGEqFBL+fmCO/cTGJAXfCn9djAgMBAAECggEAV2QBCg
|edopShHKZdoyeiWsX621o7B341LR0RI99VYc2GGGNCWcPGPwZQVvEXh0JtLXU4UTR4dw3OApbLG6+qYS7JCzaRqVwhcFYrlbT804Hh
|FMbYWNFsEsxyfUqh3peyrbWUZsqfYI+lKHd61F+CtHW7nje3V6jISnXEeP78cgioKOX8gsCG8DEWsmaLrQz0PyMwdhucRfa8Bm6qeX
|NY+wCMg8lyH/+OLlyCZTdkaWbTBBD5UXGbZly8iX17McmsYhdjFyx1l0NQnVMAYjOpXXEkeEixZpSfm3GYxmdaQqZFkpbI/FbQF0yD
|7hLrGwiRTDcyPUz+QypUv8CZxpXbgQKBgQD3btuYmb+BpPZjryfa3worv/3XQCTs08V0TX3mDxHVQL95TgP+L8/Z/brxIMBNpwG1wk
|iCWLYLer68+qioMTohuzeUx7hRKcoHa9ezW8m7m9AcPmAnzNticPYv835BQjEu/avU98rwIDihsYgxcjU3L7/P2ajVgUDigQxmE3gO
|OwKBgQD2fXBLwch0P5g2GCyOPYvSgyF/umS7mcyUVTE4WOoJNDf8Q+Bx1dA2yAKQaoVghqW4uCfOAo/rERvGAYZ7fm7nFwx1jZ8ToT
|dKZwybIFPjF/zkfuZLajYxVOPnzuQrsXnjcGg/ltMKZg3NqnGQGnD1S3eOhZ+dIOBmb7+jSO4A+QKBgASqnpGeNLJpPgxbPVEva62v
|jUYF+6xLwimTXJB+MEPpWLMc+Y5NsInX8zKg/393atzWsS9kJOrKgdZmk8+4PfRs53ty2NMPCrRhIExNqtxS7/XYZ0/Y2TpeDwaQfQ
|0WBn9wYVE+6yDkOq0x//OOx9ommGN/I2QDcAnVjTpPm7AJAoGAYT8cDsdlTnfIlY70BSpC/8q8bKgdFeaXz+3MfW6W5wqzC9O7uS2h
|9/rxCAj+lhaJS1dcXOql3Rfi3Tu80vwOxR1SzQ4StKvmJHSDhLA8aFwOahemxBojR1M2lz4IxzQ94n12o5/dozygNYQJSdEkv6IGiT
|QuxM8zuTZdZQ5g2AECgYAujetfkwgVW7/gumpMKytoY0VuTzF4Y/XZfqBMVIiPIuUl57JbDzrcx6YVXX3PavxNWmBLBmMq3SHMbdva
|H7LnU/8rvkT8xRVLg/w/bRJc3Lb3oUjrdhkUQUYDoOfMoFA+ceZ2L6bnSXwm86KKV+xoXWpxAoL4AvdNrMhoWw3+yg=="""
.trimMargin()
val johnPublicKey = """-----BEGIN CERTIFICATE-----
MIIDkDCCAnigAwIBAgIBADANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQGEwJERTEb
MBkGA1UECAwSQmFkZW4tV3VlcnR0ZW1iZXJnMRIwEAYDVQQHDAlTdHV0dGdhcnQx
EjAQBgNVBAoMCU5leHRjbG91ZDENMAsGA1UEAwwEam9objAeFw0yMzA3MTQwNzM0
NTZaFw00MzA3MDkwNzM0NTZaMGExCzAJBgNVBAYTAkRFMRswGQYDVQQIDBJCYWRl
bi1XdWVydHRlbWJlcmcxEjAQBgNVBAcMCVN0dXR0Z2FydDESMBAGA1UECgwJTmV4
dGNsb3VkMQ0wCwYDVQQDDARqb2huMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEA7j3Er5YahJT0LAnSRLhpqbRo+E1AVnt98rvp3DmEfBHNzNB+DS9IBDkS
SXM/YtfAci6Tcw8ujVBjrZX/WEmrf8ynQHxYmSaJSnP8uAT306/MceZpdpruEc9/
S10a7vp54Zbld4NYdmfS71oVFVKgM7c/Vthx+rgu48fuxzbWAvVYLFcx47hz0DJT
njz2Za/R68uXpxfz7J9uEXYiqsAs/FobDsLZluT3RyywVRwKBed1EZxUeLIJiyxp
UthhGfIb8b3Vf9jZoUVi3m5gmc4spJQHvYAkfZYHzd9ras8jBu1abQRxcu2CYnVo
6Y0mTxhKhQS/n5gjv3ExiQF3wp/XYwIDAQABo1MwUTAdBgNVHQ4EFgQUmTeILVuB
tv70fTGkXWGAueDp5kAwHwYDVR0jBBgwFoAUmTeILVuBtv70fTGkXWGAueDp5kAw
DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAyVtq9XAvW7nxSW/8
hp30z6xbzGiuviXhy/Jo91VEa8IRsWCCn3OmDFiVduTEowx76tf8clJP0gk7Pozi
6dg/7Fin+FqQGXfCk8bLAh9gXKAikQ2GK8yRN3slRFwYC2mm23HrLdKXZHUqJcpB
Mz2zsSrOGPj1YsYOl/U8FU6KA7Yj7U3q7kDMYTAgzUPZAH+d1DISGWpZsMa0RYid
vigCCLByiccmS/Co4Sb1esF58H+YtV5+nFBRwx881U2g2TgDKF1lPMK/y3d8B8mh
UtW+lFxRpvyNUDpsMjOErOrtNFEYbgoUJLtqwBMmyGR+nmmh6xna331QWcRAmw0P
nDO4ew==
-----END CERTIFICATE-----"""
@Throws(java.lang.Exception::class)
fun generateFolderMetadataV2(userId: String, cert: String): DecryptedFolderMetadataFile {
val metadata = DecryptedMetadata().apply {
metadataKey = EncryptionUtils.generateKey()
keyChecksums.add(EncryptionUtilsV2().hashMetadataKey(metadataKey))
}
val file1 = DecryptedFile(
"image1.png",
"image/png",
"gKm3n+mJzeY26q4OfuZEqg==",
"PboI9tqHHX3QeAA22PIu4w==",
"WANM0gRv+DhaexIsI0T3Lg=="
)
val file2 = DecryptedFile(
"image2.png",
"image/png",
"hnJLF8uhDvDoFK4ajuvwrg==",
"qOQZdu5soFO77Y7y4rAOVA==",
"9dfzbIYDt28zTyZfbcll+g=="
)
val users = mutableListOf(
DecryptedUser(userId, cert)
)
// val filedrop = mutableMapOf(
// Pair(
// "eie8iaeiaes8e87td6",
// DecryptedFile(
// "test2.txt",
// "txt/plain",
// "hnJLF8uhDvDoFK4ajuvwrg==",
// "qOQZdu5soFO77Y7y4rAOVA==",
// "9dfzbIYDt28zTyZfbcll+g=="
// )
// )
// )
metadata.files["ia7OEEEyXMoRa1QWQk8r"] = file1
metadata.files["n9WXAIXO2wRY4R8nXwmo"] = file2
return DecryptedFolderMetadataFile(metadata, users, mutableMapOf(), E2EVersion.V2_0.value)
}
}

View file

@ -0,0 +1,47 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.utils
import com.owncloud.android.AbstractIT
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl
import com.owncloud.android.lib.resources.e2ee.CsrHelper
import org.junit.Assert.assertEquals
import org.junit.Test
class EncryptionUtilsIT : AbstractIT() {
@Throws(
java.security.NoSuchAlgorithmException::class,
java.io.IOException::class,
org.bouncycastle.operator.OperatorCreationException::class
)
@Test
fun saveAndRestorePublicKey() {
val arbitraryDataProvider = ArbitraryDataProviderImpl(targetContext)
val keyPair = EncryptionUtils.generateKeyPair()
val e2eUser = "e2e-user"
val key = CsrHelper().generateCsrPemEncodedString(keyPair, e2eUser)
EncryptionUtils.savePublicKey(user, key, e2eUser, arbitraryDataProvider)
assertEquals(key, EncryptionUtils.getPublicKey(user, e2eUser, arbitraryDataProvider))
}
}

View file

@ -0,0 +1,911 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.utils
import com.google.gson.reflect.TypeToken
import com.nextcloud.client.account.MockUser
import com.nextcloud.common.User
import com.owncloud.android.AbstractIT
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.e2e.v1.decrypted.Data
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedMetadata
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedUser
import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFiledrop
import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFiledropUser
import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFolderMetadataFile
import com.owncloud.android.util.EncryptionTestIT
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
import org.junit.Assert.assertNotEquals
import org.junit.Test
class EncryptionUtilsV2IT : AbstractIT() {
private val encryptionTestUtils = EncryptionTestUtils()
private val encryptionUtilsV2 = EncryptionUtilsV2()
private val enc1UserId = "enc1"
private val enc1PrivateKey = """
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAo
IBAQDsn0JKS/THu328z1IgN0VzYU53HjSX03WJIgWkmyTaxbiKpoJaKbksXmfSpgzV
GzKFvGfZ03fwFrN7Q8P8R2e8SNiell7mh1TDw9/0P7Bt/ER8PJrXORo+GviKHxaLr7
Y0BJX9i/nW/L0L/VaE8CZTAqYBdcSJGgHJjY4UMf892ZPTa9T2Dl3ggdMZ7BQ2kiCi
CC3qV99b0igRJGmmLQaGiAflhFzuDQPMifUMq75wI8RSRPdxUAtjTfkl68QHu7Umye
yy33OQgdUKaTl5zcS3VSQbNjveVCNM4RDH1RlEc+7Wf1BY8APqT6jbiBcROJD2CeoL
H2eiIJCi+61ZkSGfAgMBAAECggEBALFStCHrhBf+GL9a+qer4/8QZ/X6i91PmaBX/7
SYk2jjjWVSXRNmex+V6+Y/jBRT2mvAgm8J+7LPwFdatE+lz0aZrMRD2gCWYF6Itpda
90OlLkmQPVWWtGTgX2ta2tF5r2iSGzk0IdoL8zw98Q2UzpOcw30KnWtFMxuxWk0mHq
pgp00g80cDWg3+RPbWOhdLp5bflQ36fKDfmjq05cGlIk6unnVyC5HXpvh4d4k2EWlX
rjGsndVBPCjGkZePlLRgDHxT06r+5XdJ+1CBDZgCsmjGz3M8uOHyCfVW0WhB7ynzDT
agVgz0iqpuhAi9sPt6iWWwpAnRw8cQgqEKw9bvKKECgYEA/WPi2PJtL6u/xlysh/H7
A717CId6fPHCMDace39ZNtzUzc0nT5BemlcF0wZ74NeJSur3Q395YzB+eBMLs5p8mA
95wgGvJhM65/J+HX+k9kt6Z556zLMvtG+j1yo4D0VEwm3xahB4SUUP+1kD7dNvo4+8
xeSCyjzNllvYZZC0DrECgYEA7w8pEqhHHn0a+twkPCZJS+gQTB9Rm+FBNGJqB3XpWs
TeLUxYRbVGk0iDve+eeeZ41drxcdyWP+WcL34hnrjgI1Fo4mK88saajpwUIYMy6+qM
LY+jC2NRSBox56eH7nsVYvQQK9eKqv9wbB+PF9SwOIvuETN7fd8mAY02UnoaaU8CgY
BoHRKocXPLkpZJuuppMVQiRUi4SHJbxDo19Tp2w+y0TihiJ1lvp7I3WGpcOt3LlMQk
tEbExSvrRZGxZKH6Og/XqwQsYuTEkEIz679F/5yYVosE6GkskrOXQAfh8Mb3/04xVV
tMaVgDQw0+CWVD4wyL+BNofGwBDNqsXTCdCsfxAQKBgQCDv2EtbRw0y1HRKv21QIxo
ju5cZW4+cDfVPN+eWPdQFOs1H7wOPsc0aGRiiupV2BSEF3O1ApKziEE5U1QH+29bR4
R8L1pemeGX8qCNj5bCubKjcWOz5PpouDcEqimZ3q98p3E6GEHN15UHoaTkx0yO/V8o
j6zhQ9fYRxDHB5ACtQKBgQCOO7TJUO1IaLTjcrwS4oCfJyRnAdz49L1AbVJkIBK0fh
JLecOFu3ZlQl/RStQb69QKb5MNOIMmQhg8WOxZxHcpmIDbkDAm/J/ovJXFSoBdOr5o
uQsYsDZhsWW97zvLMzg5pH9/3/1BNz5q3Vu4HgfBSwWGt4E2NENj+XA+QAVmGA==
""".trimIndent()
private val enc1Cert = """
-----BEGIN CERTIFICATE-----
MIIDpzCCAo+gAwIBAgIBADANBgkqhkiG9w0BAQUFADBuMRowGAYDVQQDDBF3d3cu
bmV4dGNsb3VkLmNvbTESMBAGA1UECgwJTmV4dGNsb3VkMRIwEAYDVQQHDAlTdHV0
dGdhcnQxGzAZBgNVBAgMEkJhZGVuLVd1ZXJ0dGVtYmVyZzELMAkGA1UEBhMCREUw
HhcNMTcwOTI2MTAwNDMwWhcNMzcwOTIxMTAwNDMwWjBuMRowGAYDVQQDDBF3d3cu
bmV4dGNsb3VkLmNvbTESMBAGA1UECgwJTmV4dGNsb3VkMRIwEAYDVQQHDAlTdHV0
dGdhcnQxGzAZBgNVBAgMEkJhZGVuLVd1ZXJ0dGVtYmVyZzELMAkGA1UEBhMCREUw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDsn0JKS/THu328z1IgN0Vz
YU53HjSX03WJIgWkmyTaxbiKpoJaKbksXmfSpgzVGzKFvGfZ03fwFrN7Q8P8R2e8
SNiell7mh1TDw9/0P7Bt/ER8PJrXORo+GviKHxaLr7Y0BJX9i/nW/L0L/VaE8CZT
AqYBdcSJGgHJjY4UMf892ZPTa9T2Dl3ggdMZ7BQ2kiCiCC3qV99b0igRJGmmLQaG
iAflhFzuDQPMifUMq75wI8RSRPdxUAtjTfkl68QHu7Umyeyy33OQgdUKaTl5zcS3
VSQbNjveVCNM4RDH1RlEc+7Wf1BY8APqT6jbiBcROJD2CeoLH2eiIJCi+61ZkSGf
AgMBAAGjUDBOMB0GA1UdDgQWBBTFrXz2tk1HivD9rQ75qeoyHrAgIjAfBgNVHSME
GDAWgBTFrXz2tk1HivD9rQ75qeoyHrAgIjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3
DQEBBQUAA4IBAQARQTX21QKO77gAzBszFJ6xVnjfa23YZF26Z4X1KaM8uV8TGzuN
JA95XmReeP2iO3r8EWXS9djVCD64m2xx6FOsrUI8HZaw1JErU8mmOaLAe8q9RsOm
9Eq37e4vFp2YUEInYUqs87ByUcA4/8g3lEYeIUnRsRsWsA45S3wD7wy07t+KAn7j
yMmfxdma6hFfG9iN/egN6QXUAyIPXvUvlUuZ7/BhWBj/3sHMrF9quy9Q2DOI8F3t
1wdQrkq4BtStKhciY5AIXz9SqsctFHTv4Lwgtkapoel4izJnO0ZqYTXVe7THwri9
H/gua6uJDWH9jk2/CiZDWfsyFuNUuXvDSp05
-----END CERTIFICATE-----
""".trimIndent()
private val enc2Cert = """
-----BEGIN CERTIFICATE-----
MIIC7DCCAdSgAwIBAgIBADANBgkqhkiG9w0BAQUFADAPMQ0wCwYDVQQDDARlbmMz
MB4XDTIwMDcwODA3MzE1OFoXDTQwMDcwMzA3MzE1OFowDzENMAsGA1UEAwwEZW5j
MzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI/83eC/EF3xOocwjO+Z
ZkPc1TFxt3aUgjEvrpZu45LOqesG67kkkVDYgjeg3Biz9XRUQXqtXaAyxRZH8GiH
PFyKUiP1bUlCptd8X+hk9vxeN25YS5OS2RrxU9tDQ/dVOHr20427UvVCighotQnR
/6+md1FQMV92PFxji7OP5TWOE1y389X6eb7kSPLs8Tu+2PpqaNVQ9C/89Y8KNYWs
x9Zo+kbQhjfFFUikEpkuzMgT9QLaeq6xuXIPP+y1tzNmF6NTL0a2GoYULuxYWnCe
joFyXj77LuLmK+KXfPdhvlxa5Kl9XHSxKPHBVVQpwPqNMT+b2T1VLE2l7M9NfImy
iLcCAwEAAaNTMFEwHQYDVR0OBBYEFBKubDeR2lXwuyTrdyv6O7euPS4PMB8GA1Ud
IwQYMBaAFBKubDeR2lXwuyTrdyv6O7euPS4PMA8GA1UdEwEB/wQFMAMBAf8wDQYJ
KoZIhvcNAQEFBQADggEBAChCOIH8CkEpm1eqjsuuNPa93aduLjtnZXat5eIKsKCl
rL9nFslpg/DO5SeU5ynPY9F2QjX5CN/3RxDXum9vFfpXhTJphOv8N0uHU4ucmQxE
DN388Vt5VtN3V2pzNUL3JSiG6qeYG047/r/zhGFVpcgb2465G5mEwFT0qnkEseCC
VVZ63GN8hZgUobyRXxMIhkfWlbO1dgABB4VNyudq0CW8urmewkkbUBwCslvtUvPM
WuzpQjq2A80bvbrAqO5VUfvMcqRiUWkDgfa6cHXyV0o4N11mMIoxsMgh+PFYr6lR
BHkuQHqKEwP8kkWugIFj3TMcy9dYtXfMXWvzFaDoE4s=
-----END CERTIFICATE-----
""".trimIndent()
private val enc2PrivateKey = """
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCP/N3gvxBd8TqH
MIzvmWZD3NUxcbd2lIIxL66WbuOSzqnrBuu5JJFQ2II3oNwYs/V0VEF6rV2gMsUW
R/BohzxcilIj9W1JQqbXfF/oZPb8XjduWEuTktka8VPbQ0P3VTh69tONu1L1QooI
aLUJ0f+vpndRUDFfdjxcY4uzj+U1jhNct/PV+nm+5Ejy7PE7vtj6amjVUPQv/PWP
CjWFrMfWaPpG0IY3xRVIpBKZLszIE/UC2nqusblyDz/stbczZhejUy9GthqGFC7s
WFpwno6Bcl4++y7i5ivil3z3Yb5cWuSpfVx0sSjxwVVUKcD6jTE/m9k9VSxNpezP
TXyJsoi3AgMBAAECggEACWwKFtlZ2FPfORZ3unwGwZ0TRFOFJljMdiyBF6307Vfh
rZP729clPS2Vw88eZ+1qu+yBhmYO0NtRo0Yc2LI0xHd2rYyzVI5sfYBRhFMLCHOf
2/QiKet7knRFQP1TVr14Xy+Eo2slIBB1GNzFL/nSaeuSNjtxp6YEiCUpcJwTayAi
Squ5QWMxhlciLKvwUkraFRBqkugvMz3jXzuk/i+DcYlOgoj+tytweNn/azOMH9MH
mWI+3owYspjzE1rVpbrcWImvlnbInd0z9KaQPpBf7Njj7wtyBMaYww4K4GCMhboD
SQCYgpnznWkPIN3jyXtmNVSsZ1nvD+Laod+0p7giOQKBgQDA6KEKctYpbt051yTe
2UP8hpq+MUSS7FIXiHlUc8s0PSujouypUyzfrPeL6yquI0GtKHkMVCWwfT+otMZR
VnklofrmPTPovvsUQFM4Di411NZwzfxEbBFyVXAUWcLd9NxJ1hZW7w+hLk/N5Bej
DOa2CncZmifyMNIlvIn7T1vDyQKBgQC/FE8HaDBoN98m/3rEjx7/rVtX8dCei5By
Fzg/yQ2u4ELbf/Qk/n4k75sy0690EwnFdJxVn2gdNgS1YDv8YP/N5Wfq8xnX9V9B
irWY/W24cN2qDNXm5i8o5wklyt+fDVqMcEHFfONUpLC+RYmOdc1rrFxPaQOYYYpp
dWsnuG0ofwKBgBm6rUf8ew35qG3/gP5sEgJLXbZCUfgapvRWkoAuFYs5IWno4BHR
cym+IyI5Um75atgSjtqTGpfIjMYOnmjY1L2tNg6hWRwQ5OIVlkPiuE0bvyI6hwwF
MeqC9LjyI+iAsSTz9fTQW9BOofw/ENwBa4AaMzpp8iv+UPkRhYHMWtvpAoGAX6As
RMqxnxaHCR9GM2Rk4RPC6OpNu2qhKVfRgKp/vIrjKrKIXpM2UgnPo8oovnBgrX7E
Vl1mX2gPRy4YFx/8JPCv5vcucdOMjmJ6q0v5QxrI9DdkPR/pbhDhlRZIf3LRZAMy
B0GPC2c4RKDMTI1L9pzVvbASaoo2GLz4mXJEvsUCgYEAibwFNXz1H52sZtL6/1zQ
1rHCTS8qkryBhxl5eYa6MV5YkbLJZZstF0w2nLxkPba8NttS/nJqjX/iJobD5uLb
UzeD8jMeAWPNt4DZCtA4ossNYcXIMKqBVFKOANMvAAvLMpVdlNYSucNnTSQcLwI6
2J9mW5WvAAaG+j28Q/GKSuE=
""".trimIndent()
@Test
fun testEncryptDecryptMetadata() {
val metadataKey = EncryptionUtils.generateKey()
val metadata = DecryptedMetadata(
mutableListOf("hash1", "hash of key 2"),
false,
1,
mutableMapOf(
Pair(EncryptionUtils.generateUid(), "Folder 1"),
Pair(EncryptionUtils.generateUid(), "Folder 2"),
Pair(EncryptionUtils.generateUid(), "Folder 3")
),
mutableMapOf(
Pair(
EncryptionUtils.generateUid(),
DecryptedFile(
"file 1.png",
"image/png",
"initializationVector",
"authenticationTag",
"key 1"
)
),
Pair(
EncryptionUtils.generateUid(),
DecryptedFile(
"file 2.png",
"image/png",
"initializationVector 2",
"authenticationTag 2",
"key 2"
)
)
),
metadataKey
)
val encrypted = encryptionUtilsV2.encryptMetadata(metadata, metadataKey)
val decrypted = encryptionUtilsV2.decryptMetadata(encrypted, metadataKey)
assertEquals(metadata, decrypted)
}
@Throws(Throwable::class)
@Test
fun encryptDecryptSymmetric() {
val string = "123"
val metadataKey = EncryptionUtils.generateKeyString()
val e = EncryptionUtils.encryptStringSymmetricAsString(
string,
metadataKey.toByteArray()
)
val d = EncryptionUtils.decryptStringSymmetric(e, metadataKey.toByteArray())
assertEquals(string, d)
val encryptedMetadata = EncryptionUtils.encryptStringSymmetric(
string,
metadataKey.toByteArray(),
EncryptionUtils.ivDelimiter
)
val d2 = EncryptionUtils.decryptStringSymmetric(
encryptedMetadata.ciphertext,
metadataKey.toByteArray()
)
assertEquals(string, d2)
val decrypted = EncryptionUtils.decryptStringSymmetric(
encryptedMetadata.ciphertext,
metadataKey.toByteArray(),
encryptedMetadata.authenticationTag,
encryptedMetadata.nonce
)
assertEquals(string, EncryptionUtils.decodeBase64BytesToString(decrypted))
}
@Test
fun testEncryptDecryptUser() {
val metadataKeyBase64 = EncryptionUtils.generateKeyString()
val metadataKey = EncryptionUtils.decodeStringToBase64Bytes(metadataKeyBase64)
val user = DecryptedUser("t1", encryptionTestUtils.t1PublicKey)
val encryptedUser = encryptionUtilsV2.encryptUser(user, metadataKey)
assertNotEquals(encryptedUser.encryptedMetadataKey, metadataKeyBase64)
val decryptedMetadataKey = encryptionUtilsV2.decryptMetadataKey(encryptedUser, encryptionTestUtils.t1PrivateKey)
val decryptedMetadataKeyBase64 = EncryptionUtils.encodeBytesToBase64String(decryptedMetadataKey)
assertEquals(metadataKeyBase64, decryptedMetadataKeyBase64)
}
@Throws(com.owncloud.android.operations.UploadException::class, Throwable::class)
@Test
fun testEncryptDecryptMetadataFile() {
val enc1 = MockUser("enc1", "Nextcloud")
val root = OCFile("/")
storageManager.saveFile(root)
val folder = OCFile("/enc/").apply {
parentId = storageManager.getFileByDecryptedRemotePath("/")?.fileId ?: throw IllegalStateException()
}
val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert)
val encrypted = encryptionUtilsV2.encryptFolderMetadataFile(
metadataFile,
enc1.accountName,
folder,
storageManager,
client,
enc1PrivateKey,
user,
targetContext,
arbitraryDataProvider
)
val signature = encryptionUtilsV2.getMessageSignature(enc1Cert, enc1PrivateKey, encrypted)
val decrypted = encryptionUtilsV2.decryptFolderMetadataFile(
encrypted,
enc1.accountName,
enc1PrivateKey,
folder,
storageManager,
client,
0,
signature,
user,
targetContext,
arbitraryDataProvider
)
assertEquals(metadataFile, decrypted)
}
@Test
fun addFile() {
val enc1 = MockUser("enc1", "Nextcloud")
val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert)
assertEquals(2, metadataFile.metadata.files.size)
assertEquals(1, metadataFile.metadata.counter)
val updatedMetadata = encryptionUtilsV2.addFileToMetadata(
EncryptionUtils.generateUid(),
OCFile("/test.jpg").apply {
mimeType = MimeType.JPEG
},
EncryptionUtils.generateIV(),
EncryptionUtils.generateUid(), // random string, not real tag
EncryptionUtils.generateKey(),
metadataFile,
storageManager
)
assertEquals(3, updatedMetadata.metadata.files.size)
assertEquals(2, updatedMetadata.metadata.counter)
}
@Test
fun removeFile() {
val enc1 = MockUser("enc1", "Nextcloud")
val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert)
assertEquals(2, metadataFile.metadata.files.size)
val filename = metadataFile.metadata.files.keys.first()
encryptionUtilsV2.removeFileFromMetadata(filename, metadataFile)
assertEquals(1, metadataFile.metadata.files.size)
}
@Test
fun renameFile() {
val enc1 = MockUser("enc1", "Nextcloud")
val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert)
assertEquals(2, metadataFile.metadata.files.size)
val key = metadataFile.metadata.files.keys.first()
val decryptedFile = metadataFile.metadata.files[key]
val filename = decryptedFile?.filename
val newFilename = "New File 1"
encryptionUtilsV2.renameFile(key, newFilename, metadataFile)
assertEquals(newFilename, metadataFile.metadata.files[key]?.filename)
assertNotEquals(filename, newFilename)
assertNotEquals(filename, metadataFile.metadata.files[key]?.filename)
}
@Test
fun addFolder() {
val folder = OCFile("/e/")
val enc1 = MockUser("enc1", "Nextcloud")
val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert)
assertEquals(2, metadataFile.metadata.files.size)
assertEquals(3, metadataFile.metadata.folders.size)
val updatedMetadata = encryptionUtilsV2.addFolderToMetadata(
EncryptionUtils.generateUid(),
"new subfolder",
metadataFile,
folder,
storageManager
)
assertEquals(2, updatedMetadata.metadata.files.size)
assertEquals(4, updatedMetadata.metadata.folders.size)
}
@Test
fun removeFolder() {
val folder = OCFile("/e/")
val enc1 = MockUser("enc1", "Nextcloud")
val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert)
assertEquals(2, metadataFile.metadata.files.size)
assertEquals(3, metadataFile.metadata.folders.size)
val encryptedFileName = EncryptionUtils.generateUid()
var updatedMetadata = encryptionUtilsV2.addFolderToMetadata(
encryptedFileName,
"new subfolder",
metadataFile,
folder,
storageManager
)
assertEquals(2, updatedMetadata.metadata.files.size)
assertEquals(4, updatedMetadata.metadata.folders.size)
updatedMetadata = encryptionUtilsV2.removeFolderFromMetadata(
encryptedFileName,
updatedMetadata
)
assertEquals(2, updatedMetadata.metadata.files.size)
assertEquals(3, updatedMetadata.metadata.folders.size)
}
@Test
fun verifyMetadata() {
val folder = OCFile("/e/")
val enc1 = MockUser("enc1", "Nextcloud")
val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert)
val encrypted = encryptionUtilsV2.encryptFolderMetadataFile(
metadataFile,
enc1UserId,
folder,
storageManager,
client,
enc1PrivateKey,
user,
targetContext,
arbitraryDataProvider
)
val signature = encryptionUtilsV2.getMessageSignature(enc1Cert, enc1PrivateKey, encrypted)
encryptionUtilsV2.verifyMetadata(encrypted, metadataFile, 0, signature)
assertTrue(true) // if we reach this, test is successful
}
private fun generateDecryptedFileV1(): com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile {
return com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile().apply {
encrypted = Data().apply {
key = EncryptionUtils.generateKeyString()
filename = "Random filename.jpg"
mimetype = MimeType.JPEG
version = 1.0
}
initializationVector = EncryptionUtils.generateKeyString()
authenticationTag = EncryptionUtils.generateKeyString()
}
}
@Test
fun testMigrateDecryptedV1ToV2() {
val v1 = generateDecryptedFileV1()
val v2 = encryptionUtilsV2.migrateDecryptedFileV1ToV2(v1)
assertEquals(v1.encrypted.filename, v2.filename)
assertEquals(v1.encrypted.mimetype, v2.mimetype)
assertEquals(v1.authenticationTag, v2.authenticationTag)
assertEquals(v1.initializationVector, v2.nonce)
assertEquals(v1.encrypted.key, v2.key)
}
@Test
fun testMigrateMetadataV1ToV2() {
OCFile("/").apply {
storageManager.saveFile(this)
}
val folder = OCFile("/enc/").apply {
parentId = storageManager.getFileByDecryptedRemotePath("/")?.fileId ?: throw IllegalStateException()
}
val v1 = DecryptedFolderMetadataFileV1().apply {
metadata = com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedMetadata().apply {
metadataKeys = mapOf(Pair(0, EncryptionUtils.generateKeyString()))
}
files = mapOf(
Pair(EncryptionUtils.generateUid(), generateDecryptedFileV1()),
Pair(EncryptionUtils.generateUid(), generateDecryptedFileV1()),
Pair(
EncryptionUtils.generateUid(),
com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile().apply {
encrypted = Data().apply {
key = EncryptionUtils.generateKeyString()
filename = "subFolder"
mimetype = MimeType.WEBDAV_FOLDER
}
initializationVector = EncryptionUtils.generateKeyString()
authenticationTag = null
}
)
)
}
val v2 = encryptionUtilsV2.migrateV1ToV2(
v1,
enc1UserId,
enc1Cert,
folder,
storageManager
)
assertEquals(2, v2.metadata.files.size)
assertEquals(1, v2.metadata.folders.size)
assertEquals(1, v2.users.size) // only one user upon migration
}
@Throws(com.owncloud.android.operations.UploadException::class, Throwable::class)
@Test
fun addSharee() {
val enc1 = MockUser("enc1", "Nextcloud")
val enc2 = MockUser("enc2", "Nextcloud")
val root = OCFile("/")
storageManager.saveFile(root)
val folder = OCFile("/enc/").apply {
parentId = storageManager.getFileByDecryptedRemotePath("/")?.fileId ?: throw IllegalStateException()
}
var metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert)
metadataFile = encryptionUtilsV2.addShareeToMetadata(metadataFile, enc2.accountName, enc2Cert)
val encryptedMetadataFile = encryptionUtilsV2.encryptFolderMetadataFile(
metadataFile,
client.userId,
folder,
storageManager,
client,
enc1PrivateKey,
user,
targetContext,
arbitraryDataProvider
)
val signature = encryptionUtilsV2.getMessageSignature(enc1Cert, enc1PrivateKey, encryptedMetadataFile)
val decryptedByEnc1 = encryptionUtilsV2.decryptFolderMetadataFile(
encryptedMetadataFile,
enc1.accountName,
enc1PrivateKey,
folder,
storageManager,
client,
metadataFile.metadata.counter,
signature,
user,
targetContext,
arbitraryDataProvider
)
assertEquals(metadataFile.metadata, decryptedByEnc1.metadata)
val decryptedByEnc2 = encryptionUtilsV2.decryptFolderMetadataFile(
encryptedMetadataFile,
enc2.accountName,
enc2PrivateKey,
folder,
storageManager,
client,
metadataFile.metadata.counter,
signature,
user,
targetContext,
arbitraryDataProvider
)
assertEquals(metadataFile.metadata, decryptedByEnc2.metadata)
}
@Test
fun removeSharee() {
val enc1 = MockUser("enc1", "Nextcloud")
val enc2 = MockUser("enc2", "Nextcloud")
var metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert)
metadataFile = encryptionUtilsV2.addShareeToMetadata(metadataFile, enc2.accountName, enc2Cert)
assertEquals(2, metadataFile.users.size)
metadataFile = encryptionUtilsV2.removeShareeFromMetadata(metadataFile, enc2.accountName)
assertEquals(1, metadataFile.users.size)
}
private fun generateDecryptedFolderMetadataFile(user: User, cert: String): DecryptedFolderMetadataFile {
val metadata = DecryptedMetadata(
mutableListOf("hash1", "hash of key 2"),
false,
1,
mutableMapOf(
Pair(EncryptionUtils.generateUid(), "Folder 1"),
Pair(EncryptionUtils.generateUid(), "Folder 2"),
Pair(EncryptionUtils.generateUid(), "Folder 3")
),
mutableMapOf(
Pair(
EncryptionUtils.generateUid(),
DecryptedFile(
"file 1.png",
"image/png",
"initializationVector",
"authenticationTag",
"key 1"
)
),
Pair(
EncryptionUtils.generateUid(),
DecryptedFile(
"file 2.png",
"image/png",
"initializationVector 2",
"authenticationTag 2",
"key 2"
)
)
),
EncryptionUtils.generateKey()
)
val users = mutableListOf(
DecryptedUser(user.accountName, cert)
)
metadata.keyChecksums.add(encryptionUtilsV2.hashMetadataKey(metadata.metadataKey))
return DecryptedFolderMetadataFile(metadata, users, mutableMapOf())
}
@Test
fun testGZip() {
val string = """
This is a test.
This is a test.
This is a test.
This is a test.
This is a test.
This is a test.
This is a test.
This is a test.
This is a test.
This is a test.
This is a test.
This is a test.
This is a test.
It contains linewraps and special characters:
$$|²³¥!
""".trimIndent()
val gzipped = encryptionUtilsV2.gZipCompress(string)
val result = encryptionUtilsV2.gZipDecompress(gzipped)
assertEquals(string, result)
}
@Test
fun gunzip() {
val string = "H4sICNVkD2QAAwArycgsVgCiRIWS1OISPQDD9wZODwAAAA=="
val decoded = EncryptionUtils.decodeStringToBase64Bytes(string)
val gunzip = encryptionUtilsV2.gZipDecompress(decoded)
assertEquals("this is a test.\n", gunzip)
}
// @Test
// fun validate() {
// // ALEX
// val metadata1 = """{
// "metadata": {
// "authenticationTag": "zMozev5R09UopLrq7Je1lw==",
// "ciphertext": "j0OBtUrEt4IveGiexjmGK7eKEaWrY70ZkteA5KxHDaZT/t2wwGy9j2FPQGpqXnW6OO3iAYPNgwFikI1smnfNvqdxzVDvhavl/IXa9Kg2niWyqK3D9zpz0YD6mDvl0XsOgTNVyGXNVREdWgzGEERCQoyHI1xowt/swe3KCXw+lf+XPF/t1PfHv0DiDVk70AeWGpPPPu6yggAIxB4Az6PEZhaQWweTC0an48l2FHj2MtB2PiMHtW2v7RMuE8Al3PtE4gOA8CMFrB+Npy6rKcFCXOgTZm5bp7q+J1qkhBDbiBYtvdsYujJ52Xa5SifTpEhGeWWLFnLLgPAQ8o6bXcWOyCoYfLfp4Jpft/Y7H8qzHbPewNSyD6maEv+xljjfU7hxibbszz5A4JjMdQy2BDGoTmJx7Mas+g6l6ZuHLVbdmgQOvD3waJBy6rOg0euux0Cn4bB4bIFEF2KvbhdGbY1Uiq9DYa7kEmSEnlcAYaHyroTkDg4ew7ER0vIBBMzKM3r+UdPVKKS66uyXtZc=",
// "nonce": "W+lxQJeGq7XAJiGfcDohkg=="
// },
// "users": [{
// "certificate": "-----BEGIN CERTIFICATE-----\nMIIDkDCCAnigAwIBAgIBADANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQGEwJERTEb\nMBkGA1UECAwSQmFkZW4tV3VlcnR0ZW1iZXJnMRIwEAYDVQQHDAlTdHV0dGdhcnQx\nEjAQBgNVBAoMCU5leHRjbG91ZDENMAsGA1UEAwwEam9objAeFw0yMzA3MTQwNzM0\nNTZaFw00MzA3MDkwNzM0NTZaMGExCzAJBgNVBAYTAkRFMRswGQYDVQQIDBJCYWRl\nbi1XdWVydHRlbWJlcmcxEjAQBgNVBAcMCVN0dXR0Z2FydDESMBAGA1UECgwJTmV4\ndGNsb3VkMQ0wCwYDVQQDDARqb2huMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEA7j3Er5YahJT0LAnSRLhpqbRo+E1AVnt98rvp3DmEfBHNzNB+DS9IBDkS\nSXM/YtfAci6Tcw8ujVBjrZX/WEmrf8ynQHxYmSaJSnP8uAT306/MceZpdpruEc9/\nS10a7vp54Zbld4NYdmfS71oVFVKgM7c/Vthx+rgu48fuxzbWAvVYLFcx47hz0DJT\nnjz2Za/R68uXpxfz7J9uEXYiqsAs/FobDsLZluT3RyywVRwKBed1EZxUeLIJiyxp\nUthhGfIb8b3Vf9jZoUVi3m5gmc4spJQHvYAkfZYHzd9ras8jBu1abQRxcu2CYnVo\n6Y0mTxhKhQS/n5gjv3ExiQF3wp/XYwIDAQABo1MwUTAdBgNVHQ4EFgQUmTeILVuB\ntv70fTGkXWGAueDp5kAwHwYDVR0jBBgwFoAUmTeILVuBtv70fTGkXWGAueDp5kAw\nDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAyVtq9XAvW7nxSW/8\nhp30z6xbzGiuviXhy/Jo91VEa8IRsWCCn3OmDFiVduTEowx76tf8clJP0gk7Pozi\n6dg/7Fin+FqQGXfCk8bLAh9gXKAikQ2GK8yRN3slRFwYC2mm23HrLdKXZHUqJcpB\nMz2zsSrOGPj1YsYOl/U8FU6KA7Yj7U3q7kDMYTAgzUPZAH+d1DISGWpZsMa0RYid\nvigCCLByiccmS/Co4Sb1esF58H+YtV5+nFBRwx881U2g2TgDKF1lPMK/y3d8B8mh\nUtW+lFxRpvyNUDpsMjOErOrtNFEYbgoUJLtqwBMmyGR+nmmh6xna331QWcRAmw0P\nnDO4ew==\n-----END CERTIFICATE-----\n",
// "encryptedMetadataKey": "HVT49bYmwXbGs/dJ2avgU9unrKnPf03MYUI5ZysSR1Bz5pqz64gzH2GBAuUJ+Q4VmHtEfcMaWW7VXgzfCQv5xLBrk+RSgcLOKnlIya8jaDlfttWxbe8jJK+/0+QVPOc6ycA/t5HNCPg09hzj+gnb2L89UHxL5accZD0iEzb5cQbGrc/N6GthjgGrgFKtFf0HhDVplUr+DL9aTyKuKLBPjrjuZbv8M6ZfXO93mOMwSZH3c3rwDUHb/KEaTR/Og4pWQmrqr1VxGLqeV/+GKWhzMYThrOZAUz+5gsbckU2M5V9i+ph0yBI5BjOZVhNuDwW8yP8WtyRJwQc+UBRei/RGBQ==",
// "userId": "john"
// }],
// "version": "2"
// }
//
// """
//
// val signature1 =
// "ewogICAgIm1ldGFkYXRhIjogewogICAgICAgICJhdXRoZW50aWNhdGlvblRhZyI6ICJ6TW96ZXY1UjA5VW9wTHJxN0plMWx3PT0iLAogICAgICAgICJjaXBoZXJ0ZXh0IjogImowT0J0VXJFdDRJdmVHaWV4am1HSzdlS0VhV3JZNzBaa3RlQTVLeEhEYVpUL3Qyd3dHeTlqMkZQUUdwcVhuVzZPTzNpQVlQTmd3RmlrSTFzbW5mTnZxZHh6VkR2aGF2bC9JWGE5S2cybmlXeXFLM0Q5enB6MFlENm1EdmwwWHNPZ1ROVnlHWE5WUkVkV2d6R0VFUkNRb3lISTF4b3d0L3N3ZTNLQ1h3K2xmK1hQRi90MVBmSHYwRGlEVms3MEFlV0dwUFBQdTZ5Z2dBSXhCNEF6NlBFWmhhUVd3ZVRDMGFuNDhsMkZIajJNdEIyUGlNSHRXMnY3Uk11RThBbDNQdEU0Z09BOENNRnJCK05weTZyS2NGQ1hPZ1RabTVicDdxK0oxcWtoQkRiaUJZdHZkc1l1ako1MlhhNVNpZlRwRWhHZVdXTEZuTExnUEFROG82YlhjV095Q29ZZkxmcDRKcGZ0L1k3SDhxekhiUGV3TlN5RDZtYUV2K3hsampmVTdoeGliYnN6ejVBNEpqTWRReTJCREdvVG1KeDdNYXMrZzZsNlp1SExWYmRtZ1FPdkQzd2FKQnk2ck9nMGV1dXgwQ240YkI0YklGRUYyS3ZiaGRHYlkxVWlxOURZYTdrRW1TRW5sY0FZYUh5cm9Ua0RnNGV3N0VSMHZJQkJNektNM3IrVWRQVktLUzY2dXlYdFpjPSIsCiAgICAgICAgIm5vbmNlIjogIlcrbHhRSmVHcTdYQUppR2ZjRG9oa2c9PSIKICAgIH0sCiAgICAidXNlcnMiOiB7CiAgICAgICAgImNlcnRpZmljYXRlIjogIi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLVxuTUlJRGtEQ0NBbmlnQXdJQkFnSUJBREFOQmdrcWhraUc5dzBCQVFVRkFEQmhNUXN3Q1FZRFZRUUdFd0pFUlRFYlxuTUJrR0ExVUVDQXdTUW1Ga1pXNHRWM1ZsY25SMFpXMWlaWEpuTVJJd0VBWURWUVFIREFsVGRIVjBkR2RoY25ReFxuRWpBUUJnTlZCQW9NQ1U1bGVIUmpiRzkxWkRFTk1Bc0dBMVVFQXd3RWFtOW9iakFlRncweU16QTNNVFF3TnpNMFxuTlRaYUZ3MDBNekEzTURrd056TTBOVFphTUdFeEN6QUpCZ05WQkFZVEFrUkZNUnN3R1FZRFZRUUlEQkpDWVdSbFxuYmkxWGRXVnlkSFJsYldKbGNtY3hFakFRQmdOVkJBY01DVk4wZFhSMFoyRnlkREVTTUJBR0ExVUVDZ3dKVG1WNFxuZEdOc2IzVmtNUTB3Q3dZRFZRUUREQVJxYjJodU1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQlxuQ2dLQ0FRRUE3ajNFcjVZYWhKVDBMQW5TUkxocHFiUm8rRTFBVm50OThydnAzRG1FZkJITnpOQitEUzlJQkRrU1xuU1hNL1l0ZkFjaTZUY3c4dWpWQmpyWlgvV0VtcmY4eW5RSHhZbVNhSlNuUDh1QVQzMDYvTWNlWnBkcHJ1RWM5L1xuUzEwYTd2cDU0WmJsZDROWWRtZlM3MW9WRlZLZ003Yy9WdGh4K3JndTQ4ZnV4emJXQXZWWUxGY3g0N2h6MERKVFxubmp6MlphL1I2OHVYcHhmejdKOXVFWFlpcXNBcy9Gb2JEc0xabHVUM1J5eXdWUndLQmVkMUVaeFVlTElKaXl4cFxuVXRoaEdmSWI4YjNWZjlqWm9VVmkzbTVnbWM0c3BKUUh2WUFrZlpZSHpkOXJhczhqQnUxYWJRUnhjdTJDWW5Wb1xuNlkwbVR4aEtoUVMvbjVnanYzRXhpUUYzd3AvWFl3SURBUUFCbzFNd1VUQWRCZ05WSFE0RUZnUVVtVGVJTFZ1QlxudHY3MGZUR2tYV0dBdWVEcDVrQXdId1lEVlIwakJCZ3dGb0FVbVRlSUxWdUJ0djcwZlRHa1hXR0F1ZURwNWtBd1xuRHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCQVFVRkFBT0NBUUVBeVZ0cTlYQXZXN254U1cvOFxuaHAzMHo2eGJ6R2l1dmlYaHkvSm85MVZFYThJUnNXQ0NuM09tREZpVmR1VEVvd3g3NnRmOGNsSlAwZ2s3UG96aVxuNmRnLzdGaW4rRnFRR1hmQ2s4YkxBaDlnWEtBaWtRMkdLOHlSTjNzbFJGd1lDMm1tMjNIckxkS1haSFVxSmNwQlxuTXoyenNTck9HUGoxWXNZT2wvVThGVTZLQTdZajdVM3E3a0RNWVRBZ3pVUFpBSCtkMURJU0dXcFpzTWEwUllpZFxudmlnQ0NMQnlpY2NtUy9DbzRTYjFlc0Y1OEgrWXRWNStuRkJSd3g4ODFVMmcyVGdES0YxbFBNSy95M2Q4QjhtaFxuVXRXK2xGeFJwdnlOVURwc01qT0VyT3J0TkZFWWJnb1VKTHRxd0JNbXlHUitubW1oNnhuYTMzMVFXY1JBbXcwUFxubkRPNGV3PT1cbi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS1cbiIsCiAgICAgICAgImVuY3J5cHRlZE1ldGFkYXRhS2V5IjogIkhWVDQ5Ylltd1hiR3MvZEoyYXZnVTl1bnJLblBmMDNNWVVJNVp5c1NSMUJ6NXBxejY0Z3pIMkdCQXVVSitRNFZtSHRFZmNNYVdXN1ZYZ3pmQ1F2NXhMQnJrK1JTZ2NMT0tubEl5YThqYURsZnR0V3hiZThqSksrLzArUVZQT2M2eWNBL3Q1SE5DUGcwOWh6aitnbmIyTDg5VUh4TDVhY2NaRDBpRXpiNWNRYkdyYy9ONkd0aGpnR3JnRkt0RmYwSGhEVnBsVXIrREw5YVR5S3VLTEJQanJqdVpidjhNNlpmWE85M21PTXdTWkgzYzNyd0RVSGIvS0VhVFIvT2c0cFdRbXJxcjFWeEdMcWVWLytHS1doek1ZVGhyT1pBVXorNWdzYmNrVTJNNVY5aStwaDB5Qkk1QmpPWlZoTnVEd1c4eVA4V3R5Ukp3UWMrVUJSZWkvUkdCUT09IiwKICAgICAgICAidXNlcklkIjogImpvaG4iCiAgICB9LAogICAgInZlcnNpb24iOiAiMiIKfQo="
//
// // TOBI
// val metadata =
// """{"metadata":{"authenticationTag":"qDcJnAAGtGDlHWiQMBfXgw\u003d\u003d","ciphertext":"3zUhwIgJWMB7DvrbsDaMvh8MbJdoTxL0OMPCCdYSfBt7gB+V/hwqelL1IOaLto3avhHGSebnrotF06iEP/jZwWg9hApIPTHc8B4XTOY0/kezqYyVqTyquTUZpDpqgVAheQskZZ8I4Ir0seajUkt4KtVRfzO6v8CePRrEg6uKwdYsqDcJnAAGtGDlHWiQMBfXgw\u003d\u003d|4hbOyn1ykQL+9D6SnPY3cQ\u003d\u003d","nonce":"4hbOyn1ykQL+9D6SnPY3cQ\u003d\u003d"},"users":[{"certificate":"-----BEGIN CERTIFICATE-----\nMIIC6DCCAdCgAwIBAgIBADANBgkqhkiG9w0BAQUFADANMQswCQYDVQQDDAJ0MTAe\nFw0yMzA3MjUwNzU3MTJaFw00MzA3MjAwNzU3MTJaMA0xCzAJBgNVBAMMAnQxMIIB\nIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtafHmDBcBqIu4HmMxMDW3j0S\ny+S0YaKwHnBRt85KSwcEov0B5FOLuLknoBGx4Dn3u93ilThXXxacPMHeXL7WPuAs\n21/G7vsqwvrRRnCduf+FUO/AZeDCNErzpsQ8LmTa4PUloLPUcImpSjrHwhMs9Ekv\nEbLRjbeSmSp9XvM+1fV/3jkT5jkOSnCFx5TGwGN5uHqwUir4UWXasvg253NK2XmW\nipKCDCR9TmH1baP3pNdoiChdmErT1c6E4DbBXpTw8XgP5ZbYH+qg1UQ/hC8nRJ3D\nyCcHL+dg/GYraBMhDn4w2Vvq77xNNoNWQ9cT5Ay6cJbQLBQoJQirygQFrobYRQID\nAQABo1MwUTAdBgNVHQ4EFgQUE9zCeA9/QMAtVgLxD23X6ZcodhMwHwYDVR0jBBgw\nFoAUE9zCeA9/QMAtVgLxD23X6ZcodhMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG\n9w0BAQUFAAOCAQEAZdy/YjJlvnz3FQwxp6oVtMJccpdxveEPfLzgaverhtd/vP8O\nAvDzOLgQJHmrDS91SG503eU4cYGyuNKwd77OyTnqMg+GUEmJhGfPpSVrEIdh65jv\nq61T4oqBdehevVmBq54rGiwL0DGv1DlXQlwiJZP4qni2KnOEFcnvL3gVtRnQjXQ+\nkHvlMshkK6w021EMV5NfjG2zg67wC65rLaej5f6Ssp2S7g2VtmE4aXq1bjAuEbqk\n4TiyZHLDdsJuqzyGyyOpMV7i9ucXDoaZt9cGS9hT2vRxTrSH63vKR8Xeig9+stLw\nt9ONcUqCKP7hd8rajtxM4JIIRExwD8OkgARWGg\u003d\u003d\n-----END CERTIFICATE-----\n","encryptedMetadataKey":"s4kDkkLpk1mSmXedP7huiCNC4DYmDAmA2VYGem5M8jIGPC6miVQoo4WXZrEBhdsLw7Msf5iT3A3fTaHhwsI8Jf4McsFyM9/FXT1mCEaGOEpNjbKOlJY1uPUFNOhLqUfFiBos6oBT53hWwoXWjytYvLBbXuXY5YLOysjgBh6URrgFUZAJAmcOJ6OFKgfIIthoqkQc7CQUY97VsRzAXzeYTANBc2yW1pSN51HqftvMzvewFRsJQLcu7a9NjpTdG9LiLhn5eLXOLymXEE/aaPHKXeprlXLzrdWU1xwZRJqV+to2FEiH6CQNsO4+9h5m0VjXekiNeAFrsXB5cJgUipGuzQ\u003d\u003d","userId":"t1"}],"version":"2.0"}"""
//
// val base = EncryptionUtils.encodeStringToBase64String(metadata)
//
// val signature =
// "MIAGCSqGSIb3DQEHAqCAMIACAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBoIAwggLoMIIB0KADAgECAgEAMA0GCSqGSIb3DQEBBQUAMA0xCzAJBgNVBAMMAnQxMB4XDTIzMDcyNTA3NTcxMloXDTQzMDcyMDA3NTcxMlowDTELMAkGA1UEAwwCdDEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1p8eYMFwGoi7geYzEwNbePRLL5LRhorAecFG3zkpLBwSi/QHkU4u4uSegEbHgOfe73eKVOFdfFpw8wd5cvtY+4CzbX8bu+yrC+tFGcJ25/4VQ78Bl4MI0SvOmxDwuZNrg9SWgs9RwialKOsfCEyz0SS8RstGNt5KZKn1e8z7V9X/eORPmOQ5KcIXHlMbAY3m4erBSKvhRZdqy+Dbnc0rZeZaKkoIMJH1OYfVto/ek12iIKF2YStPVzoTgNsFelPDxeA/lltgf6qDVRD+ELydEncPIJwcv52D8ZitoEyEOfjDZW+rvvE02g1ZD1xPkDLpwltAsFCglCKvKBAWuhthFAgMBAAGjUzBRMB0GA1UdDgQWBBQT3MJ4D39AwC1WAvEPbdfplyh2EzAfBgNVHSMEGDAWgBQT3MJ4D39AwC1WAvEPbdfplyh2EzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBl3L9iMmW+fPcVDDGnqhW0wlxyl3G94Q98vOBq96uG13+8/w4C8PM4uBAkeasNL3VIbnTd5ThxgbK40rB3vs7JOeoyD4ZQSYmEZ8+lJWsQh2HrmO+rrVPiioF16F69WYGrnisaLAvQMa/UOVdCXCIlk/iqeLYqc4QVye8veBW1GdCNdD6Qe+UyyGQrrDTbUQxXk1+MbbODrvALrmstp6Pl/pKynZLuDZW2YThperVuMC4RuqThOLJkcsN2wm6rPIbLI6kxXuL25xcOhpm31wZL2FPa9HFOtIfre8pHxd6KD36y0vC3041xSoIo/uF3ytqO3EzgkghETHAPw6SABFYaAAAxggHUMIIB0AIBATASMA0xCzAJBgNVBAMMAnQxAgEAMAsGCWCGSAFlAwQCAaCBljAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMzA3MjgwNzMwMTJaMCsGCSqGSIb3DQEJNDEeMBwwCwYJYIZIAWUDBAIBoQ0GCSqGSIb3DQEBCwUAMC8GCSqGSIb3DQEJBDEiBCAx7RTJg7hbY5Mkzjw3f6qhX7k/J0FdVz2cL3ow0AmyYjANBgkqhkiG9w0BAQsFAASCAQAbUmb9e7eoIcPNzDSmnzbrueBzgT8YszNGEI+1YCq8XdWN4kDztvP1ZNV21VCO6BvcbfUAnXXgcX5BPeLZNsgXPj3c8TbD59GQl3oT/tIchgMsA20RdAtIwvItlZKh+X6sp0OHkRPYSk/mEYKCKPqrKdJicRWex8ItCwpDR91KSOiKJrN/+DKOGG0sVI9gjzbtrHsN8HmVKxOoNV+wwipcLsWsEmuV+wvPCQ9HJidLX9Q17Bgfc+qJg19aB6iKLWPhjgnfpKGbK5VJuQTdDWPUJ2O4G3W/iwxJ0hAJ7tks4zIATmgGzhgTWYx5LVXbKcuL04xhIOjqwedHeCSBZSSaAAAAAAAA"
//
// val metadataFile = EncryptionUtils.deserializeJSON(
// metadata,
// object : TypeToken<EncryptedFolderMetadataFile>() {}
// )
// assertNotNull(metadataFile)
//
// val certJohnString = metadataFile.users[0].certificate
// val certJohn = EncryptionUtils.convertCertFromString(certJohnString)
//
// val t1String = """-----BEGIN CERTIFICATE-----
// MIIC6DCCAdCgAwIBAgIBADANBgkqhkiG9w0BAQUFADANMQswCQYDVQQDDAJ0MTAe
// Fw0yMzA3MjUwNzU3MTJaFw00MzA3MjAwNzU3MTJaMA0xCzAJBgNVBAMMAnQxMIIB
// IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtafHmDBcBqIu4HmMxMDW3j0S
// y+S0YaKwHnBRt85KSwcEov0B5FOLuLknoBGx4Dn3u93ilThXXxacPMHeXL7WPuAs
// 21/G7vsqwvrRRnCduf+FUO/AZeDCNErzpsQ8LmTa4PUloLPUcImpSjrHwhMs9Ekv
// EbLRjbeSmSp9XvM+1fV/3jkT5jkOSnCFx5TGwGN5uHqwUir4UWXasvg253NK2XmW
// ipKCDCR9TmH1baP3pNdoiChdmErT1c6E4DbBXpTw8XgP5ZbYH+qg1UQ/hC8nRJ3D
// yCcHL+dg/GYraBMhDn4w2Vvq77xNNoNWQ9cT5Ay6cJbQLBQoJQirygQFrobYRQID
// AQABo1MwUTAdBgNVHQ4EFgQUE9zCeA9/QMAtVgLxD23X6ZcodhMwHwYDVR0jBBgw
// FoAUE9zCeA9/QMAtVgLxD23X6ZcodhMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG
// 9w0BAQUFAAOCAQEAZdy/YjJlvnz3FQwxp6oVtMJccpdxveEPfLzgaverhtd/vP8O
// AvDzOLgQJHmrDS91SG503eU4cYGyuNKwd77OyTnqMg+GUEmJhGfPpSVrEIdh65jv
// q61T4oqBdehevVmBq54rGiwL0DGv1DlXQlwiJZP4qni2KnOEFcnvL3gVtRnQjXQ+
// kHvlMshkK6w021EMV5NfjG2zg67wC65rLaej5f6Ssp2S7g2VtmE4aXq1bjAuEbqk
// 4TiyZHLDdsJuqzyGyyOpMV7i9ucXDoaZt9cGS9hT2vRxTrSH63vKR8Xeig9+stLw
// t9ONcUqCKP7hd8rajtxM4JIIRExwD8OkgARWGg==
// -----END CERTIFICATE-----"""
//
// val t1cert = EncryptionUtils.convertCertFromString(t1String)
// val t1PrivateKeyKey = EncryptionUtils.PEMtoPrivateKey(encryptionTestUtils.t1PrivateKey)
//
// // val signed = encryptionUtilsV2.getMessageSignature(
// // t1cert,
// // t1PrivateKeyKey,
// // metadataFile
// // )
//
// assertTrue(encryptionUtilsV2.verifySignedMessage(signature1, metadata1, listOf(certJohn, t1cert)))
// }
@Throws(Throwable::class)
@Test
fun testSigning() {
val metadata =
"""{"metadata": {"authenticationTag": "zMozev5R09UopLrq7Je1lw==","ciphertext": "j0OBtUrEt4IveGiexjm
|GK7eKEaWrY70ZkteA5KxHDaZT/t2wwGy9j2FPQGpqXnW6OO3iAYPNgwFikI1smnfNvqdxzVDvhavl/IXa9Kg2niWyqK3D9
|zpz0YD6mDvl0XsOgTNVyGXNVREdWgzGEERCQoyHI1xowt/swe3KCXw+lf+XPF/t1PfHv0DiDVk70AeWGpPPPu6yggAIxB4
|Az6PEZhaQWweTC0an48l2FHj2MtB2PiMHtW2v7RMuE8Al3PtE4gOA8CMFrB+Npy6rKcFCXOgTZm5bp7q+J1qkhBDbiBYtv
|dsYujJ52Xa5SifTpEhGeWWLFnLLgPAQ8o6bXcWOyCoYfLfp4Jpft/Y7H8qzHbPewNSyD6maEv+xljjfU7hxibbszz5A4Jj
|MdQy2BDGoTmJx7Mas+g6l6ZuHLVbdmgQOvD3waJBy6rOg0euux0Cn4bB4bIFEF2KvbhdGbY1Uiq9DYa7kEmSEnlcAYaHyr
|oTkDg4ew7ER0vIBBMzKM3r+UdPVKKS66uyXtZc=","nonce": "W+lxQJeGq7XAJiGfcDohkg=="},"users": [{"cert
|ificate": "-----BEGIN CERTIFICATE-----\nMIIDkDCCAnigAwIBAgIBADANBgkqhkiG9w0BAQUFADBhMQswCQYDVQ
|QGEwJERTEb\nMBkGA1UECAwSQmFkZW4tV3VlcnR0ZW1iZXJnMRIwEAYDVQQHDAlTdHV0dGdhcnQx\nEjAQBgNVBAoMCU5l
|eHRjbG91ZDENMAsGA1UEAwwEam9objAeFw0yMzA3MTQwNzM0\nNTZaFw00MzA3MDkwNzM0NTZaMGExCzAJBgNVBAYTAkRF
|MRswGQYDVQQIDBJCYWRl\nbi1XdWVydHRlbWJlcmcxEjAQBgNVBAcMCVN0dXR0Z2FydDESMBAGA1UECgwJTmV4\ndGNsb3
|VkMQ0wCwYDVQQDDARqb2huMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEA7j3Er5YahJT0LAnSRLhpqbRo+E
|1AVnt98rvp3DmEfBHNzNB+DS9IBDkS\nSXM/YtfAci6Tcw8ujVBjrZX/WEmrf8ynQHxYmSaJSnP8uAT306/MceZpdpruEc
|9/\nS10a7vp54Zbld4NYdmfS71oVFVKgM7c/Vthx+rgu48fuxzbWAvVYLFcx47hz0DJT\nnjz2Za/R68uXpxfz7J9uEXYi
|qsAs/FobDsLZluT3RyywVRwKBed1EZxUeLIJiyxp\nUthhGfIb8b3Vf9jZoUVi3m5gmc4spJQHvYAkfZYHzd9ras8jBu1a
|bQRxcu2CYnVo\n6Y0mTxhKhQS/n5gjv3ExiQF3wp/XYwIDAQABo1MwUTAdBgNVHQ4EFgQUmTeILVuB\ntv70fTGkXWGAue
|Dp5kAwHwYDVR0jBBgwFoAUmTeILVuBtv70fTGkXWGAueDp5kAw\nDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAA
|OCAQEAyVtq9XAvW7nxSW/8\nhp30z6xbzGiuviXhy/Jo91VEa8IRsWCCn3OmDFiVduTEowx76tf8clJP0gk7Pozi\n6dg/
|7Fin+FqQGXfCk8bLAh9gXKAikQ2GK8yRN3slRFwYC2mm23HrLdKXZHUqJcpB\nMz2zsSrOGPj1YsYOl/U8FU6KA7Yj7U3q
|7kDMYTAgzUPZAH+d1DISGWpZsMa0RYid\nvigCCLByiccmS/Co4Sb1esF58H+YtV5+nFBRwx881U2g2TgDKF1lPMK/y3d8
|B8mh\nUtW+lFxRpvyNUDpsMjOErOrtNFEYbgoUJLtqwBMmyGR+nmmh6xna331QWcRAmw0P\nnDO4ew==\n-----END CER
|TIFICATE-----\n","encryptedMetadataKey": "HVT49bYmwXbGs/dJ2avgU9unrKnPf03MYUI5ZysSR1Bz5pqz64gz
|H2GBAuUJ+Q4VmHtEfcMaWW7VXgzfCQv5xLBrk+RSgcLOKnlIya8jaDlfttWxbe8jJK+/0+QVPOc6ycA/t5HNCPg09hzj+g
|nb2L89UHxL5accZD0iEzb5cQbGrc/N6GthjgGrgFKtFf0HhDVplUr+DL9aTyKuKLBPjrjuZbv8M6ZfXO93mOMwSZH3c3rw
|DUHb/KEaTR/Og4pWQmrqr1VxGLqeV/+GKWhzMYThrOZAUz+5gsbckU2M5V9i+ph0yBI5BjOZVhNuDwW8yP8WtyRJwQc+UB
|Rei/RGBQ==","userId": "john"}],"version": "2"}
""".trimMargin()
val base64Metadata = EncryptionUtils.encodeStringToBase64String(metadata)
val privateKey = EncryptionUtils.PEMtoPrivateKey(encryptionTestUtils.t1PrivateKey)
val certificateT1 = EncryptionUtils.convertCertFromString(encryptionTestUtils.t1PublicKey)
val certificateEnc2 = EncryptionUtils.convertCertFromString(enc2Cert)
val signed = encryptionUtilsV2.signMessage(
certificateT1,
privateKey,
metadata
)
val base64Ans = encryptionUtilsV2.extractSignedString(signed)
// verify
val certs = listOf(
certificateEnc2,
certificateT1
)
assertTrue(encryptionUtilsV2.verifySignedMessage(signed, certs))
assertTrue(encryptionUtilsV2.verifySignedMessage(base64Ans, base64Metadata, certs))
}
@Throws(Throwable::class)
@Test
fun sign() {
val sut = "randomstring123"
val json = "randomstring123"
val jsonBase64 = EncryptionUtils.encodeStringToBase64String(json)
val privateKey = EncryptionUtils.PEMtoPrivateKey(encryptionTestUtils.t1PrivateKey)
val certificate = EncryptionUtils.convertCertFromString(encryptionTestUtils.t1PublicKey)
val signed = encryptionUtilsV2.signMessage(
certificate,
privateKey,
sut
)
val base64Ans = encryptionUtilsV2.extractSignedString(signed)
// verify
val certs = listOf(
EncryptionUtils.convertCertFromString(enc2Cert),
certificate
)
assertTrue(encryptionUtilsV2.verifySignedMessage(signed, certs))
assertTrue(encryptionUtilsV2.verifySignedMessage(base64Ans, jsonBase64, certs))
}
/**
* DecryptedFolderMetadata -> EncryptedFolderMetadata -> JSON -> encrypt -> decrypt -> JSON ->
* EncryptedFolderMetadata -> DecryptedFolderMetadata
*/
@Test
@Throws(Exception::class, Throwable::class)
fun encryptionMetadataV2() {
val decryptedFolderMetadata1: DecryptedFolderMetadataFile =
EncryptionTestUtils().generateFolderMetadataV2(client.userId, EncryptionTestIT.publicKey)
val root = OCFile("/")
storageManager.saveFile(root)
val folder = OCFile("/enc")
folder.parentId = storageManager.getFileByDecryptedRemotePath("/")?.fileId ?: throw IllegalStateException()
storageManager.saveFile(folder)
decryptedFolderMetadata1.filedrop.clear()
// encrypt
val encryptedFolderMetadata1 = encryptionUtilsV2.encryptFolderMetadataFile(
decryptedFolderMetadata1,
client.userId,
folder,
storageManager,
client,
EncryptionTestIT.publicKey,
user,
targetContext,
arbitraryDataProvider
)
val signature = encryptionUtilsV2.getMessageSignature(enc1Cert, enc1PrivateKey, encryptedFolderMetadata1)
// serialize
val encryptedJson = EncryptionUtils.serializeJSON(encryptedFolderMetadata1)
// de-serialize
val encryptedFolderMetadata2 = EncryptionUtils.deserializeJSON(
encryptedJson,
object : TypeToken<EncryptedFolderMetadataFile?>() {}
)
// decrypt
val decryptedFolderMetadata2 = encryptionUtilsV2.decryptFolderMetadataFile(
encryptedFolderMetadata2!!,
getUserId(user),
EncryptionTestIT.privateKey,
folder,
fileDataStorageManager,
client,
decryptedFolderMetadata1.metadata.counter,
signature,
user,
targetContext,
arbitraryDataProvider
)
// compare
assertTrue(
EncryptionTestIT.compareJsonStrings(
EncryptionUtils.serializeJSON(decryptedFolderMetadata1),
EncryptionUtils.serializeJSON(decryptedFolderMetadata2)
)
)
}
@Throws(Throwable::class)
@Test
fun decryptFiledropV2() {
val sut = EncryptedFiledrop(
"""QE5nJmA8QC3rBJxbpsZu6MvkomwHMKTYf/3dEz9Zq3ITHLK/wNAIqWTbDehBJ7SlTfXakkKR9o0sOkUDI7PD8qJyv5hW7LzifszYGe
|xE0V1daFcCFApKrIEBABHVOq+ZHJd8IzNSz3hdA9bWd2eiaEGyQzgdTPELE6Ie84IwFANJHcaRB5B43aaDdbUXNJ4/oMboOReKTJ
|/vT6ZGhve4DRPEsez0quyDZDNlin5hD6UaUzw=
""".trimMargin(),
"HC87OgVzbR2CXdWp7rKI5A==",
"7PSq7INkM2WKfmEPpRpTPA==",
listOf(
EncryptedFiledropUser(
"android3",
"""cNzk8cNyoTJ49Cj/x2WPlsMAnUWlZsfnKJ3VIRiczASeUYUFhaJpD8HDWE0uhkXSD7i9nzpe6pR7zllE7UE/QniDd+BQiF
|80E5fSO1KVfFkLZRT+2pX5oPnl4CVtMnxb4xG7J1nAUqMhfS8PtQIr0+S7NKDdrUc41aNOB/4kH0D9LSo/bSC38L7ewv
|mISM6ZFi1bfI1505kZV0HqcW12nZwHwe3s6rYkoSPBOPX1oPkvMYTVLkYuU+7DNL4HW7D9dc9X4bsSGLdj4joRi9FURi
|mMv6MOrWOnYlX2zmMKAF3nEjLlhngKG7pUi/qMIlft2AhRM4cJuuIQ29vvTGFFDQ==
""".trimMargin()
)
)
)
val privateKey =
"""MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDPNCnYcPgGQwCzL8sxLTiE0atn5tiW1nfPQc1aY/+aXvkpF4h2vT
|S/hQg2SCNFUlw8aYKksk5IH5FFcPv9QFG/TQDQOnZhi7moVPnVwLkx+cDfWQQs1EOhI/ZPdSo7MdaRLttbZZs/GJfnr1ziYZTxLO
|UUxT541cnSpqGTKmUXhGMoX+jQTmcn1NyBD537NdetOxSdMfvBIobjRQ70/9c1HFGQSrJa+DmPiis6iFkd1LH6WWRbreC6DsRSqK
|ne3sD1ujx39k+VxtBe035c2L9PbTMMW3kBdZxlRkV1tUQhDAys0K+CyvNIFsOjqvQKTnXNfWO+kVnpOkpbTK4imuPbAgMBAAECgf
|9T537U/6TuwJLSj4bfYev8qYaakfVIpyMkL33e4YBQnUzhlCPBVYgpHkDPwznk2XhjQQiVcRAycmUHBmy4aPkcOjuBmd87aTj03k
|niDk+doFDNU8myuwWTw/1fHdElRnLyZxEKrb391HD4SVVQMuxnw8UoC4iNcPnYneY/GTiTtB3dVcRKdabX3Oak2TFiJyJBtTz4RN
|sRYVXM3jyCbxj8uV+XNr+3OuQe5u7cV5gkXOXHqcNczOrxGzSXVGULuw8FiHIlhId7tot3dGdyVvWD9YIwwGA/9/3g8JixqpQHKZ
|6YJAeqltydisGa3CIIEzBAh52GJC7yzMKSC2ZAtW0CgYEA6B/O+EgtZthiXOwivqZmKKGgWGLSOGjVsExSa1iiTTz3EFwcdD54mU
|TKc6hw787NFlfN1m7B7EDQxIldRDI3One1q2dj87taco/qFqKsHuAuC3gmZIp2F4l2P8NpdHHFMzUzsfs+grY/wLHZiJdfOTdulA
|s9go5mDloMC96n0/UCgYEA5IQo7c4ZxwhlssIn89XaOlKGoIct07wsBMu47HZYFqgG2/NUN8zRfSdSvot+6zinAb6Z3iGZ2FBL+C
|MmoEMGwuXSQjWxeD//UU6V5AZqlgis5s9WakKWmkTkVV3bPSwW0DuNcqbMk7BxAXcQ6QGIiBtzeaPuL/3gzA9e9vm8xo8CgYEAqL
|I9S6nA/UZzLg8bLS1nf03/Z1ziZMajzk2ZdJRk1/dfow8eSskAAnvBGo8nDNFhsUQ8vwOdgeKVFtCx7JcGFkLbz+cC+CaIFExNFw
|hASOwp6oH2fQk3y+FGBA8ze8IXTCD1IftzMbHb4WIfsyo3tTB497S3jkOJHhMJQDMgC2UCgYEAzjUgRe98vWkrdFLWAKfSxFxiFg
|vF49JjGnTHy8HDHbbEccizD6NoyvooJb/1aMd3lRBtAtDpZhSXaTQ3D9lMCaWfxZV0LyH5AGLcyaasmfT8KU+iGEM8abuPHCWUyC
|+36nJC4tn3s7I9V2gdP1Xd4Yx7+KFgN7huGVYpiM61dasCgYAQs5mPHRBeU+BHtPRyaLHhYq+jjYeocwyOpfw5wkiH3jsyUWTK9+
|GlAoV75SYvQVIQS0VH1C1/ajz9yV02frAaUXbGtZJbyeAcyy3DjCc7iF0swJ4slP3gGVJipVF4aQ0d9wMoJ7SBaaTR0ohXeUWmTT
|X+VGf+cZQ2IefKVnz9mg==
""".trimMargin()
val decryptedFile = EncryptionUtilsV2().decryptFiledrop(sut, privateKey, arbitraryDataProvider, user)
assertEquals("test.txt", decryptedFile.filename)
}
}

View file

@ -69,7 +69,8 @@ import com.owncloud.android.db.ProviderMeta
AutoMigration(from = 72, to = 73),
AutoMigration(from = 73, to = 74, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 74, to = 75),
AutoMigration(from = 75, to = 76)
AutoMigration(from = 75, to = 76),
AutoMigration(from = 76, to = 77)
],
exportSchema = true
)

View file

@ -98,6 +98,8 @@ data class CapabilityEntity(
val endToEndEncryption: Int?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION_KEYS_EXIST)
val endToEndEncryptionKeysExist: Int?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION_API_VERSION)
val endToEndEncryptionApiVersion: String?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_ACTIVITY)
val activity: Int?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_DEFAULT)

View file

@ -128,5 +128,7 @@ data class FileEntity(
@ColumnInfo(name = ProviderTableMeta.FILE_TAGS)
val tags: String?,
@ColumnInfo(name = ProviderTableMeta.FILE_METADATA_GPS)
val metadataGPS: String?
val metadataGPS: String?,
@ColumnInfo(name = ProviderTableMeta.FILE_E2E_COUNTER)
val e2eCounter: Long?
)

View file

@ -54,6 +54,7 @@ import com.nextcloud.client.notifications.AppNotificationManager;
import com.nextcloud.client.notifications.AppNotificationManagerImpl;
import com.nextcloud.client.preferences.AppPreferences;
import com.nextcloud.client.utils.Throttler;
import com.owncloud.android.providers.UsersAndGroupsSearchConfig;
import com.owncloud.android.authentication.PassCodeManager;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
@ -261,4 +262,11 @@ class AppModule {
PassCodeManager passCodeManager(AppPreferences preferences, Clock clock) {
return new PassCodeManager(preferences, clock);
}
@Provides
@Singleton
UsersAndGroupsSearchConfig userAndGroupSearchConfig() {
return new UsersAndGroupsSearchConfig();
}
}

View file

@ -32,6 +32,7 @@ interface ArbitraryDataProvider {
fun incrementValue(accountName: String, key: String)
fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: Boolean)
fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: String)
fun storeOrUpdateKeyValue(user: User, key: String, newValue: String)
fun getLongValue(accountName: String, key: String): Long
fun getLongValue(user: User, key: String): Long
@ -45,6 +46,7 @@ interface ArbitraryDataProvider {
const val DIRECT_EDITING = "DIRECT_EDITING"
const val DIRECT_EDITING_ETAG = "DIRECT_EDITING_ETAG"
const val PREDEFINED_STATUS = "PREDEFINED_STATUS"
const val PUBLIC_KEY = "PUBLIC_KEY_"
const val E2E_ERRORS = "E2E_ERRORS"
const val E2E_ERRORS_TIMESTAMP = "E2E_ERRORS_TIMESTAMP"
}

View file

@ -91,6 +91,13 @@ public class ArbitraryDataProviderImpl implements ArbitraryDataProvider {
}
}
@Override
public void storeOrUpdateKeyValue(@NonNull User user,
@NonNull String key,
@NonNull String newValue) {
storeOrUpdateKeyValue(user.getAccountName(), key, newValue);
}
@Override
public long getLongValue(@NonNull String accountName, @NonNull String key) {
String value = getValue(accountName, key);

View file

@ -29,18 +29,18 @@ import androidx.annotation.VisibleForTesting;
/**
* Decrypted class representation of metadata json of folder metadata.
*/
public class DecryptedFolderMetadata {
public class DecryptedFolderMetadataOld {
private Metadata metadata;
private Map<String, DecryptedFile> files;
private Map<String, DecryptedFile> filedrop;
public DecryptedFolderMetadata() {
public DecryptedFolderMetadataOld() {
this.metadata = new Metadata();
this.files = new HashMap<>();
}
public DecryptedFolderMetadata(Metadata metadata, Map<String, DecryptedFile> files) {
public DecryptedFolderMetadataOld(Metadata metadata, Map<String, DecryptedFile> files) {
this.metadata = metadata;
this.files = files;
}

View file

@ -58,6 +58,7 @@ import com.owncloud.android.lib.resources.shares.OCShare;
import com.owncloud.android.lib.resources.shares.ShareType;
import com.owncloud.android.lib.resources.shares.ShareeUser;
import com.owncloud.android.lib.resources.status.CapabilityBooleanType;
import com.owncloud.android.lib.resources.status.E2EVersion;
import com.owncloud.android.lib.resources.status.OCCapability;
import com.owncloud.android.operations.RemoteOperationFailedException;
import com.owncloud.android.utils.FileStorageUtils;
@ -556,6 +557,7 @@ public class FileDataStorageManager {
cv.put(ProviderTableMeta.FILE_METADATA_SIZE, gson.toJson(file.getImageDimension()));
cv.put(ProviderTableMeta.FILE_METADATA_GPS, gson.toJson(file.getGeoLocation()));
cv.put(ProviderTableMeta.FILE_METADATA_LIVE_PHOTO, file.getLinkedFileIdForLivePhoto());
cv.put(ProviderTableMeta.FILE_E2E_COUNTER, file.getE2eCounter());
return cv;
}
@ -988,6 +990,7 @@ public class FileDataStorageManager {
ocFile.setLockToken(fileEntity.getLockToken());
ocFile.setLivePhoto(fileEntity.getMetadataLivePhoto());
ocFile.setHidden(nullToZero(fileEntity.getHidden()) == 1);
ocFile.setE2eCounter(fileEntity.getE2eCounter());
String sharees = fileEntity.getSharees();
// Surprisingly JSON deserialization causes significant overhead.
@ -1974,6 +1977,8 @@ public class FileDataStorageManager {
capability.getEndToEndEncryption().getValue());
contentValues.put(ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION_KEYS_EXIST,
capability.getEndToEndEncryptionKeysExist().getValue());
contentValues.put(ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION_API_VERSION,
capability.getEndToEndEncryptionApiVersion().getValue());
contentValues.put(ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_DEFAULT,
capability.getServerBackgroundDefault().getValue());
contentValues.put(ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_PLAIN,
@ -2127,6 +2132,16 @@ public class FileDataStorageManager {
getBoolean(cursor,
ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION_KEYS_EXIST)
);
String e2eVersionString = getString(cursor, ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION_API_VERSION);
E2EVersion e2EVersion;
if (e2eVersionString == null) {
e2EVersion = E2EVersion.UNKNOWN;
} else {
e2EVersion = E2EVersion.fromValue(e2eVersionString);
}
capability.setEndToEndEncryptionApiVersion(e2EVersion);
capability.setServerBackgroundDefault(
getBoolean(cursor, ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_DEFAULT));
capability.setServerBackgroundPlain(getBoolean(cursor,

View file

@ -121,6 +121,7 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
private String lockToken;
@Nullable
private ImageDimension imageDimension;
private long e2eCounter = -1;
@Nullable
private GeoLocation geolocation;
private List<String> tags = new ArrayList<>();
@ -1056,4 +1057,15 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
this.tags = tags;
}
public long getE2eCounter() {
return e2eCounter;
}
public void setE2eCounter(@Nullable Long e2eCounter) {
if (e2eCounter == null) {
this.e2eCounter = -1;
} else {
this.e2eCounter = e2eCounter;
}
}
}

View file

@ -0,0 +1,62 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v1.decrypted;
public class Data {
private String filename;
private String mimetype;
private String key;
private double version;
public String getKey() {
return this.key;
}
public String getFilename() {
return this.filename;
}
public String getMimetype() {
return this.mimetype;
}
public double getVersion() {
return this.version;
}
public void setKey(String key) {
this.key = key;
}
public void setFilename(String filename) {
this.filename = filename;
}
public void setMimetype(String mimetype) {
this.mimetype = mimetype;
}
public void setVersion(double version) {
this.version = version;
}
}

View file

@ -0,0 +1,62 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v1.decrypted;
public class DecryptedFile {
private Data encrypted;
private String initializationVector;
private String authenticationTag;
private int metadataKey;
public Data getEncrypted() {
return this.encrypted;
}
public String getInitializationVector() {
return this.initializationVector;
}
public String getAuthenticationTag() {
return this.authenticationTag;
}
public int getMetadataKey() {
return this.metadataKey;
}
public void setEncrypted(Data encrypted) {
this.encrypted = encrypted;
}
public void setInitializationVector(String initializationVector) {
this.initializationVector = initializationVector;
}
public void setAuthenticationTag(String authenticationTag) {
this.authenticationTag = authenticationTag;
}
public void setMetadataKey(int metadataKey) {
this.metadataKey = metadataKey;
}
}

View file

@ -0,0 +1,73 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v1.decrypted;
import java.util.HashMap;
import java.util.Map;
import androidx.annotation.VisibleForTesting;
/**
* Decrypted class representation of metadata json of folder metadata.
*/
public class DecryptedFolderMetadataFileV1 {
private DecryptedMetadata metadata;
private Map<String, DecryptedFile> files;
private Map<String, DecryptedFile> filedrop;
public DecryptedFolderMetadataFileV1() {
this.metadata = new DecryptedMetadata();
this.files = new HashMap<>();
}
public DecryptedFolderMetadataFileV1(DecryptedMetadata metadata, Map<String, DecryptedFile> files) {
this.metadata = metadata;
this.files = files;
}
public DecryptedMetadata getMetadata() {
return this.metadata;
}
public Map<String, DecryptedFile> getFiles() {
return this.files;
}
public void setMetadata(DecryptedMetadata metadata) {
this.metadata = metadata;
}
public void setFiles(Map<String, DecryptedFile> files) {
this.files = files;
}
@VisibleForTesting
public void setFiledrop(Map<String, DecryptedFile> filedrop) {
this.filedrop = filedrop;
}
public Map<String, DecryptedFile> getFiledrop() {
return filedrop;
}
}

View file

@ -0,0 +1,74 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v1.decrypted;
import java.util.Map;
public class DecryptedMetadata {
transient
private Map<Integer, String> metadataKeys; // outdated with v1.1
private String metadataKey;
private String checksum;
private double version;
@Override
public String toString() {
return String.valueOf(version);
}
public Map<Integer, String> getMetadataKeys() {
return this.metadataKeys;
}
public String getMetadataKey() {
if (metadataKey == null) {
// fallback to old keys array
return metadataKeys.get(0);
}
return metadataKey;
}
public double getVersion() {
return this.version;
}
public void setMetadataKeys(Map<Integer, String> metadataKeys) {
this.metadataKeys = metadataKeys;
}
public void setMetadataKey(String metadataKey) {
this.metadataKey = metadataKey;
}
public void setVersion(double version) {
this.version = version;
}
public String getChecksum() {
return checksum;
}
public void setChecksum(String checksum) {
this.checksum = checksum;
}
}

View file

@ -0,0 +1,37 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v1.decrypted;
import java.util.Map;
public class Encrypted {
private Map<Integer, String> metadataKeys;
public Map<Integer, String> getMetadataKeys() {
return this.metadataKeys;
}
public void setMetadataKeys(Map<Integer, String> metadataKeys) {
this.metadataKeys = metadataKeys;
}
}

View file

@ -0,0 +1,46 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v1.decrypted;
import java.util.Map;
public class Sharing {
private Map<String, String> recipient;
private String signature;
public Map<String, String> getRecipient() {
return this.recipient;
}
public String getSignature() {
return this.signature;
}
public void setRecipient(Map<String, String> recipient) {
this.recipient = recipient;
}
public void setSignature(String signature) {
this.signature = signature;
}
}

View file

@ -0,0 +1,24 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v1.encrypted
class EncryptedFile(var encryptedBytes: ByteArray, var authenticationTag: String)

View file

@ -1,14 +1,15 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2017 Tobias Kaminsky
* Copyright (C) 2017 Nextcloud GmbH.
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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.
* (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
@ -16,23 +17,26 @@
* 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 <http://www.gnu.org/licenses/>.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel;
package com.owncloud.android.datamodel.e2e.v1.encrypted;
import com.owncloud.android.datamodel.EncryptedFiledrop;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedMetadata;
import java.util.Map;
/**
* Encrypted class representation of metadata json of folder metadata
*/
public class EncryptedFolderMetadata {
private DecryptedFolderMetadata.Metadata metadata;
public class EncryptedFolderMetadataFileV1 {
private DecryptedMetadata metadata;
private Map<String, EncryptedFile> files;
private Map<String, EncryptedFiledrop> filedrop;
public EncryptedFolderMetadata(DecryptedFolderMetadata.Metadata metadata,
public EncryptedFolderMetadataFileV1(DecryptedMetadata metadata,
Map<String, EncryptedFile> files,
Map<String, EncryptedFiledrop> filesdrop) {
this.metadata = metadata;
@ -40,7 +44,7 @@ public class EncryptedFolderMetadata {
this.filedrop = filesdrop;
}
public DecryptedFolderMetadata.Metadata getMetadata() {
public DecryptedMetadata getMetadata() {
return this.metadata;
}
@ -52,7 +56,7 @@ public class EncryptedFolderMetadata {
return filedrop;
}
public void setMetadata(DecryptedFolderMetadata.Metadata metadata) {
public void setMetadata(DecryptedMetadata metadata) {
this.metadata = metadata;
}

View file

@ -0,0 +1,30 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v2.decrypted
data class DecryptedFile(
var filename: String,
val mimetype: String,
val nonce: String,
val authenticationTag: String,
val key: String
)

View file

@ -0,0 +1,33 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v2.decrypted
/**
* Decrypted class representation of metadata json of folder metadata.
*/
data class DecryptedFolderMetadataFile(
val metadata: DecryptedMetadata,
var users: MutableList<DecryptedUser> = mutableListOf(),
@Transient
val filedrop: MutableMap<String, DecryptedFile> = HashMap(),
val version: String = "2.0"
)

View file

@ -0,0 +1,59 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v2.decrypted
import com.owncloud.android.utils.EncryptionUtils
data class DecryptedMetadata(
val keyChecksums: MutableList<String> = mutableListOf(),
val deleted: Boolean = false,
var counter: Long = 0,
val folders: MutableMap<String, String> = mutableMapOf(),
val files: MutableMap<String, DecryptedFile> = mutableMapOf(),
@Transient
var metadataKey: ByteArray = EncryptionUtils.generateKey()
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as DecryptedMetadata
if (keyChecksums != other.keyChecksums) return false
if (deleted != other.deleted) return false
if (counter != other.counter) return false
if (folders != other.folders) return false
if (files != other.files) return false
return true
}
override fun hashCode(): Int {
var result = keyChecksums.hashCode()
result = 31 * result + deleted.hashCode()
result = 31 * result + counter.hashCode()
result = 31 * result + folders.hashCode()
result = 31 * result + files.hashCode()
return result
}
}

View file

@ -0,0 +1,28 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v2.decrypted
data class DecryptedUser(
val userId: String,
val certificate: String
)

View file

@ -0,0 +1,29 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v2.encrypted
data class EncryptedFiledrop(
val ciphertext: String,
val nonce: String,
val authenticationTag: String,
val users: List<EncryptedFiledropUser>
)

View file

@ -0,0 +1,28 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v2.encrypted
data class EncryptedFiledropUser(
val userId: String,
val encryptedFiledropKey: String
)

View file

@ -0,0 +1,32 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v2.encrypted
/**
* Decrypted class representation of metadata json of folder metadata.
*/
data class EncryptedFolderMetadataFile(
val metadata: EncryptedMetadata,
val users: List<EncryptedUser>,
val filedrop: MutableMap<String, EncryptedFiledrop>?,
val version: String = "2.0"
)

View file

@ -0,0 +1,29 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v2.encrypted
data class EncryptedMetadata(
val ciphertext: String,
val nonce: String,
val authenticationTag: String
)

View file

@ -0,0 +1,29 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v2.encrypted
data class EncryptedUser(
val userId: String,
val certificate: String,
val encryptedMetadataKey: String
)

View file

@ -0,0 +1,28 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 Nextcloud GmbH
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel.e2e.v2.encrypted
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile
class FiledropData {
private val files: Map<String, DecryptedFile> = mutableMapOf()
}

View file

@ -35,7 +35,7 @@ import java.util.List;
*/
public class ProviderMeta {
public static final String DB_NAME = "filelist";
public static final int DB_VERSION = 76;
public static final int DB_VERSION = 77;
private ProviderMeta() {
// No instance
@ -129,6 +129,7 @@ public class ProviderMeta {
public static final String FILE_LOCK_TIMEOUT = "lock_timeout";
public static final String FILE_LOCK_TOKEN = "lock_token";
public static final String FILE_TAGS = "tags";
public static final String FILE_E2E_COUNTER = "e2e_counter";
public static final List<String> FILE_ALL_COLUMNS = Collections.unmodifiableList(Arrays.asList(
_ID,
@ -178,6 +179,7 @@ public class ProviderMeta {
FILE_LOCK_TOKEN,
FILE_METADATA_SIZE,
FILE_METADATA_LIVE_PHOTO,
FILE_E2E_COUNTER,
FILE_TAGS,
FILE_METADATA_GPS));
public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME + " collate nocase asc";
@ -248,6 +250,7 @@ public class ProviderMeta {
public static final String CAPABILITIES_SERVER_BACKGROUND_PLAIN = "background_plain";
public static final String CAPABILITIES_END_TO_END_ENCRYPTION = "end_to_end_encryption";
public static final String CAPABILITIES_END_TO_END_ENCRYPTION_KEYS_EXIST = "end_to_end_encryption_keys_exist";
public static final String CAPABILITIES_END_TO_END_ENCRYPTION_API_VERSION = "end_to_end_encryption_api_version";
public static final String CAPABILITIES_ACTIVITY = "activity";
public static final String CAPABILITIES_RICHDOCUMENT = "richdocument";
public static final String CAPABILITIES_RICHDOCUMENT_MIMETYPE_LIST = "richdocument_mimetype_list";

View file

@ -186,7 +186,7 @@ public class FileMenuFilter {
private void filterShareFile(List<Integer> toHide, OCCapability capability) {
if (!isSingleSelection() || containsEncryptedFile() ||
if (!isSingleSelection() || containsEncryptedFile() || hasEncryptedParent() ||
(!isShareViaLinkAllowed() && !isShareWithUsersAllowed()) ||
!isShareApiEnabled(capability) || !files.iterator().next().canReshare()) {
toHide.add(R.id.action_send_share_file);
@ -220,7 +220,11 @@ public class FileMenuFilter {
}
private void filterLock(List<Integer> toHide, boolean fileLockingEnabled) {
if (files.isEmpty() || !isSingleSelection() || !fileLockingEnabled) {
if (files.isEmpty() ||
!isSingleSelection() ||
!fileLockingEnabled ||
containsEncryptedFile() ||
containsEncryptedFolder()) {
toHide.add(R.id.action_lock_file);
} else {
OCFile file = files.iterator().next();
@ -340,7 +344,7 @@ public class FileMenuFilter {
private void filterRemove(List<Integer> toHide, boolean synchronizing) {
if (files.isEmpty() || synchronizing || containsLockedFile()
|| containsEncryptedFolder() || containsEncryptedFile()) {
|| containsEncryptedFolder() || isFolderAndContainsEncryptedFile()) {
toHide.add(R.id.action_remove_file);
}
}
@ -485,6 +489,24 @@ public class FileMenuFilter {
return isSingleSelection() && (MimeTypeUtil.isVideo(file) || MimeTypeUtil.isAudio(file));
}
private boolean isFolderAndContainsEncryptedFile() {
for (OCFile file : files) {
if (!file.isFolder()) {
continue;
}
if (file.isFolder()) {
List<OCFile> children = storageManager.getFolderContent(file, false);
for (OCFile child : children) {
if (child.isEncrypted()) {
return true;
}
}
}
}
return false;
}
private boolean containsEncryptedFile() {
for (OCFile file : files) {
if (!file.isFolder() && file.isEncrypted()) {

View file

@ -27,10 +27,13 @@ import android.util.Pair;
import com.nextcloud.client.account.User;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
import com.owncloud.android.datamodel.DecryptedFolderMetadata;
import com.owncloud.android.datamodel.EncryptedFolderMetadata;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.e2e.v1.decrypted.Data;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFolderMetadataFileV1;
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile;
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.operations.OnRemoteOperationListener;
import com.owncloud.android.lib.common.operations.RemoteOperation;
@ -40,8 +43,10 @@ import com.owncloud.android.lib.resources.e2ee.ToggleEncryptionRemoteOperation;
import com.owncloud.android.lib.resources.files.CreateFolderRemoteOperation;
import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation;
import com.owncloud.android.lib.resources.files.model.RemoteFile;
import com.owncloud.android.lib.resources.status.E2EVersion;
import com.owncloud.android.operations.common.SyncOperation;
import com.owncloud.android.utils.EncryptionUtils;
import com.owncloud.android.utils.EncryptionUtilsV2;
import com.owncloud.android.utils.FileStorageUtils;
import com.owncloud.android.utils.MimeType;
@ -54,8 +59,8 @@ import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR;
import static com.owncloud.android.datamodel.OCFile.ROOT_PATH;
/**
* Access to remote operation performing the creation of a new folder in the ownCloud server.
* Save the new folder in Database.
* Access to remote operation performing the creation of a new folder in the ownCloud server. Save the new folder in
* Database.
*/
public class CreateFolderOperation extends SyncOperation implements OnRemoteOperationListener {
@ -100,20 +105,28 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
boolean encryptedAncestor = FileStorageUtils.checkEncryptionStatus(parent, getStorageManager());
if (encryptedAncestor) {
return encryptedCreate(parent, client);
E2EVersion e2EVersion = getStorageManager().getCapability(user).getEndToEndEncryptionApiVersion();
if (e2EVersion == E2EVersion.V1_0 ||
e2EVersion == E2EVersion.V1_1 ||
e2EVersion == E2EVersion.V1_2) {
return encryptedCreateV1(parent, client);
} else if (e2EVersion == E2EVersion.V2_0) {
return encryptedCreateV2(parent, client);
}
return new RemoteOperationResult(new IllegalStateException("E2E not supported"));
} else {
return normalCreate(client);
}
}
private RemoteOperationResult encryptedCreate(OCFile parent, OwnCloudClient client) {
private RemoteOperationResult encryptedCreateV1(OCFile parent, OwnCloudClient client) {
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(context);
String privateKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PRIVATE_KEY);
String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY);
String token = null;
Boolean metadataExists;
DecryptedFolderMetadata metadata;
DecryptedFolderMetadataFileV1 metadata;
String encryptedRemotePath = null;
String filename = new File(remotePath).getName();
@ -123,12 +136,13 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
token = EncryptionUtils.lockFolder(parent, client);
// get metadata
Pair<Boolean, DecryptedFolderMetadata> metadataPair = EncryptionUtils.retrieveMetadata(parent,
Pair<Boolean, DecryptedFolderMetadataFileV1> metadataPair = EncryptionUtils.retrieveMetadataV1(parent,
client,
privateKey,
publicKey,
arbitraryDataProvider,
user);
user
);
metadataExists = metadataPair.first;
metadata = metadataPair.second;
@ -142,7 +156,7 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
String encryptedFileName = createRandomFileName(metadata);
encryptedRemotePath = parent.getRemotePath() + encryptedFileName;
RemoteOperationResult result = new CreateFolderRemoteOperation(encryptedRemotePath,
RemoteOperationResult<String> result = new CreateFolderRemoteOperation(encryptedRemotePath,
true,
token)
.execute(client);
@ -151,11 +165,12 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
// update metadata
metadata.getFiles().put(encryptedFileName, createDecryptedFile(filename));
EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata,
EncryptedFolderMetadataFileV1 encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata,
publicKey,
arbitraryDataProvider,
parent.getLocalId(),
user,
parent.getLocalId());
arbitraryDataProvider
);
String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);
// upload metadata
@ -164,17 +179,19 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
token,
client,
metadataExists,
E2EVersion.V1_2,
"",
arbitraryDataProvider,
user);
// unlock folder
if (token != null) {
RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(parent, client, token);
RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolderV1(parent, client, token);
if (unlockFolderResult.isSuccess()) {
token = null;
} else {
// TODO do better
// TODO E2E: do better
throw new RuntimeException("Could not unlock folder!");
}
}
@ -202,6 +219,146 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
return result;
} catch (Exception e) {
if (!EncryptionUtils.unlockFolderV1(parent, client, token).isSuccess()) {
throw new RuntimeException("Could not clean up after failing folder creation!", e);
}
// remove folder
if (encryptedRemotePath != null) {
RemoteOperationResult removeResult = new RemoveRemoteEncryptedFileOperation(encryptedRemotePath,
user,
context,
filename,
parent,
true
).execute(client);
if (!removeResult.isSuccess()) {
throw new RuntimeException("Could not clean up after failing folder creation!");
}
}
// TODO E2E: do better
return new RemoteOperationResult(e);
} finally {
// unlock folder
if (token != null) {
RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolderV1(parent, client, token);
if (!unlockFolderResult.isSuccess()) {
// TODO E2E: do better
throw new RuntimeException("Could not unlock folder!");
}
}
}
}
private RemoteOperationResult encryptedCreateV2(OCFile parent, OwnCloudClient client) {
String token = null;
Boolean metadataExists;
DecryptedFolderMetadataFile metadata;
String encryptedRemotePath = null;
String filename = new File(remotePath).getName();
try {
// lock folder
token = EncryptionUtils.lockFolder(parent, client);
// get metadata
EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2();
kotlin.Pair<Boolean, DecryptedFolderMetadataFile> metadataPair = encryptionUtilsV2.retrieveMetadata(parent,
client,
user,
context);
metadataExists = metadataPair.getFirst();
metadata = metadataPair.getSecond();
// check if filename already exists
if (isFileExisting(metadata, filename)) {
return new RemoteOperationResult(RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS);
}
// generate new random file name, check if it exists in metadata
String encryptedFileName = createRandomFileName(metadata);
encryptedRemotePath = parent.getRemotePath() + encryptedFileName;
RemoteOperationResult<String> result = new CreateFolderRemoteOperation(encryptedRemotePath,
true,
token)
.execute(client);
String remoteId = result.getResultData();
if (result.isSuccess()) {
DecryptedFolderMetadataFile subFolderMetadata = encryptionUtilsV2.createDecryptedFolderMetadataFile();
// upload metadata
encryptionUtilsV2.serializeAndUploadMetadata(remoteId,
subFolderMetadata,
token,
client,
false,
context,
user,
parent,
getStorageManager());
}
if (result.isSuccess()) {
// update metadata
DecryptedFolderMetadataFile updatedMetadataFile = encryptionUtilsV2.addFolderToMetadata(encryptedFileName,
filename,
metadata,
parent,
getStorageManager());
// upload metadata
encryptionUtilsV2.serializeAndUploadMetadata(parent,
updatedMetadataFile,
token,
client,
metadataExists,
context,
user,
getStorageManager());
// unlock folder
RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(parent, client, token);
if (unlockFolderResult.isSuccess()) {
token = null;
} else {
// TODO E2E: do better
throw new RuntimeException("Could not unlock folder!");
}
RemoteOperationResult remoteFolderOperationResult = new ReadFolderRemoteOperation(encryptedRemotePath)
.execute(client);
createdRemoteFolder = (RemoteFile) remoteFolderOperationResult.getData().get(0);
OCFile newDir = createRemoteFolderOcFile(parent, filename, createdRemoteFolder);
getStorageManager().saveFile(newDir);
RemoteOperationResult encryptionOperationResult = new ToggleEncryptionRemoteOperation(
newDir.getLocalId(),
newDir.getRemotePath(),
true)
.execute(client);
if (!encryptionOperationResult.isSuccess()) {
throw new RuntimeException("Error creating encrypted subfolder!");
}
} else {
// revert to sane state in case of any error
Log_OC.e(TAG, remotePath + " hasn't been created");
}
return result;
} catch (Exception e) {
// TODO remove folder
if (!EncryptionUtils.unlockFolder(parent, client, token).isSuccess()) {
throw new RuntimeException("Could not clean up after failing folder creation!", e);
}
@ -209,17 +366,18 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
// remove folder
if (encryptedRemotePath != null) {
RemoteOperationResult removeResult = new RemoveRemoteEncryptedFileOperation(encryptedRemotePath,
parent.getLocalId(),
user,
context,
filename).execute(client);
filename,
parent,
true).execute(client);
if (!removeResult.isSuccess()) {
throw new RuntimeException("Could not clean up after failing folder creation!");
}
}
// TODO do better
// TODO E2E: do better
return new RemoteOperationResult(e);
} finally {
// unlock folder
@ -227,21 +385,30 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(parent, client, token);
if (!unlockFolderResult.isSuccess()) {
// TODO do better
// TODO E2E: do better
throw new RuntimeException("Could not unlock folder!");
}
}
}
}
private boolean isFileExisting(DecryptedFolderMetadata metadata, String filename) {
for (String key : metadata.getFiles().keySet()) {
DecryptedFolderMetadata.DecryptedFile file = metadata.getFiles().get(key);
if (file != null && filename.equalsIgnoreCase(file.getEncrypted().getFilename())) {
private boolean isFileExisting(DecryptedFolderMetadataFileV1 metadata, String filename) {
for (com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile file : metadata.getFiles().values()) {
if (filename.equalsIgnoreCase(file.getEncrypted().getFilename())) {
return true;
}
}
return false;
}
private boolean isFileExisting(DecryptedFolderMetadataFile metadata, String filename) {
for (DecryptedFile file : metadata.getMetadata().getFiles().values()) {
if (filename.equalsIgnoreCase(file.getFilename())) {
return true;
}
}
return false;
}
@ -261,15 +428,16 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
}
@NonNull
private DecryptedFolderMetadata.DecryptedFile createDecryptedFile(String filename) {
private com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile createDecryptedFile(String filename) {
// Key, always generate new one
byte[] key = EncryptionUtils.generateKey();
// IV, always generate new one
byte[] iv = EncryptionUtils.randomBytes(EncryptionUtils.ivLength);
DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile();
DecryptedFolderMetadata.Data data = new DecryptedFolderMetadata.Data();
com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile decryptedFile =
new com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile();
Data data = new Data();
data.setFilename(filename);
data.setMimetype(MimeType.WEBDAV_FOLDER);
data.setKey(EncryptionUtils.encodeBytesToBase64String(key));
@ -281,7 +449,32 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
}
@NonNull
private String createRandomFileName(DecryptedFolderMetadata metadata) {
private DecryptedFile createDecryptedFolder(String filename) {
// Key, always generate new one
byte[] key = EncryptionUtils.generateKey();
// IV, always generate new one
byte[] iv = EncryptionUtils.randomBytes(EncryptionUtils.ivLength);
return new DecryptedFile(filename,
MimeType.WEBDAV_FOLDER,
EncryptionUtils.encodeBytesToBase64String(iv),
"",
EncryptionUtils.encodeBytesToBase64String(key));
}
@NonNull
private String createRandomFileName(DecryptedFolderMetadataFile metadata) {
String encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
while (metadata.getMetadata().getFiles().get(encryptedFileName) != null) {
encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
}
return encryptedFileName;
}
@NonNull
private String createRandomFileName(DecryptedFolderMetadataFileV1 metadata) {
String encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
while (metadata.getFiles().get(encryptedFileName) != null) {

View file

@ -23,17 +23,30 @@
package com.owncloud.android.operations;
import android.content.Context;
import android.text.TextUtils;
import com.nextcloud.client.account.User;
import com.nextcloud.client.network.ClientFactory;
import com.nextcloud.client.network.ClientFactoryImpl;
import com.nextcloud.common.NextcloudClient;
import com.owncloud.android.R;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedUser;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.resources.files.FileUtils;
import com.owncloud.android.lib.resources.shares.CreateShareRemoteOperation;
import com.owncloud.android.lib.resources.shares.OCShare;
import com.owncloud.android.lib.resources.shares.ShareType;
import com.owncloud.android.lib.resources.users.GetPublicKeyRemoteOperation;
import com.owncloud.android.operations.common.SyncOperation;
import com.owncloud.android.utils.EncryptionUtils;
import com.owncloud.android.utils.EncryptionUtilsV2;
import java.util.Arrays;
import java.util.HashSet;
@ -44,15 +57,19 @@ import java.util.Set;
*/
public class CreateShareWithShareeOperation extends SyncOperation {
private String path;
private String shareeName;
private ShareType shareType;
private int permissions;
private String noteMessage;
private String sharePassword;
private boolean hideFileDownload;
private long expirationDateInMillis;
private final String path;
private final String shareeName;
private final ShareType shareType;
private final int permissions;
private final String noteMessage;
private final String sharePassword;
private final boolean hideFileDownload;
private final long expirationDateInMillis;
private String label;
private final Context context;
private final User user;
private ArbitraryDataProvider arbitraryDataProvider;
private static final Set<ShareType> supportedShareTypes = new HashSet<>(Arrays.asList(ShareType.USER,
ShareType.GROUP,
@ -68,35 +85,9 @@ public class CreateShareWithShareeOperation extends SyncOperation {
* @param shareeName User or group name of the target sharee.
* @param shareType Type of share determines type of sharee; {@link ShareType#USER} and {@link ShareType#GROUP}
* are the only valid values for the moment.
* @param permissions Share permissions key as detailed in
* https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-share-api.html#create-a-new-share
* .
*/
public CreateShareWithShareeOperation(String path,
String shareeName,
ShareType shareType,
int permissions,
FileDataStorageManager storageManager) {
super(storageManager);
if (!supportedShareTypes.contains(shareType)) {
throw new IllegalArgumentException("Illegal share type " + shareType);
}
this.path = path;
this.shareeName = shareeName;
this.shareType = shareType;
this.permissions = permissions;
}
/**
* Constructor.
*
* @param path Full path of the file/folder being shared.
* @param shareeName User or group name of the target sharee.
* @param shareType Type of share determines type of sharee; {@link ShareType#USER} and {@link ShareType#GROUP}
* are the only valid values for the moment.
* @param permissions Share permissions key as detailed in https://doc.owncloud.org/server/8.2/developer_manual/core/ocs-share-api.html
* .
* @param permissions Share permissions key as detailed in <a
* href="https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-share-api.html">OCS
* Share API</a>.
*/
public CreateShareWithShareeOperation(String path,
String shareeName,
@ -106,7 +97,10 @@ public class CreateShareWithShareeOperation extends SyncOperation {
String sharePassword,
long expirationDateInMillis,
boolean hideFileDownload,
FileDataStorageManager storageManager) {
FileDataStorageManager storageManager,
Context context,
User user,
ArbitraryDataProvider arbitraryDataProvider) {
super(storageManager);
if (!supportedShareTypes.contains(shareType)) {
@ -120,10 +114,52 @@ public class CreateShareWithShareeOperation extends SyncOperation {
this.hideFileDownload = hideFileDownload;
this.noteMessage = noteMessage;
this.sharePassword = sharePassword;
this.context = context;
this.user = user;
this.arbitraryDataProvider = arbitraryDataProvider;
}
@Override
protected RemoteOperationResult run(OwnCloudClient client) {
OCFile folder = getStorageManager().getFileByDecryptedRemotePath(path);
if (folder == null) {
throw new IllegalArgumentException("Trying to share on a null folder: " + path);
}
boolean isEncrypted = folder.isEncrypted();
String token = null;
long newCounter = folder.getE2eCounter() + 1;
// E2E: lock folder
if (isEncrypted) {
try {
String publicKey = EncryptionUtils.getPublicKey(user, shareeName, arbitraryDataProvider);
if (publicKey.equals("")) {
NextcloudClient nextcloudClient = new ClientFactoryImpl(context).createNextcloudClient(user);
RemoteOperationResult<String> result = new GetPublicKeyRemoteOperation(shareeName).execute(nextcloudClient);
if (result.isSuccess()) {
// store it
EncryptionUtils.savePublicKey(
user,
result.getResultData(),
shareeName,
arbitraryDataProvider
);
} else {
RemoteOperationResult e = new RemoteOperationResult(new IllegalStateException());
e.setMessage(context.getString(R.string.secure_share_not_set_up));
return e;
}
}
token = EncryptionUtils.lockFolder(folder, client, newCounter);
} catch (UploadException | ClientFactory.CreationException e) {
return new RemoteOperationResult(e);
}
}
CreateShareRemoteOperation operation = new CreateShareRemoteOperation(
path,
@ -135,16 +171,75 @@ public class CreateShareWithShareeOperation extends SyncOperation {
noteMessage
);
operation.setGetShareDetails(true);
RemoteOperationResult result = operation.execute(client);
RemoteOperationResult shareResult = operation.execute(client);
if (!shareResult.isSuccess() || shareResult.getData().size() == 0) {
// something went wrong
return shareResult;
}
if (result.isSuccess() && result.getData().size() > 0) {
OCShare share = (OCShare) result.getData().get(0);
// E2E: update metadata
if (isEncrypted) {
Object object = EncryptionUtils.downloadFolderMetadata(folder,
client,
context,
user
);
if (object instanceof DecryptedFolderMetadataFileV1) {
throw new RuntimeException("Trying to share on e2e v1!");
}
DecryptedFolderMetadataFile metadata = (DecryptedFolderMetadataFile) object;
boolean metadataExists;
if (metadata == null) {
String cert = EncryptionUtils.retrievePublicKeyForUser(user, context);
metadata = new EncryptionUtilsV2().createDecryptedFolderMetadataFile();
metadata.getUsers().add(new DecryptedUser(client.getUserId(), cert));
metadataExists = false;
} else {
metadataExists = true;
}
EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2();
// add sharee to metadata
String publicKey = EncryptionUtils.getPublicKey(user, shareeName, arbitraryDataProvider);
DecryptedFolderMetadataFile newMetadata = encryptionUtilsV2.addShareeToMetadata(metadata,
shareeName,
publicKey);
// upload metadata
metadata.getMetadata().setCounter(newCounter);
try {
encryptionUtilsV2.serializeAndUploadMetadata(folder,
newMetadata,
token,
client,
metadataExists,
context,
user,
getStorageManager());
} catch (UploadException e) {
return new RemoteOperationResult<>(new RuntimeException("Uploading metadata failed"));
}
// E2E: unlock folder
RemoteOperationResult<Void> unlockResult = EncryptionUtils.unlockFolder(folder, client, token);
if (!unlockResult.isSuccess()) {
return new RemoteOperationResult<>(new RuntimeException("Unlock failed"));
}
}
OCShare share = (OCShare) shareResult.getData().get(0);
// once creating share link update other information
UpdateShareInfoOperation updateShareInfoOperation = new UpdateShareInfoOperation(share, getStorageManager());
updateShareInfoOperation.setExpirationDateInMillis(expirationDateInMillis);
updateShareInfoOperation.setHideFileDownload(hideFileDownload);
updateShareInfoOperation.setNote(noteMessage);
updateShareInfoOperation.setLabel(label);
//update permissions for external share (will otherwise default to read-only)
@ -156,9 +251,8 @@ public class CreateShareWithShareeOperation extends SyncOperation {
OCShare shareUpdated = (OCShare) updateShareInfoResult.getData().get(0);
updateData(shareUpdated);
}
}
return result;
return shareResult;
}
private void updateData(OCShare share) {

View file

@ -28,9 +28,11 @@ import android.webkit.MimeTypeMap;
import com.nextcloud.client.account.User;
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
import com.owncloud.android.datamodel.DecryptedFolderMetadata;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile;
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
import com.owncloud.android.lib.common.operations.OperationCancelledException;
@ -50,6 +52,8 @@ import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.owncloud.android.utils.EncryptionUtils.decodeStringToBase64Bytes;
/**
* Remote DownloadOperation performing the download of a file to an ownCloud server
*/
@ -217,20 +221,49 @@ public class DownloadFileOperation extends RemoteOperation {
OCFile parent = fileDataStorageManager.getFileByEncryptedRemotePath(file.getParentRemotePath());
DecryptedFolderMetadata metadata = EncryptionUtils.downloadFolderMetadata(parent,
Object object = EncryptionUtils.downloadFolderMetadata(parent,
client,
operationContext,
user);
if (metadata == null) {
if (object == null) {
return new RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND);
}
byte[] key = EncryptionUtils.decodeStringToBase64Bytes(metadata.getFiles()
.get(file.getEncryptedFileName()).getEncrypted().getKey());
byte[] iv = EncryptionUtils.decodeStringToBase64Bytes(metadata.getFiles()
.get(file.getEncryptedFileName()).getInitializationVector());
byte[] authenticationTag = EncryptionUtils.decodeStringToBase64Bytes(metadata.getFiles()
.get(file.getEncryptedFileName()).getAuthenticationTag());
String keyString;
String nonceString;
String authenticationTagString;
if (object instanceof DecryptedFolderMetadataFile) {
DecryptedFile decryptedFile = ((DecryptedFolderMetadataFile) object)
.getMetadata()
.getFiles()
.get(file.getEncryptedFileName());
if (decryptedFile == null) {
return new RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND);
}
keyString = decryptedFile.getKey();
nonceString = decryptedFile.getNonce();
authenticationTagString = decryptedFile.getAuthenticationTag();
} else {
com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile decryptedFile =
((DecryptedFolderMetadataFileV1) object)
.getFiles()
.get(file.getEncryptedFileName());
if (decryptedFile == null) {
return new RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND);
}
keyString = decryptedFile.getEncrypted().getKey();
nonceString = decryptedFile.getInitializationVector();
authenticationTagString = decryptedFile.getAuthenticationTag();
}
byte[] key = decodeStringToBase64Bytes(keyString);
byte[] iv = decodeStringToBase64Bytes(nonceString);
byte[] authenticationTag = decodeStringToBase64Bytes(authenticationTagString);
try {
byte[] decryptedBytes = EncryptionUtils.decryptFile(tmpFile,

View file

@ -29,9 +29,11 @@ import com.nextcloud.client.account.User;
import com.nextcloud.common.NextcloudClient;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
import com.owncloud.android.datamodel.DecryptedFolderMetadata;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile;
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
import com.owncloud.android.lib.common.DirectEditing;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.OwnCloudClientFactory;
@ -47,6 +49,7 @@ import com.owncloud.android.lib.resources.files.model.RemoteFile;
import com.owncloud.android.lib.resources.shares.GetSharesForFileRemoteOperation;
import com.owncloud.android.lib.resources.shares.OCShare;
import com.owncloud.android.lib.resources.shares.ShareType;
import com.owncloud.android.lib.resources.status.E2EVersion;
import com.owncloud.android.lib.resources.users.GetPredefinedStatusesRemoteOperation;
import com.owncloud.android.lib.resources.users.PredefinedStatus;
import com.owncloud.android.syncadapter.FileSyncAdapter;
@ -55,6 +58,7 @@ import com.owncloud.android.utils.EncryptionUtils;
import com.owncloud.android.utils.FileStorageUtils;
import com.owncloud.android.utils.MimeType;
import com.owncloud.android.utils.MimeTypeUtil;
import com.owncloud.android.utils.theme.CapabilityUtils;
import java.util.ArrayList;
import java.util.HashMap;
@ -236,6 +240,7 @@ public class RefreshFolderOperation extends RemoteOperation {
if (result.isSuccess()) {
if (mRemoteFolderChanged) {
// TODO catch IllegalStateException, show properly to user
result = fetchAndSyncRemoteFolder(client);
} else {
mChildren = mStorageManager.getFolderContent(mLocalFolder, false);
@ -403,7 +408,8 @@ public class RefreshFolderOperation extends RemoteOperation {
private RemoteOperationResult fetchAndSyncRemoteFolder(OwnCloudClient client) {
String remotePath = mLocalFolder.getRemotePath();
RemoteOperationResult result = new ReadFolderRemoteOperation(remotePath).execute(client);
Log_OC.d(TAG, "Synchronizing " + user.getAccountName() + remotePath);
Log_OC.d(TAG, "Refresh folder " + user.getAccountName() + remotePath);
Log_OC.d(TAG, "Refresh folder with remote id" + mLocalFolder.getRemoteId());
if (result.isSuccess()) {
synchronizeData(result.getData());
@ -470,15 +476,38 @@ public class RefreshFolderOperation extends RemoteOperation {
// update size
mLocalFolder.setFileLength(remoteFolder.getFileLength());
DecryptedFolderMetadata metadata = getDecryptedFolderMetadata(encryptedAncestor,
Object object = null;
if (mLocalFolder.isEncrypted()) {
object = getDecryptedFolderMetadata(encryptedAncestor,
mLocalFolder,
getClient(),
user,
mContext);
}
if (CapabilityUtils.getCapability(mContext).getEndToEndEncryptionApiVersion().compareTo(E2EVersion.V2_0) >= 0) {
if (encryptedAncestor && object == null) {
throw new IllegalStateException("metadata is null!");
}
}
// get current data about local contents of the folder to synchronize
Map<String, OCFile> localFilesMap = prefillLocalFilesMap(metadata,
Map<String, OCFile> localFilesMap;
E2EVersion e2EVersion;
if (object instanceof DecryptedFolderMetadataFileV1) {
e2EVersion = E2EVersion.V1_2;
localFilesMap = prefillLocalFilesMap((DecryptedFolderMetadataFileV1) object,
mStorageManager.getFolderContent(mLocalFolder, false));
} else {
e2EVersion = E2EVersion.V2_0;
localFilesMap = prefillLocalFilesMap((DecryptedFolderMetadataFile) object,
mStorageManager.getFolderContent(mLocalFolder, false));
// update counter
if (object != null) {
mLocalFolder.setE2eCounter(((DecryptedFolderMetadataFile) object).getMetadata().getCounter());
}
}
// loop to update every child
OCFile remoteFile;
@ -518,8 +547,17 @@ public class RefreshFolderOperation extends RemoteOperation {
FileStorageUtils.searchForLocalFileInDefaultPath(updatedFile, user.getAccountName());
// update file name for encrypted files
if (metadata != null) {
updateFileNameForEncryptedFile(mStorageManager, metadata, updatedFile);
if (e2EVersion == E2EVersion.V1_2) {
updateFileNameForEncryptedFileV1(mStorageManager,
(DecryptedFolderMetadataFileV1) object,
updatedFile);
} else {
updateFileNameForEncryptedFile(mStorageManager,
(DecryptedFolderMetadataFile) object,
updatedFile);
if (localFile != null) {
updatedFile.setE2eCounter(localFile.getE2eCounter());
}
}
// we parse content, so either the folder itself or its direct parent (which we check) must be encrypted
@ -531,8 +569,14 @@ public class RefreshFolderOperation extends RemoteOperation {
// save updated contents in local database
// update file name for encrypted files
if (metadata != null) {
updateFileNameForEncryptedFile(mStorageManager, metadata, mLocalFolder);
if (e2EVersion == E2EVersion.V1_2) {
updateFileNameForEncryptedFileV1(mStorageManager,
(DecryptedFolderMetadataFileV1) object,
mLocalFolder);
} else {
updateFileNameForEncryptedFile(mStorageManager,
(DecryptedFolderMetadataFile) object,
mLocalFolder);
}
mStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values());
@ -540,12 +584,12 @@ public class RefreshFolderOperation extends RemoteOperation {
}
@Nullable
public static DecryptedFolderMetadata getDecryptedFolderMetadata(boolean encryptedAncestor,
public static Object getDecryptedFolderMetadata(boolean encryptedAncestor,
OCFile localFolder,
OwnCloudClient client,
User user,
Context context) {
DecryptedFolderMetadata metadata;
Object metadata;
if (encryptedAncestor) {
metadata = EncryptionUtils.downloadFolderMetadata(localFolder, client, context, user);
} else {
@ -554,13 +598,23 @@ public class RefreshFolderOperation extends RemoteOperation {
return metadata;
}
public static void updateFileNameForEncryptedFile(FileDataStorageManager storageManager,
@NonNull DecryptedFolderMetadata metadata,
public static void updateFileNameForEncryptedFileV1(FileDataStorageManager storageManager,
@NonNull DecryptedFolderMetadataFileV1 metadata,
OCFile updatedFile) {
try {
String decryptedFileName = metadata.getFiles().get(updatedFile.getFileName()).getEncrypted()
.getFilename();
String mimetype = metadata.getFiles().get(updatedFile.getFileName()).getEncrypted().getMimetype();
String decryptedFileName;
String mimetype;
if (updatedFile.isFolder()) {
decryptedFileName = metadata.getFiles().get(updatedFile.getFileName()).getEncrypted().getFilename();
mimetype = MimeType.DIRECTORY;
} else {
com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile decryptedFile =
metadata.getFiles().get(updatedFile.getFileName());
decryptedFileName = decryptedFile.getEncrypted().getFilename();
mimetype = decryptedFile.getEncrypted().getMimetype();
}
OCFile parentFile = storageManager.getFileById(updatedFile.getParentId());
String decryptedRemotePath = parentFile.getDecryptedRemotePath() + decryptedFileName;
@ -580,7 +634,46 @@ public class RefreshFolderOperation extends RemoteOperation {
updatedFile.setMimeType(mimetype);
}
} catch (NullPointerException e) {
Log_OC.e(TAG, "Metadata for file " + updatedFile.getFileId() + " not found!");
Log_OC.e(TAG, "DecryptedMetadata for file " + updatedFile.getFileId() + " not found!");
}
}
public static void updateFileNameForEncryptedFile(FileDataStorageManager storageManager,
@NonNull DecryptedFolderMetadataFile metadata,
OCFile updatedFile) {
try {
String decryptedFileName;
String mimetype;
if (updatedFile.isFolder()) {
decryptedFileName = metadata.getMetadata().getFolders().get(updatedFile.getFileName());
mimetype = MimeType.DIRECTORY;
} else {
DecryptedFile decryptedFile = metadata.getMetadata().getFiles().get(updatedFile.getFileName());
decryptedFileName = decryptedFile.getFilename();
mimetype = decryptedFile.getMimetype();
}
OCFile parentFile = storageManager.getFileById(updatedFile.getParentId());
String decryptedRemotePath = parentFile.getDecryptedRemotePath() + decryptedFileName;
if (updatedFile.isFolder()) {
decryptedRemotePath += "/";
}
updatedFile.setDecryptedRemotePath(decryptedRemotePath);
if (mimetype == null || mimetype.isEmpty()) {
if (updatedFile.isFolder()) {
updatedFile.setMimeType(MimeType.DIRECTORY);
} else {
updatedFile.setMimeType("application/octet-stream");
}
} else {
updatedFile.setMimeType(mimetype);
}
} catch (NullPointerException e) {
Log_OC.e(TAG, "DecryptedMetadata for file " + updatedFile.getFileId() + " not found!");
}
}
@ -634,7 +727,7 @@ public class RefreshFolderOperation extends RemoteOperation {
}
@NonNull
public static Map<String, OCFile> prefillLocalFilesMap(DecryptedFolderMetadata metadata, List<OCFile> localFiles) {
public static Map<String, OCFile> prefillLocalFilesMap(Object metadata, List<OCFile> localFiles) {
Map<String, OCFile> localFilesMap = Maps.newHashMapWithExpectedSize(localFiles.size());
for (OCFile file : localFiles) {

View file

@ -104,10 +104,11 @@ public class RemoveFileOperation extends SyncOperation {
if (fileToRemove.isEncrypted()) {
OCFile parent = getStorageManager().getFileByPath(fileToRemove.getParentRemotePath());
operation = new RemoveRemoteEncryptedFileOperation(fileToRemove.getRemotePath(),
parent.getLocalId(),
user,
context,
fileToRemove.getEncryptedFileName());
fileToRemove.getEncryptedFileName(),
parent,
fileToRemove.isFolder());
} else {
operation = new RemoveFileRemoteOperation(fileToRemove.getRemotePath());
}

View file

@ -23,68 +23,58 @@ package com.owncloud.android.operations;
import android.content.Context;
import com.google.gson.reflect.TypeToken;
import com.nextcloud.client.account.User;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
import com.owncloud.android.datamodel.DecryptedFolderMetadata;
import com.owncloud.android.datamodel.EncryptedFolderMetadata;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
import com.owncloud.android.lib.common.OwnCloudClient;
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.e2ee.GetMetadataRemoteOperation;
import com.owncloud.android.lib.resources.e2ee.LockFileRemoteOperation;
import com.owncloud.android.lib.resources.e2ee.UnlockFileRemoteOperation;
import com.owncloud.android.lib.resources.e2ee.UpdateMetadataRemoteOperation;
import com.owncloud.android.utils.EncryptionUtils;
import com.owncloud.android.utils.EncryptionUtilsV2;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.jackrabbit.webdav.client.methods.DeleteMethod;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import kotlin.Pair;
/**
* Remote operation performing the removal of a remote encrypted file or folder
*/
public class RemoveRemoteEncryptedFileOperation extends RemoteOperation {
public class RemoveRemoteEncryptedFileOperation extends RemoteOperation<Void> {
private static final String TAG = RemoveRemoteEncryptedFileOperation.class.getSimpleName();
private static final int REMOVE_READ_TIMEOUT = 30000;
private static final int REMOVE_CONNECTION_TIMEOUT = 5000;
private final String remotePath;
private final long parentId;
private User user;
private final ArbitraryDataProvider arbitraryDataProvider;
private final OCFile parentFolder;
private final User user;
private final String fileName;
private final Context context;
private final boolean isFolder;
private final ArbitraryDataProvider arbitraryDataProvider;
/**
* Constructor
*
* @param remotePath RemotePath of the remote file or folder to remove from the server
* @param parentId local id of parent folder
* @param parentFolder parent folder
*/
RemoveRemoteEncryptedFileOperation(String remotePath,
long parentId,
User user,
Context context,
String fileName) {
String fileName,
OCFile parentFolder,
boolean isFolder) {
this.remotePath = remotePath;
this.parentId = parentId;
this.user = user;
this.fileName = fileName;
this.context = context;
this.parentFolder = parentFolder;
this.isFolder = isFolder;
arbitraryDataProvider = new ArbitraryDataProviderImpl(context);
}
@ -93,46 +83,19 @@ public class RemoveRemoteEncryptedFileOperation extends RemoteOperation {
* Performs the remove operation.
*/
@Override
protected RemoteOperationResult run(OwnCloudClient client) {
RemoteOperationResult result;
protected RemoteOperationResult<Void> run(OwnCloudClient client) {
RemoteOperationResult<Void> result;
DeleteMethod delete = null;
String token = null;
DecryptedFolderMetadata metadata;
String privateKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PRIVATE_KEY);
String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY);
try {
// Lock folder
RemoteOperationResult lockFileOperationResult = new LockFileRemoteOperation(parentId).execute(client);
token = EncryptionUtils.lockFolder(parentFolder, client);
if (lockFileOperationResult.isSuccess()) {
token = (String) lockFileOperationResult.getData().get(0);
} else if (lockFileOperationResult.getHttpCode() == HttpStatus.SC_FORBIDDEN) {
throw new RemoteOperationFailedException("Forbidden! Please try again later.)");
} else {
throw new RemoteOperationFailedException("Unknown error!");
}
// refresh metadata
RemoteOperationResult getMetadataOperationResult = new GetMetadataRemoteOperation(parentId).execute(client);
if (getMetadataOperationResult.isSuccess()) {
// decrypt metadata
String serializedEncryptedMetadata = (String) getMetadataOperationResult.getData().get(0);
EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.deserializeJSON(
serializedEncryptedMetadata, new TypeToken<EncryptedFolderMetadata>() {
});
metadata = EncryptionUtils.decryptFolderMetaData(encryptedFolderMetadata,
privateKey,
arbitraryDataProvider,
user,
parentId);
} else {
throw new RemoteOperationFailedException("No Metadata found!");
}
EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2();
Pair<Boolean, DecryptedFolderMetadataFile> pair = encryptionUtilsV2.retrieveMetadata(parentFolder, client, user, context);
boolean metadataExists = pair.getFirst();
DecryptedFolderMetadataFile metadata = pair.getSecond();
// delete file remote
delete = new DeleteMethod(client.getFilesDavUri(remotePath));
@ -140,35 +103,29 @@ public class RemoveRemoteEncryptedFileOperation extends RemoteOperation {
int status = client.executeMethod(delete, REMOVE_READ_TIMEOUT, REMOVE_CONNECTION_TIMEOUT);
delete.getResponseBodyAsString(); // exhaust the response, although not interesting
result = new RemoteOperationResult(delete.succeeded() || status == HttpStatus.SC_NOT_FOUND, delete);
result = new RemoteOperationResult<>(delete.succeeded() || status == HttpStatus.SC_NOT_FOUND, delete);
Log_OC.i(TAG, "Remove " + remotePath + ": " + result.getLogMessage());
// remove file from metadata
metadata.getFiles().remove(fileName);
EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(
metadata,
publicKey,
arbitraryDataProvider,
user,
parentId);
String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);
if (isFolder) {
encryptionUtilsV2.removeFolderFromMetadata(fileName, metadata);
} else {
encryptionUtilsV2.removeFileFromMetadata(fileName, metadata);
}
// upload metadata
RemoteOperationResult uploadMetadataOperationResult =
new UpdateMetadataRemoteOperation(parentId,
serializedFolderMetadata, token).execute(client);
if (!uploadMetadataOperationResult.isSuccess()) {
throw new RemoteOperationFailedException("Metadata not uploaded!");
}
encryptionUtilsV2.serializeAndUploadMetadata(parentFolder,
metadata,
token,
client,
metadataExists,
context,
user,
new FileDataStorageManager(user, context.getContentResolver()));
// return success
return result;
} catch (NoSuchAlgorithmException | IOException | InvalidKeyException | InvalidAlgorithmParameterException |
NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException | InvalidKeySpecException |
CertificateException e) {
result = new RemoteOperationResult(e);
} catch (Exception e) {
result = new RemoteOperationResult<>(e);
Log_OC.e(TAG, "Remove " + remotePath + ": " + result.getLogMessage(), e);
} finally {
@ -178,11 +135,12 @@ public class RemoveRemoteEncryptedFileOperation extends RemoteOperation {
// unlock file
if (token != null) {
RemoteOperationResult unlockFileOperationResult = new UnlockFileRemoteOperation(parentId, token)
.execute(client);
RemoteOperationResult<Void> unlockFileOperationResult = EncryptionUtils.unlockFolder(parentFolder,
client,
token);
if (!unlockFileOperationResult.isSuccess()) {
Log_OC.e(TAG, "Failed to unlock " + parentId);
Log_OC.e(TAG, "Failed to unlock " + parentFolder.getLocalId());
}
}
}

View file

@ -26,9 +26,10 @@ import android.text.TextUtils;
import com.nextcloud.client.account.User;
import com.nextcloud.client.files.downloader.FileDownloadHelper;
import com.owncloud.android.datamodel.DecryptedFolderMetadata;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.operations.OperationCancelledException;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
@ -37,6 +38,7 @@ import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation;
import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation;
import com.owncloud.android.lib.resources.files.model.RemoteFile;
import com.owncloud.android.lib.resources.status.E2EVersion;
import com.owncloud.android.operations.common.SyncOperation;
import com.owncloud.android.services.OperationsService;
import com.owncloud.android.utils.FileStorageUtils;
@ -49,6 +51,8 @@ import java.util.Map;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicBoolean;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
* Remote operation performing the synchronization of the list of files contained
@ -215,6 +219,7 @@ public class SynchronizeFolderOperation extends SyncOperation {
ReadFolderRemoteOperation operation = new ReadFolderRemoteOperation(mRemotePath);
RemoteOperationResult result = operation.execute(client);
Log_OC.d(TAG, "Synchronizing " + user.getAccountName() + mRemotePath);
Log_OC.d(TAG, "Synchronizing remote id" + mLocalFolder.getRemoteId());
if (result.isSuccess()) {
synchronizeData(result.getData());
@ -281,16 +286,28 @@ public class SynchronizeFolderOperation extends SyncOperation {
// update richWorkspace
mLocalFolder.setRichWorkspace(remoteFolder.getRichWorkspace());
DecryptedFolderMetadata metadata = RefreshFolderOperation.getDecryptedFolderMetadata(encryptedAncestor,
Object object = RefreshFolderOperation.getDecryptedFolderMetadata(encryptedAncestor,
mLocalFolder,
getClient(),
user,
mContext);
if (mLocalFolder.isEncrypted() && object == null) {
throw new IllegalStateException("metadata is null!");
}
// get current data about local contents of the folder to synchronize
Map<String, OCFile> localFilesMap =
RefreshFolderOperation.prefillLocalFilesMap(metadata,
Map<String, OCFile> localFilesMap;
E2EVersion e2EVersion;
if (object instanceof DecryptedFolderMetadataFileV1) {
e2EVersion = E2EVersion.V1_2;
localFilesMap = RefreshFolderOperation.prefillLocalFilesMap((DecryptedFolderMetadataFileV1) object,
storageManager.getFolderContent(mLocalFolder, false));
} else {
e2EVersion = E2EVersion.V2_0;
localFilesMap = RefreshFolderOperation.prefillLocalFilesMap((DecryptedFolderMetadataFile) object,
storageManager.getFolderContent(mLocalFolder, false));
}
// loop to synchronize every child
List<OCFile> updatedFiles = new ArrayList<>(folderAndFiles.size() - 1);
@ -323,8 +340,14 @@ public class SynchronizeFolderOperation extends SyncOperation {
FileStorageUtils.searchForLocalFileInDefaultPath(updatedFile, user.getAccountName());
// update file name for encrypted files
if (metadata != null) {
RefreshFolderOperation.updateFileNameForEncryptedFile(storageManager, metadata, updatedFile);
if (e2EVersion == E2EVersion.V1_2) {
RefreshFolderOperation.updateFileNameForEncryptedFileV1(storageManager,
(DecryptedFolderMetadataFileV1) object,
updatedFile);
} else {
RefreshFolderOperation.updateFileNameForEncryptedFile(storageManager,
(DecryptedFolderMetadataFile) object,
updatedFile);
}
// we parse content, so either the folder itself or its direct parent (which we check) must be encrypted
@ -337,8 +360,15 @@ public class SynchronizeFolderOperation extends SyncOperation {
updatedFiles.add(updatedFile);
}
if (metadata != null) {
RefreshFolderOperation.updateFileNameForEncryptedFile(storageManager, metadata, mLocalFolder);
// update file name for encrypted files
if (e2EVersion == E2EVersion.V1_2) {
RefreshFolderOperation.updateFileNameForEncryptedFileV1(storageManager,
(DecryptedFolderMetadataFileV1) object,
mLocalFolder);
} else {
RefreshFolderOperation.updateFileNameForEncryptedFile(storageManager,
(DecryptedFolderMetadataFile) object,
mLocalFolder);
}
// save updated contents in local database
@ -391,6 +421,7 @@ public class SynchronizeFolderOperation extends SyncOperation {
}
@SuppressFBWarnings("JLM")
private void prepareOpsFromLocalKnowledge() throws OperationCancelledException {
List<OCFile> children = getStorageManager().getFolderContent(mLocalFolder, false);
for (OCFile child : children) {

View file

@ -21,8 +21,13 @@
package com.owncloud.android.operations;
import android.content.Context;
import com.nextcloud.client.account.User;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
@ -32,6 +37,8 @@ import com.owncloud.android.lib.resources.shares.OCShare;
import com.owncloud.android.lib.resources.shares.RemoveShareRemoteOperation;
import com.owncloud.android.lib.resources.shares.ShareType;
import com.owncloud.android.operations.common.SyncOperation;
import com.owncloud.android.utils.EncryptionUtils;
import com.owncloud.android.utils.EncryptionUtilsV2;
import java.util.List;
@ -45,27 +52,89 @@ public class UnshareOperation extends SyncOperation {
private final String remotePath;
private final long shareId;
private final Context context;
private final User user;
public UnshareOperation(String remotePath, long shareId, FileDataStorageManager storageManager) {
public UnshareOperation(String remotePath,
long shareId,
FileDataStorageManager storageManager,
User user,
Context context) {
super(storageManager);
this.remotePath = remotePath;
this.shareId = shareId;
this.user = user;
this.context = context;
}
@Override
protected RemoteOperationResult run(OwnCloudClient client) {
RemoteOperationResult result;
String token = null;
// Get Share for a file
OCShare share = getStorageManager().getShareById(shareId);
if (share != null) {
OCFile file = getStorageManager().getFileByEncryptedRemotePath(remotePath);
if (file.isEncrypted() && share.getShareType() != ShareType.PUBLIC_LINK) {
// E2E: lock folder
try {
token = EncryptionUtils.lockFolder(file, client, file.getE2eCounter() + 1);
} catch (UploadException e) {
return new RemoteOperationResult(e);
}
// download metadata
Object object = EncryptionUtils.downloadFolderMetadata(file,
client,
context,
user);
if (object == null) {
return new RemoteOperationResult(new RuntimeException("No metadata!"));
}
if (object instanceof DecryptedFolderMetadataFileV1) {
throw new RuntimeException("Trying to unshare on e2e v1!");
}
DecryptedFolderMetadataFile metadata = (DecryptedFolderMetadataFile) object;
// remove sharee from metadata
EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2();
DecryptedFolderMetadataFile newMetadata = encryptionUtilsV2.removeShareeFromMetadata(metadata,
share.getShareWith());
// upload metadata
try {
encryptionUtilsV2.serializeAndUploadMetadata(file,
newMetadata,
token,
client,
true,
context,
user,
getStorageManager());
} catch (UploadException e) {
return new RemoteOperationResult(new RuntimeException("Upload of metadata failed!"));
}
}
RemoveShareRemoteOperation operation = new RemoveShareRemoteOperation(share.getRemoteId());
result = operation.execute(client);
if (result.isSuccess()) {
// E2E: unlock folder
if (file.isEncrypted() && share.getShareType() != ShareType.PUBLIC_LINK) {
RemoteOperationResult<Void> unlockResult = EncryptionUtils.unlockFolder(file, client, token);
if (!unlockResult.isSuccess()) {
return new RemoteOperationResult<>(new RuntimeException("Unlock failed"));
}
}
Log_OC.d(TAG, "Share id = " + share.getRemoteId() + " deleted");
if (ShareType.PUBLIC_LINK == share.getShareType()) {

View file

@ -25,7 +25,6 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Pair;
import com.nextcloud.client.account.User;
import com.nextcloud.client.device.BatteryStatus;
@ -34,12 +33,17 @@ import com.nextcloud.client.network.Connectivity;
import com.nextcloud.client.network.ConnectivityService;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
import com.owncloud.android.datamodel.DecryptedFolderMetadata;
import com.owncloud.android.datamodel.EncryptedFolderMetadata;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.ThumbnailsCacheManager;
import com.owncloud.android.datamodel.UploadsStorageManager;
import com.owncloud.android.datamodel.e2e.v1.decrypted.Data;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedMetadata;
import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFile;
import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFolderMetadataFileV1;
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
import com.owncloud.android.db.OCUpload;
import com.owncloud.android.files.services.FileUploader;
import com.owncloud.android.files.services.NameCollisionPolicy;
@ -56,13 +60,16 @@ import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation;
import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation;
import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation;
import com.owncloud.android.lib.resources.files.model.RemoteFile;
import com.owncloud.android.lib.resources.status.E2EVersion;
import com.owncloud.android.operations.common.SyncOperation;
import com.owncloud.android.utils.EncryptionUtils;
import com.owncloud.android.utils.EncryptionUtilsV2;
import com.owncloud.android.utils.FileStorageUtils;
import com.owncloud.android.utils.FileUtil;
import com.owncloud.android.utils.MimeType;
import com.owncloud.android.utils.MimeTypeUtil;
import com.owncloud.android.utils.UriUtils;
import com.owncloud.android.utils.theme.CapabilityUtils;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.RequestEntity;
@ -80,9 +87,11 @@ import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import androidx.annotation.CheckResult;
@ -90,8 +99,7 @@ import androidx.annotation.Nullable;
/**
* Operation performing the update in the ownCloud server
* of a file that was modified locally.
* Operation performing the update in the ownCloud server of a file that was modified locally.
*/
public class UploadFileOperation extends SyncOperation {
@ -261,7 +269,9 @@ public class UploadFileOperation extends SyncOperation {
return mWhileChargingOnly;
}
public boolean isIgnoringPowerSaveMode() { return mIgnoringPowerSaveMode; }
public boolean isIgnoringPowerSaveMode() {
return mIgnoringPowerSaveMode;
}
public User getUser() {
return user;
@ -444,8 +454,6 @@ public class UploadFileOperation extends SyncOperation {
String token = null;
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(getContext());
String privateKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PRIVATE_KEY);
String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY);
try {
@ -456,32 +464,75 @@ public class UploadFileOperation extends SyncOperation {
return result;
}
/***** E2E *****/
// Only on V2+: whenever we change something, increase counter
long counter = -1;
if (CapabilityUtils.getCapability(mContext).getEndToEndEncryptionApiVersion().compareTo(E2EVersion.V2_0) >= 0) {
counter = parentFile.getE2eCounter() + 1;
}
// we might have an old token from interrupted upload
if (mFolderUnlockToken != null && !mFolderUnlockToken.isEmpty()) {
token = mFolderUnlockToken;
} else {
token = EncryptionUtils.lockFolder(parentFile, client);
token = EncryptionUtils.lockFolder(parentFile, client, counter);
// immediately store it
mUpload.setFolderUnlockToken(token);
uploadsStorageManager.updateUpload(mUpload);
}
// Update metadata
Pair<Boolean, DecryptedFolderMetadata> metadataPair = EncryptionUtils.retrieveMetadata(parentFile,
client,
privateKey,
publicKey,
arbitraryDataProvider,
user);
EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2();
// kotlin.Pair<Boolean, DecryptedFolderMetadataFile> metadataPair =
// encryptionUtilsV2.retrieveMetadata(parentFile,
// client,
// user,
// mContext);
metadataExists = metadataPair.first;
DecryptedFolderMetadata metadata = metadataPair.second;
Object object = EncryptionUtils.downloadFolderMetadata(parentFile, client, mContext, user);
if (CapabilityUtils.getCapability(mContext).getEndToEndEncryptionApiVersion().compareTo(E2EVersion.V2_0) >= 0) {
if (object == null) {
// TODO return error
return new RemoteOperationResult(new IllegalStateException("Metadata does not exist"));
} else {
metadataExists = true;
}
} else {
// v1 is allowed to be null, thus create it
DecryptedFolderMetadataFileV1 metadata = new DecryptedFolderMetadataFileV1();
metadata.setMetadata(new DecryptedMetadata());
metadata.getMetadata().setVersion(1.2);
metadata.getMetadata().setMetadataKeys(new HashMap<>());
String metadataKey = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey());
String encryptedMetadataKey = EncryptionUtils.encryptStringAsymmetric(metadataKey, publicKey);
metadata.getMetadata().setMetadataKey(encryptedMetadataKey);
object = metadata;
metadataExists = false;
}
// todo fail if no metadata
// metadataExists = metadataPair.getFirst();
// DecryptedFolderMetadataFile metadata = metadataPair.getSecond();
// TODO E2E: check counter: must be less than our counter, check rest: signature, etc
/**** E2E *****/
// check name collision
RemoteOperationResult collisionResult = checkNameCollision(client, metadata, parentFile.isEncrypted());
List<String> fileNames = new ArrayList<>();
if (object instanceof DecryptedFolderMetadataFileV1 metadata) {
for (DecryptedFile file : metadata.getFiles().values()) {
fileNames.add(file.getEncrypted().getFilename());
}
} else {
for (com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile file :
((DecryptedFolderMetadataFile) object).getMetadata().getFiles().values()) {
fileNames.add(file.getFilename());
}
}
RemoteOperationResult collisionResult = checkNameCollision(client, fileNames, parentFile.isEncrypted());
if (collisionResult != null) {
result = collisionResult;
return collisionResult;
@ -509,18 +560,24 @@ public class UploadFileOperation extends SyncOperation {
// IV, always generate new one
byte[] iv = EncryptionUtils.randomBytes(EncryptionUtils.ivLength);
EncryptionUtils.EncryptedFile encryptedFile = EncryptionUtils.encryptFile(mFile, key, iv);
EncryptedFile encryptedFile = EncryptionUtils.encryptFile(mFile, key, iv);
// new random file name, check if it exists in metadata
String encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
String encryptedFileName = EncryptionUtils.generateUid();
if (object instanceof DecryptedFolderMetadataFileV1 metadata) {
while (metadata.getFiles().get(encryptedFileName) != null) {
encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
encryptedFileName = EncryptionUtils.generateUid();
}
} else {
while (((DecryptedFolderMetadataFile) object).getMetadata().getFiles().get(encryptedFileName) != null) {
encryptedFileName = EncryptionUtils.generateUid();
}
}
File encryptedTempFile = File.createTempFile("encFile", encryptedFileName);
FileOutputStream fileOutputStream = new FileOutputStream(encryptedTempFile);
fileOutputStream.write(encryptedFile.encryptedBytes);
fileOutputStream.write(encryptedFile.getEncryptedBytes());
fileOutputStream.close();
/***** E2E *****/
@ -556,7 +613,6 @@ public class UploadFileOperation extends SyncOperation {
size = new File(mFile.getStoragePath()).length();
}
updateSize(size);
/// perform the upload
@ -605,24 +661,28 @@ public class UploadFileOperation extends SyncOperation {
mFile.setDecryptedRemotePath(parentFile.getDecryptedRemotePath() + originalFile.getName());
mFile.setRemotePath(parentFile.getRemotePath() + encryptedFileName);
if (object instanceof DecryptedFolderMetadataFileV1 metadata) {
// update metadata
DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile();
DecryptedFolderMetadata.Data data = new DecryptedFolderMetadata.Data();
DecryptedFile decryptedFile = new DecryptedFile();
Data data = new Data();
data.setFilename(mFile.getDecryptedFileName());
data.setMimetype(mFile.getMimeType());
data.setKey(EncryptionUtils.encodeBytesToBase64String(key));
decryptedFile.setEncrypted(data);
decryptedFile.setInitializationVector(EncryptionUtils.encodeBytesToBase64String(iv));
decryptedFile.setAuthenticationTag(encryptedFile.authenticationTag);
decryptedFile.setAuthenticationTag(encryptedFile.getAuthenticationTag());
metadata.getFiles().put(encryptedFileName, decryptedFile);
EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata,
EncryptedFolderMetadataFileV1 encryptedFolderMetadata =
EncryptionUtils.encryptFolderMetadata(metadata,
publicKey,
arbitraryDataProvider,
parentFile.getLocalId(),
user,
parentFile.getLocalId());
arbitraryDataProvider
);
String serializedFolderMetadata;
@ -639,9 +699,38 @@ public class UploadFileOperation extends SyncOperation {
token,
client,
metadataExists,
E2EVersion.V1_2,
"",
arbitraryDataProvider,
user);
// unlock
result = EncryptionUtils.unlockFolderV1(parentFile, client, token);
if (result.isSuccess()) {
token = null;
}
} else {
DecryptedFolderMetadataFile metadata = (DecryptedFolderMetadataFile) object;
encryptionUtilsV2.addFileToMetadata(
encryptedFileName,
mFile,
iv,
encryptedFile.getAuthenticationTag(),
key,
metadata,
getStorageManager());
// upload metadata
encryptionUtilsV2.serializeAndUploadMetadata(parentFile,
metadata,
token,
client,
metadataExists,
mContext,
user,
getStorageManager());
// unlock
result = EncryptionUtils.unlockFolder(parentFile, client, token);
@ -649,6 +738,7 @@ public class UploadFileOperation extends SyncOperation {
token = null;
}
}
}
} catch (FileNotFoundException e) {
Log_OC.d(TAG, mFile.getStoragePath() + " not exists anymore");
result = new RemoteOperationResult(ResultCode.LOCAL_FILE_NOT_FOUND);
@ -947,18 +1037,18 @@ public class UploadFileOperation extends SyncOperation {
@CheckResult
private RemoteOperationResult checkNameCollision(OwnCloudClient client,
DecryptedFolderMetadata metadata,
List<String> fileNames,
boolean encrypted)
throws OperationCancelledException {
Log_OC.d(TAG, "Checking name collision in server");
if (existsFile(client, mRemotePath, metadata, encrypted)) {
if (existsFile(client, mRemotePath, fileNames, encrypted)) {
switch (mNameCollisionPolicy) {
case CANCEL:
Log_OC.d(TAG, "File exists; canceling");
throw new OperationCancelledException();
case RENAME:
mRemotePath = getNewAvailableRemotePath(client, mRemotePath, metadata, encrypted);
mRemotePath = getNewAvailableRemotePath(client, mRemotePath, fileNames, encrypted);
mWasRenamed = true;
createNewOCFile(mRemotePath);
Log_OC.d(TAG, "File renamed as " + mRemotePath);
@ -1041,15 +1131,14 @@ public class UploadFileOperation extends SyncOperation {
}
/**
* Checks the existence of the folder where the current file will be uploaded both
* in the remote server and in the local database.
* Checks the existence of the folder where the current file will be uploaded both in the remote server and in the
* local database.
* <p/>
* If the upload is set to enforce the creation of the folder, the method tries to
* create it both remote and locally.
* If the upload is set to enforce the creation of the folder, the method tries to create it both remote and
* locally.
*
* @param pathToGrant Full remote path whose existence will be granted.
* @return An {@link OCFile} instance corresponding to the folder where the file
* will be uploaded.
* @return An {@link OCFile} instance corresponding to the folder where the file will be uploaded.
*/
private RemoteOperationResult grantFolderExistence(String pathToGrant, OwnCloudClient client) {
RemoteOperation operation = new ExistenceCheckRemoteOperation(pathToGrant, false);
@ -1117,15 +1206,16 @@ public class UploadFileOperation extends SyncOperation {
}
/**
* Returns a new and available (does not exists on the server) remotePath.
* This adds an incremental suffix.
* Returns a new and available (does not exists on the server) remotePath. This adds an incremental suffix.
*
* @param client OwnCloud client
* @param remotePath remote path of the file
* @param metadata metadata of encrypted folder
* @param fileNames list of decrypted file names
* @return new remote path
*/
private String getNewAvailableRemotePath(OwnCloudClient client, String remotePath, DecryptedFolderMetadata metadata,
private String getNewAvailableRemotePath(OwnCloudClient client,
String remotePath,
List<String> fileNames,
boolean encrypted) {
int extPos = remotePath.lastIndexOf('.');
String suffix;
@ -1142,20 +1232,22 @@ public class UploadFileOperation extends SyncOperation {
do {
suffix = " (" + count + ")";
newPath = extPos >= 0 ? remotePathWithoutExtension + suffix + "." + extension : remotePath + suffix;
exists = existsFile(client, newPath, metadata, encrypted);
exists = existsFile(client, newPath, fileNames, encrypted);
count++;
} while (exists);
return newPath;
}
private boolean existsFile(OwnCloudClient client, String remotePath, DecryptedFolderMetadata metadata,
private boolean existsFile(OwnCloudClient client,
String remotePath,
List<String> fileNames,
boolean encrypted) {
if (encrypted) {
String fileName = new File(remotePath).getName();
for (DecryptedFolderMetadata.DecryptedFile file : metadata.getFiles().values()) {
if (file.getEncrypted().getFilename().equalsIgnoreCase(fileName)) {
for (String name : fileNames) {
if (name.equalsIgnoreCase(fileName)) {
return true;
}
}
@ -1169,9 +1261,8 @@ public class UploadFileOperation extends SyncOperation {
}
/**
* Allows to cancel the actual upload operation. If actual upload operating
* is in progress it is cancelled, if upload preparation is being performed
* upload will not take place.
* Allows to cancel the actual upload operation. If actual upload operating is in progress it is cancelled, if
* upload preparation is being performed upload will not take place.
*/
public void cancel(ResultCode cancellationReason) {
if (mUploadOperation == null) {
@ -1322,9 +1413,8 @@ public class UploadFileOperation extends SyncOperation {
/**
* Saves a OC File after a successful upload.
* <p>
* A PROPFIND is necessary to keep the props in the local database
* synchronized with the server, specially the modification time and Etag
* (where available)
* A PROPFIND is necessary to keep the props in the local database synchronized with the server, specially the
* modification time and Etag (where available)
*/
private void saveUploadedFile(OwnCloudClient client) {
OCFile file = mFile;

View file

@ -0,0 +1,34 @@
/*
* Nextcloud Android client application
*
* @author Álvaro Brey
* Copyright (C) 2023 Álvaro Brey
* Copyright (C) 2023 Nextcloud GmbH
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or 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 <http://www.gnu.org/licenses/>.
*
*/
package com.owncloud.android.providers
/**
* This is a data class that holds the configuration for the user and group searchable.
* As we cannot access searchable providers in runtime, injecting a singleton into them is the only way to change their
* config.
*/
data class UsersAndGroupsSearchConfig(var searchOnlyUsers: Boolean = false) {
fun reset() {
searchOnlyUsers = false
}
}

View file

@ -34,6 +34,7 @@ import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import com.nextcloud.client.account.User;
@ -116,6 +117,8 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
@Inject
protected UserAccountManager accountManager;
@Inject
protected UsersAndGroupsSearchConfig searchConfig;
private static final Map<String, ShareType> sShareTypes = new HashMap<>();
@ -193,6 +196,10 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
}
private Cursor searchForUsersOrGroups(Uri uri) {
// TODO check searchConfig and filter results
Log.d(TAG, "searchForUsersOrGroups: searchConfig only users: " + searchConfig.getSearchOnlyUsers());
String lastPathSegment = uri.getLastPathSegment();
if (lastPathSegment == null) {
@ -206,15 +213,14 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
String userQuery = lastPathSegment.toLowerCase(Locale.ROOT);
// request to the OC server about users and groups matching userQuery
GetShareesRemoteOperation searchRequest = new GetShareesRemoteOperation(userQuery, REQUESTED_PAGE,
GetShareesRemoteOperation searchRequest = new GetShareesRemoteOperation(userQuery,
REQUESTED_PAGE,
RESULTS_PER_PAGE);
RemoteOperationResult result = searchRequest.execute(user, getContext());
RemoteOperationResult<ArrayList<JSONObject>> result = searchRequest.execute(user, getContext());
List<JSONObject> names = new ArrayList<>();
if (result.isSuccess()) {
for (Object o : result.getData()) {
names.add((JSONObject) o);
}
names = result.getResultData();
} else {
showErrorMessage(result);
}
@ -272,6 +278,11 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
status = new Status(StatusType.OFFLINE, "", "", -1);
}
if (searchConfig.getSearchOnlyUsers() && type != ShareType.USER) {
// skip all types but users, as E2E secure share is only allowed to users on same server
continue;
}
switch (type) {
case GROUP:
displayName = userName;

View file

@ -44,6 +44,7 @@ import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.java.util.Optional;
import com.nextcloud.utils.extensions.IntentExtensionsKt;
import com.owncloud.android.MainApp;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.lib.common.OwnCloudAccount;
@ -140,6 +141,7 @@ public class OperationsService extends Service {
mUndispatchedFinishedOperations = new ConcurrentHashMap<>();
@Inject UserAccountManager accountManager;
@Inject ArbitraryDataProvider arbitraryDataProvider;
private static class Target {
public Uri mServerUrl;
@ -610,7 +612,10 @@ public class OperationsService extends Service {
sharePassword,
expirationDateInMillis,
hideFileDownload,
fileDataStorageManager);
fileDataStorageManager,
getApplicationContext(),
user,
arbitraryDataProvider);
if (operationIntent.hasExtra(EXTRA_SHARE_PUBLIC_LABEL)) {
createShareWithShareeOperation.setLabel(operationIntent.getStringExtra(EXTRA_SHARE_PUBLIC_LABEL));
@ -654,7 +659,11 @@ public class OperationsService extends Service {
shareId = operationIntent.getLongExtra(EXTRA_SHARE_ID, -1);
if (shareId > 0) {
operation = new UnshareOperation(remotePath, shareId, fileDataStorageManager);
operation = new UnshareOperation(remotePath,
shareId,
fileDataStorageManager,
user,
getApplicationContext());
}
break;

View file

@ -81,6 +81,7 @@ import com.owncloud.android.operations.UpdateNoteForShareOperation;
import com.owncloud.android.operations.UpdateShareInfoOperation;
import com.owncloud.android.operations.UpdateSharePermissionsOperation;
import com.owncloud.android.operations.UpdateShareViaLinkOperation;
import com.owncloud.android.providers.UsersAndGroupsSearchConfig;
import com.owncloud.android.providers.UsersAndGroupsSearchProvider;
import com.owncloud.android.services.OperationsService;
import com.owncloud.android.services.OperationsService.OperationsServiceBinder;
@ -182,6 +183,12 @@ public abstract class FileActivity extends DrawerActivity
@Inject
EditorUtils editorUtils;
@Inject
UsersAndGroupsSearchConfig usersAndGroupsSearchConfig;
@Inject
ArbitraryDataProvider arbitraryDataProvider;
@Override
public void showFiles(boolean onDeviceOnly) {
// must be specialized in subclasses
@ -203,6 +210,7 @@ public abstract class FileActivity extends DrawerActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
usersAndGroupsSearchConfig.reset();
mHandler = new Handler();
mFileOperationsHelper = new FileOperationsHelper(this, getUserAccountManager(), connectivityService, editorUtils);
User user = null;
@ -907,7 +915,9 @@ public abstract class FileActivity extends DrawerActivity
protected void doShareWith(String shareeName, ShareType shareType) {
FileDetailFragment fragment = getFileDetailFragment();
if (fragment != null) {
fragment.initiateSharingProcess(shareeName, shareType);
fragment.initiateSharingProcess(shareeName,
shareType,
usersAndGroupsSearchConfig.getSearchOnlyUsers());
}
}

View file

@ -474,7 +474,7 @@ public class SettingsActivity extends PreferenceActivity
}
private void setupE2EMnemonicPreference(PreferenceCategory preferenceCategoryMore) {
String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC);
String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC).trim();
Preference pMnemonic = findPreference("mnemonic");
if (pMnemonic != null) {
@ -991,7 +991,7 @@ public class SettingsActivity extends PreferenceActivity
RequestCredentialsActivity.KEY_CHECK_RESULT_TRUE) {
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(this);
String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC);
String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC).trim();
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.FallbackTheming_Dialog);
AlertDialog alertDialog = builder.setTitle(R.string.prefs_e2e_mnemonic)

View file

@ -143,7 +143,8 @@ public class ShareActivity extends FileActivity {
getSupportFragmentManager().beginTransaction().replace(R.id.share_fragment_container,
FileDetailsSharingProcessFragment.newInstance(getFile(),
shareeName,
shareType),
shareType,
false),
FileDetailsSharingProcessFragment.TAG)
.commit();
}

View file

@ -25,7 +25,6 @@ import com.nextcloud.ui.ImageDetailFragment;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.ui.fragment.FileDetailActivitiesFragment;
import com.owncloud.android.ui.fragment.FileDetailSharingFragment;
import com.owncloud.android.utils.EncryptionUtils;
import com.owncloud.android.utils.MimeTypeUtil;
import androidx.annotation.NonNull;
@ -39,15 +38,20 @@ import androidx.fragment.app.FragmentStatePagerAdapter;
public class FileDetailTabAdapter extends FragmentStatePagerAdapter {
private final OCFile file;
private final User user;
private final boolean showSharingTab;
private FileDetailSharingFragment fileDetailSharingFragment;
private FileDetailActivitiesFragment fileDetailActivitiesFragment;
private ImageDetailFragment imageDetailFragment;
public FileDetailTabAdapter(FragmentManager fm, OCFile file, User user) {
public FileDetailTabAdapter(FragmentManager fm,
OCFile file,
User user,
boolean showSharingTab) {
super(fm);
this.file = file;
this.user = user;
this.showSharingTab = showSharingTab;
}
@NonNull
@ -81,17 +85,16 @@ public class FileDetailTabAdapter extends FragmentStatePagerAdapter {
@Override
public int getCount() {
if (file.isEncrypted()) {
if (EncryptionUtils.supportsSecureFiledrop(file, user)) {
return 2;
}
// sharing not allowed for encrypted files, thus only show first tab (activities)
return 1;
}
// unencrypted files/folders
if (showSharingTab) {
if (MimeTypeUtil.isImage(file)) {
return 3;
}
return 2;
} else {
if (MimeTypeUtil.isImage(file)) {
return 2;
}
return 1;
}
}
}

View file

@ -54,12 +54,13 @@ import com.owncloud.android.databinding.GridItemBinding;
import com.owncloud.android.databinding.ListFooterBinding;
import com.owncloud.android.databinding.ListHeaderBinding;
import com.owncloud.android.databinding.ListItemBinding;
import com.owncloud.android.datamodel.DecryptedFolderMetadata;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.SyncedFolderProvider;
import com.owncloud.android.datamodel.ThumbnailsCacheManager;
import com.owncloud.android.datamodel.VirtualFolderType;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
import com.owncloud.android.db.ProviderMeta;
import com.owncloud.android.lib.common.OwnCloudClientFactory;
import com.owncloud.android.lib.common.accounts.AccountUtils;
@ -285,6 +286,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
for (OCFile file : mFiles) {
if (file.getRemoteId().equals(fileId)) {
file.setEncrypted(encrypted);
file.setE2eCounter(0L);
mStorageManager.saveFile(file);
break;
@ -294,6 +296,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
for (OCFile file : mFilesAll) {
if (file.getRemoteId().equals(fileId)) {
file.setEncrypted(encrypted);
file.setE2eCounter(0L);
}
}
@ -435,7 +438,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
return;
}
ocFileListDelegate.bindGridViewHolder(gridViewHolder, file, searchType);
ocFileListDelegate.bindGridViewHolder(gridViewHolder, file, currentDirectory, searchType);
checkVisibilityOfMoreButtons(gridViewHolder);
checkVisibilityOfFileFeaturesLayout(gridViewHolder);
@ -890,19 +893,29 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
OCFile parentFolder = mStorageManager.getFileById(ocFile.getParentId());
if (parentFolder != null && (ocFile.isEncrypted() || parentFolder.isEncrypted())) {
DecryptedFolderMetadata metadata = RefreshFolderOperation.getDecryptedFolderMetadata(
Object object = RefreshFolderOperation.getDecryptedFolderMetadata(
true,
parentFolder,
OwnCloudClientFactory.createOwnCloudClient(user.toPlatformAccount(), activity),
user,
activity);
if (metadata == null) {
if (object == null) {
throw new IllegalStateException("metadata is null!");
}
if (object instanceof DecryptedFolderMetadataFileV1) {
// update ocFile
RefreshFolderOperation.updateFileNameForEncryptedFile(mStorageManager, metadata, ocFile);
RefreshFolderOperation.updateFileNameForEncryptedFileV1(mStorageManager,
(DecryptedFolderMetadataFileV1) object,
ocFile);
} else {
// update ocFile
RefreshFolderOperation.updateFileNameForEncryptedFile(mStorageManager,
(DecryptedFolderMetadataFile) object,
ocFile);
}
ocFile = mStorageManager.saveFileWithParent(ocFile, activity);
}

View file

@ -205,6 +205,7 @@ class OCFileListDelegate(
fun bindGridViewHolder(
gridViewHolder: ListGridImageViewHolder,
file: OCFile,
currentDirectory: OCFile?,
searchType: SearchType?
) {
// thumbnail
@ -250,8 +251,9 @@ class OCFileListDelegate(
file.isEncrypted ||
file.isEncrypted &&
!EncryptionUtils.supportsSecureFiledrop(file, user) ||
searchType == SearchType.FAVORITE_SEARCH
)
searchType == SearchType.FAVORITE_SEARCH ||
file.isFolder && currentDirectory?.isEncrypted ?: false
) // sharing an encrypted subfolder is not possible
if (shouldHideShare) {
gridViewHolder.shared.visibility = View.GONE
} else {

View file

@ -45,12 +45,12 @@ import com.owncloud.android.datamodel.ArbitraryDataProvider
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl
import com.owncloud.android.lib.common.accounts.AccountUtils
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.lib.resources.users.DeletePublicKeyOperation
import com.owncloud.android.lib.resources.users.GetPrivateKeyOperation
import com.owncloud.android.lib.resources.users.GetPublicKeyOperation
import com.owncloud.android.lib.resources.users.SendCSROperation
import com.owncloud.android.lib.resources.users.StorePrivateKeyOperation
import com.owncloud.android.utils.CsrHelper
import com.owncloud.android.lib.resources.e2ee.CsrHelper
import com.owncloud.android.lib.resources.users.DeletePublicKeyRemoteOperation
import com.owncloud.android.lib.resources.users.GetPrivateKeyRemoteOperation
import com.owncloud.android.lib.resources.users.GetPublicKeyRemoteOperation
import com.owncloud.android.lib.resources.users.SendCSRRemoteOperation
import com.owncloud.android.lib.resources.users.StorePrivateKeyRemoteOperation
import com.owncloud.android.utils.EncryptionUtils
import com.owncloud.android.utils.theme.ViewThemeUtils
import java.io.IOException
@ -175,7 +175,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
try {
val privateKey = task?.get()
val mnemonicUnchanged = binding.encryptionPasswordInput.text.toString()
val mnemonicUnchanged = binding.encryptionPasswordInput.text.toString().trim()
val mnemonic =
binding.encryptionPasswordInput.text.toString().replace("\\s".toRegex(), "")
.lowercase()
@ -294,11 +294,11 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
// if available
// - store public key
// - decrypt private key, store unencrypted private key in database
val context = mWeakContext.get()
val publicKeyOperation = GetPublicKeyOperation()
val context = mWeakContext.get() ?: return null
val publicKeyOperation = GetPublicKeyRemoteOperation()
val user = user ?: return null
val publicKeyResult = publicKeyOperation.execute(user, context)
val publicKeyResult = publicKeyOperation.executeNextcloudClient(user, context)
if (publicKeyResult.isSuccess) {
Log_OC.d(TAG, "public key successful downloaded for " + user.accountName)
@ -317,7 +317,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
return null
}
val privateKeyResult = GetPrivateKeyOperation().execute(user, context)
val privateKeyResult = GetPrivateKeyRemoteOperation().executeNextcloudClient(user, context)
if (privateKeyResult.isSuccess) {
Log_OC.d(TAG, "private key successful downloaded for " + user!!.accountName)
keyResult = KEY_EXISTING_USED
@ -387,6 +387,11 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
val context = mWeakContext.get()
val publicKeyString: String
if (context == null) {
keyResult = KEY_FAILED
return ""
}
// Create public/private key pair
val keyPair = EncryptionUtils.generateKeyPair()
@ -395,12 +400,12 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
val user = user ?: return ""
val userId = accountManager.getUserData(user.toPlatformAccount(), AccountUtils.Constants.KEY_USER_ID)
val urlEncoded = CsrHelper.generateCsrPemEncodedString(keyPair, userId)
val operation = SendCSROperation(urlEncoded)
val result = operation.execute(user, context)
val urlEncoded = CsrHelper().generateCsrPemEncodedString(keyPair, userId)
val operation = SendCSRRemoteOperation(urlEncoded)
val result = operation.executeNextcloudClient(user, context)
if (result.isSuccess) {
publicKeyString = result.data[0] as String
publicKeyString = result.resultData
if (!EncryptionUtils.isMatchingKeys(keyPair, publicKeyString)) {
EncryptionUtils.reportE2eError(arbitraryDataProvider, user)
throw RuntimeException("Wrong CSR returned")
@ -420,8 +425,8 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
)
// upload encryptedPrivateKey
val storePrivateKeyOperation = StorePrivateKeyOperation(encryptedPrivateKey)
val storePrivateKeyResult = storePrivateKeyOperation.execute(user, context)
val storePrivateKeyOperation = StorePrivateKeyRemoteOperation(encryptedPrivateKey)
val storePrivateKeyResult = storePrivateKeyOperation.executeNextcloudClient(user, context)
if (storePrivateKeyResult.isSuccess) {
Log_OC.d(TAG, "private key success")
arbitraryDataProvider?.storeOrUpdateKeyValue(
@ -441,10 +446,10 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
)
keyResult = KEY_CREATED
return storePrivateKeyResult.data[0] as String
return storePrivateKeyResult.resultData
} else {
val deletePublicKeyOperation = DeletePublicKeyOperation()
deletePublicKeyOperation.execute(user, context)
val deletePublicKeyOperation = DeletePublicKeyRemoteOperation()
deletePublicKeyOperation.executeNextcloudClient(user, context)
}
} catch (e: Exception) {
Log_OC.e(TAG, e.message)

View file

@ -133,11 +133,11 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
* @param user Currently active user
* @return New fragment with arguments set
*/
public static FileDetailFragment newInstance(OCFile fileToDetail, OCFile parentFile, User user) {
public static FileDetailFragment newInstance(OCFile fileToDetail, OCFile parentFolder, User user) {
FileDetailFragment frag = new FileDetailFragment();
Bundle args = new Bundle();
args.putParcelable(ARG_FILE, fileToDetail);
args.putParcelable(ARG_PARENT_FOLDER, parentFile);
args.putParcelable(ARG_PARENT_FOLDER, parentFolder);
args.putParcelable(ARG_USER, user);
frag.setArguments(args);
return frag;
@ -304,7 +304,7 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.drawer_item_activities).setIcon(R.drawable.ic_activity));
if (!getFile().isEncrypted() || EncryptionUtils.supportsSecureFiledrop(getFile(), user)) {
if (showSharingTab()) {
binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.share_dialog_title).setIcon(R.drawable.shared_via_users));
}
@ -314,7 +314,10 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
viewThemeUtils.material.themeTabLayout(binding.tabLayout);
final FileDetailTabAdapter adapter = new FileDetailTabAdapter(getFragmentManager(), getFile(), user);
final FileDetailTabAdapter adapter = new FileDetailTabAdapter(getFragmentManager(),
getFile(),
user,
showSharingTab());
binding.pager.setAdapter(adapter);
binding.pager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(binding.tabLayout) {
@Override
@ -733,11 +736,14 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
* @param shareeName
* @param shareType
*/
public void initiateSharingProcess(String shareeName, ShareType shareType) {
public void initiateSharingProcess(String shareeName,
ShareType shareType,
boolean secureShare) {
requireActivity().getSupportFragmentManager().beginTransaction().add(R.id.sharing_frame_container,
FileDetailsSharingProcessFragment.newInstance(getFile(),
shareeName,
shareType),
shareType,
secureShare),
FileDetailsSharingProcessFragment.TAG)
.commit();
@ -801,6 +807,24 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
}
}
private boolean showSharingTab() {
if (getFile().isEncrypted()) {
if (parentFolder == null) {
parentFolder = storageManager.getFileById(getFile().getParentId());
}
if (EncryptionUtils.supportsSecureFiledrop(getFile(), user) && !parentFolder.isEncrypted()) {
return true;
} else {
// sharing not allowed for encrypted files, thus only show first tab (activities)
// sharing not allowed for encrypted subfolders
return false;
}
} else {
// unencrypted files/folders
return true;
}
}
/**
* Helper class responsible for updating the progress bar shown for file downloading.
*/

View file

@ -59,6 +59,7 @@ import com.owncloud.android.lib.resources.shares.ShareType;
import com.owncloud.android.lib.resources.status.NextcloudVersion;
import com.owncloud.android.lib.resources.status.OCCapability;
import com.owncloud.android.lib.resources.status.OwnCloudVersion;
import com.owncloud.android.providers.UsersAndGroupsSearchConfig;
import com.owncloud.android.ui.activity.FileActivity;
import com.owncloud.android.ui.activity.FileDisplayActivity;
import com.owncloud.android.ui.adapter.ShareeListAdapter;
@ -108,6 +109,7 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
@Inject UserAccountManager accountManager;
@Inject ClientFactory clientFactory;
@Inject ViewThemeUtils viewThemeUtils;
@Inject UsersAndGroupsSearchConfig searchConfig;
public static FileDetailSharingFragment newInstance(OCFile file, User user) {
FileDetailSharingFragment fragment = new FileDetailSharingFragment();
@ -204,20 +206,47 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
}
}
@Override
public void onStart() {
super.onStart();
searchConfig.setSearchOnlyUsers(file.isEncrypted());
}
@Override
public void onStop() {
super.onStop();
searchConfig.reset();
}
private void setupView() {
setShareWithYou();
if (file.isEncrypted()) {
binding.searchContainer.setVisibility(View.GONE);
} else {
OCFile parentFile = fileDataStorageManager.getFileById(file.getParentId());
FileDetailSharingFragmentHelper.setupSearchView(
(SearchManager) fileActivity.getSystemService(Context.SEARCH_SERVICE),
binding.searchView,
fileActivity.getComponentName());
viewThemeUtils.androidx.themeToolbarSearchView(binding.searchView);
if (file.canReshare()) {
if (file.isEncrypted() || (parentFile != null && parentFile.isEncrypted())) {
if (file.getE2eCounter() == -1) {
// V1 cannot share
binding.searchContainer.setVisibility(View.GONE);
} else {
binding.searchView.setQueryHint(getResources().getString(R.string.secure_share_search));
if (file.isSharedViaLink()) {
binding.searchView.setQueryHint(getResources().getString(R.string.share_not_allowed_when_file_drop));
binding.searchView.setInputType(InputType.TYPE_NULL);
disableSearchView(binding.searchView);
}
}
} else {
binding.searchView.setQueryHint(getResources().getString(R.string.share_search));
}
} else {
binding.searchView.setQueryHint(getResources().getString(R.string.reshare_not_allowed));
binding.searchView.setInputType(InputType.TYPE_NULL);
@ -225,7 +254,6 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
disableSearchView(binding.searchView);
}
}
}
private void disableSearchView(View view) {
view.setEnabled(false);
@ -424,6 +452,7 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
* before reading database.
*/
public void refreshSharesFromDB() {
file = fileDataStorageManager.getFileById(file.getFileId());
ShareeListAdapter adapter = (ShareeListAdapter) binding.sharesList.getAdapter();
if (adapter == null) {

View file

@ -72,6 +72,7 @@ class FileDetailsSharingProcessFragment :
private const val ARG_SCREEN_TYPE = "arg_screen_type"
private const val ARG_RESHARE_SHOWN = "arg_reshare_shown"
private const val ARG_EXP_DATE_SHOWN = "arg_exp_date_shown"
private const val ARG_SECURE_SHARE = "secure_share"
// types of screens to be displayed
const val SCREEN_TYPE_PERMISSION = 1 // permissions screen
@ -81,11 +82,17 @@ class FileDetailsSharingProcessFragment :
* fragment instance to be called while creating new share for internal and external share
*/
@JvmStatic
fun newInstance(file: OCFile, shareeName: String, shareType: ShareType): FileDetailsSharingProcessFragment {
fun newInstance(
file: OCFile,
shareeName: String,
shareType: ShareType,
secureShare: Boolean
): FileDetailsSharingProcessFragment {
val args = Bundle()
args.putParcelable(ARG_OCFILE, file)
args.putSerializable(ARG_SHARE_TYPE, shareType)
args.putString(ARG_SHAREE_NAME, shareeName)
args.putBoolean(ARG_SECURE_SHARE, secureShare)
val fragment = FileDetailsSharingProcessFragment()
fragment.arguments = args
return fragment
@ -127,6 +134,7 @@ class FileDetailsSharingProcessFragment :
private var share: OCShare? = null
private var isReShareShown: Boolean = true // show or hide reShare option
private var isExpDateShown: Boolean = true // show or hide expiry date option
private var isSecureShare: Boolean = false
private var expirationDatePickerFragment: ExpirationDatePickerDialogFragment? = null
@ -156,6 +164,7 @@ class FileDetailsSharingProcessFragment :
shareProcessStep = it.getInt(ARG_SCREEN_TYPE, SCREEN_TYPE_PERMISSION)
isReShareShown = it.getBoolean(ARG_RESHARE_SHOWN, true)
isExpDateShown = it.getBoolean(ARG_EXP_DATE_SHOWN, true)
isSecureShare = it.getBoolean(ARG_SECURE_SHARE, false)
}
fileActivity = activity as FileActivity?
@ -222,8 +231,22 @@ class FileDetailsSharingProcessFragment :
binding.shareProcessEditShareLink.visibility = View.VISIBLE
binding.shareProcessGroupTwo.visibility = View.GONE
if (share != null) setupModificationUI() else setupUpdateUI()
binding.shareProcessSetExpDateSwitch.visibility = if (isExpDateShown) View.VISIBLE else View.GONE
if (share != null) {
setupModificationUI()
} else {
setupUpdateUI()
}
if (isSecureShare) {
binding.shareProcessAdvancePermissionTitle.visibility = View.GONE
}
// show or hide expiry date
if (isExpDateShown && !isSecureShare) {
binding.shareProcessSetExpDateSwitch.visibility = View.VISIBLE
} else {
binding.shareProcessSetExpDateSwitch.visibility = View.GONE
}
shareProcessStep = SCREEN_TYPE_PERMISSION
}
@ -310,7 +333,11 @@ class FileDetailsSharingProcessFragment :
binding.shareProcessChangeNameSwitch.visibility = View.GONE
binding.shareProcessChangeNameContainer.visibility = View.GONE
binding.shareProcessHideDownloadCheckbox.visibility = View.GONE
if (isSecureShare) {
binding.shareProcessAllowResharingCheckbox.visibility = View.GONE
} else {
binding.shareProcessAllowResharingCheckbox.visibility = View.VISIBLE
}
binding.shareProcessSetPasswordSwitch.visibility = View.GONE
if (share != null) {
@ -367,6 +394,11 @@ class FileDetailsSharingProcessFragment :
binding.shareProcessPermissionUploadEditing.text =
requireContext().resources.getString(R.string.link_share_allow_upload_and_editing)
binding.shareProcessPermissionFileDrop.visibility = View.VISIBLE
if (isSecureShare) {
binding.shareProcessPermissionFileDrop.visibility = View.GONE
binding.shareProcessAllowResharingCheckbox.visibility = View.GONE
binding.shareProcessSetExpDateSwitch.visibility = View.GONE
}
}
/**
@ -569,7 +601,8 @@ class FileDetailsSharingProcessFragment :
binding.shareProcessEnterPassword.text.toString().trim(),
chosenExpDateInMills,
noteText,
binding.shareProcessChangeName.text.toString().trim()
binding.shareProcessChangeName.text.toString().trim(),
true
)
}
removeCurrentFragment()

View file

@ -73,12 +73,11 @@ import com.nextcloud.utils.view.FastScrollUtils;
import com.owncloud.android.MainApp;
import com.owncloud.android.R;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.DecryptedFolderMetadata;
import com.owncloud.android.datamodel.EncryptedFolderMetadata;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.SyncedFolderProvider;
import com.owncloud.android.datamodel.VirtualFolderType;
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
import com.owncloud.android.lib.common.Creator;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.operations.RemoteOperation;
@ -87,6 +86,7 @@ import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.lib.resources.e2ee.ToggleEncryptionRemoteOperation;
import com.owncloud.android.lib.resources.files.SearchRemoteOperation;
import com.owncloud.android.lib.resources.files.ToggleFavoriteRemoteOperation;
import com.owncloud.android.lib.resources.status.E2EVersion;
import com.owncloud.android.lib.resources.status.OCCapability;
import com.owncloud.android.ui.activity.FileActivity;
import com.owncloud.android.ui.activity.FileDisplayActivity;
@ -116,6 +116,7 @@ import com.owncloud.android.ui.preview.PreviewMediaActivity;
import com.owncloud.android.ui.preview.PreviewTextFileFragment;
import com.owncloud.android.utils.DisplayUtils;
import com.owncloud.android.utils.EncryptionUtils;
import com.owncloud.android.utils.EncryptionUtilsV2;
import com.owncloud.android.utils.FileSortOrder;
import com.owncloud.android.utils.FileStorageUtils;
import com.owncloud.android.utils.MimeTypeUtil;
@ -1712,13 +1713,15 @@ public class OCFileListFragment extends ExtendedListFragment implements
dialog.setTargetFragment(this, SETUP_ENCRYPTION_REQUEST_CODE);
dialog.show(getParentFragmentManager(), SETUP_ENCRYPTION_DIALOG_TAG);
} else {
// TODO E2E: if encryption fails, to not set it as encrypted!
encryptFolder(file,
event.getLocalId(),
event.getRemoteId(),
event.getRemotePath(),
event.getShouldBeEncrypted(),
publicKey,
privateKey);
privateKey,
storageManager);
}
}
@ -1727,9 +1730,11 @@ public class OCFileListFragment extends ExtendedListFragment implements
String remoteId,
String remotePath,
boolean shouldBeEncrypted,
String publicKey,
String privateKey) {
String publicKeyString,
String privateKeyString,
FileDataStorageManager storageManager) {
try {
Log_OC.d(TAG, "encrypt folder " + folder.getRemoteId());
User user = accountManager.getUser();
OwnCloudClient client = clientFactory.create(user);
RemoteOperationResult remoteOperationResult = new ToggleEncryptionRemoteOperation(localId,
@ -1741,44 +1746,44 @@ public class OCFileListFragment extends ExtendedListFragment implements
// lock folder
String token = EncryptionUtils.lockFolder(folder, client);
OCCapability ocCapability = mContainerActivity.getStorageManager().getCapability(user.getAccountName());
if (ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V2_0) {
// Update metadata
Pair<Boolean, DecryptedFolderMetadata> metadataPair = EncryptionUtils.retrieveMetadata(folder,
Pair<Boolean, DecryptedFolderMetadataFile> metadataPair = EncryptionUtils.retrieveMetadata(folder,
client,
privateKey,
publicKey,
arbitraryDataProvider,
user);
privateKeyString,
publicKeyString,
storageManager,
user,
requireContext(),
arbitraryDataProvider);
boolean metadataExists = metadataPair.first;
DecryptedFolderMetadata metadata = metadataPair.second;
DecryptedFolderMetadataFile metadata = metadataPair.second;
EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata,
publicKey,
arbitraryDataProvider,
user,
folder.getLocalId());
String serializedFolderMetadata;
// check if we need metadataKeys
if (metadata.getMetadata().getMetadataKey() != null) {
serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata, true);
} else {
serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);
}
// upload metadata
EncryptionUtils.uploadMetadata(folder,
serializedFolderMetadata,
new EncryptionUtilsV2().serializeAndUploadMetadata(folder,
metadata,
token,
client,
metadataExists,
arbitraryDataProvider,
user);
requireContext(),
user,
storageManager);
// unlock folder
EncryptionUtils.unlockFolder(folder, client, token);
} else if (ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V1_0 ||
ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V1_1 ||
ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V1_2
) {
// unlock folder
EncryptionUtils.unlockFolderV1(folder, client, token);
} else if (ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.UNKNOWN) {
throw new IllegalArgumentException("Unknown E2E version");
}
mAdapter.setEncryptionAttributeForItemID(remoteId, shouldBeEncrypted);
} else if (remoteOperationResult.getHttpCode() == HttpStatus.SC_FORBIDDEN) {
Snackbar.make(getRecyclerView(),
@ -1790,7 +1795,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
Snackbar.LENGTH_LONG).show();
}
} catch (Exception e) {
} catch (Throwable e) {
Log_OC.e(TAG, "Error creating encrypted folder", e);
}
}

View file

@ -568,13 +568,22 @@ public class FileOperationsHelper {
* @param note note message for the receiver. Null or empty for no message
* @param label new label
*/
public void shareFileWithSharee(OCFile file, String shareeName, ShareType shareType, int permissions,
boolean hideFileDownload, String password, long expirationTimeInMillis,
String note, String label) {
public void shareFileWithSharee(OCFile file,
String shareeName,
ShareType shareType,
int permissions,
boolean hideFileDownload,
String password,
long expirationTimeInMillis,
String note,
String label,
boolean showLoadingDialog) {
if (file != null) {
// TODO check capability?
if (showLoadingDialog) {
fileActivity.showLoadingDialog(fileActivity.getApplicationContext().
getString(R.string.wait_a_moment));
}
Intent service = new Intent(fileActivity, OperationsService.class);
service.setAction(OperationsService.ACTION_CREATE_SHARE_WITH_SHAREE);

View file

@ -1,82 +0,0 @@
package com.owncloud.android.utils;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.ExtensionsGenerator;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.util.PrivateKeyFactory;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
import java.io.IOException;
import java.security.KeyPair;
import androidx.annotation.VisibleForTesting;
/**
* copied & modified from:
* https://github.com/awslabs/aws-sdk-android-samples/blob/master/CreateIotCertWithCSR/src/com/amazonaws/demo/csrcert/CsrHelper.java
* accessed at 31.08.17
* Original parts are licensed under the Apache License, Version 2.0: http://aws.amazon.com/apache2.0
* Own parts are licensed under GPLv3+.
*/
public final class CsrHelper {
private CsrHelper() {
// utility class -> private constructor
}
/**
* Generate CSR with PEM encoding
*
* @param keyPair the KeyPair with private and public keys
* @param userId userId of CSR owner
* @return PEM encoded CSR string
* @throws IOException thrown if key cannot be created
* @throws OperatorCreationException thrown if contentSigner cannot be build
*/
public static String generateCsrPemEncodedString(KeyPair keyPair, String userId)
throws IOException, OperatorCreationException {
PKCS10CertificationRequest csr = CsrHelper.generateCSR(keyPair, userId);
byte[] derCSR = csr.getEncoded();
return "-----BEGIN CERTIFICATE REQUEST-----\n" + android.util.Base64.encodeToString(derCSR,
android.util.Base64.NO_WRAP) + "\n-----END CERTIFICATE REQUEST-----";
}
/**
* Create the certificate signing request (CSR) from private and public keys
*
* @param keyPair the KeyPair with private and public keys
* @param userId userId of CSR owner
* @return PKCS10CertificationRequest with the certificate signing request (CSR) data
* @throws IOException thrown if key cannot be created
* @throws OperatorCreationException thrown if contentSigner cannot be build
*/
@VisibleForTesting
public static PKCS10CertificationRequest generateCSR(KeyPair keyPair, String userId) throws IOException,
OperatorCreationException {
String principal = "CN=" + userId;
AsymmetricKeyParameter privateKey = PrivateKeyFactory.createKey(keyPair.getPrivate().getEncoded());
AlgorithmIdentifier signatureAlgorithm = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA1WITHRSA");
AlgorithmIdentifier digestAlgorithm = new DefaultDigestAlgorithmIdentifierFinder().find("SHA-1");
ContentSigner signer = new BcRSAContentSignerBuilder(signatureAlgorithm, digestAlgorithm).build(privateKey);
PKCS10CertificationRequestBuilder csrBuilder = new JcaPKCS10CertificationRequestBuilder(new X500Name(principal),
keyPair.getPublic());
ExtensionsGenerator extensionsGenerator = new ExtensionsGenerator();
extensionsGenerator.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
csrBuilder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensionsGenerator.generate());
return csrBuilder.build(signer);
}
}

File diff suppressed because it is too large Load diff

View file

@ -510,6 +510,7 @@
<string name="share_via_link_unset_password">Unset</string>
<string name="share_search">Name, Federated Cloud ID or email address …</string>
<string name="secure_share_search">Secure share …</string>
<string name="share_group_clarification">%1$s (group)</string>
<string name="share_remote_clarification">%1$s (remote)</string>
@ -1125,4 +1126,6 @@
<string name="sub_folder_rule_year">Year</string>
<string name="sub_folder_rule_month">Year/Month</string>
<string name="sub_folder_rule_day">Year/Month/Day</string>
<string name="secure_share_not_set_up">Secure sharing is not set up for this user</string>
<string name="share_not_allowed_when_file_drop">Resharing is not allowed during secure file drop</string>
</resources>