/* * * 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 . */ package com.nextcloud.client; import android.accounts.AccountManager; import com.owncloud.android.AbstractOnServerIT; import com.owncloud.android.datamodel.ArbitraryDataProvider; 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.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 net.bytebuddy.utility.RandomString; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import java.io.File; import java.io.IOException; import java.security.KeyPair; import java.util.ArrayList; import java.util.List; import java.util.Random; 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; 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/"; @BeforeClass public static void initClass() { arbitraryDataProvider = new ArbitraryDataProvider(targetContext.getContentResolver()); } @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().execute(client, getStorageManager()).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 downloadFile() throws Exception { init(); uploadFile(1); downloadFile(1); } private void init() throws Exception { // create folder createFolder(rootEncFolder); OCFile encFolder = createFolder(rootEncFolder + RandomString.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() + RandomString.make(5) + "/"; Log_OC.d(this, "[" + i + "/" + actionCount + "] " + "Create folder: " + path); return createFolder(path); } private void goIntoFolder(int i) { ArrayList 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 = RandomString.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 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(account, 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()); } private void deleteFile(int i) { ArrayList 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, account, false, targetContext) .execute(client, getStorageManager()) .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<>()); createKeys(); // 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 publicKeyResult = publicKeyOperation.execute(account, targetContext); assertTrue(publicKeyResult.isSuccess()); String publicKeyFromServer = (String) publicKeyResult.getData().get(0); arbitraryDataProvider.storeOrUpdateKeyValue(account.name, EncryptionUtils.PUBLIC_KEY, publicKeyFromServer); RemoteOperationResult 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 void createKeys() throws Exception { String publicKey; // 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()) { publicKey = (String) result.getData().get(0); } 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, publicKey); arbitraryDataProvider.storeOrUpdateKeyValue(account.name, EncryptionUtils.MNEMONIC, generateMnemonicString()); } else { throw new RuntimeException("Error uploading private key!"); } } private String generateMnemonicString() { return "1 2 3 4 5 6"; } public void after() { // remove all encrypted files OCFile root = fileDataStorageManager.getFileByDecryptedRemotePath("/"); removeFolder(root); // List 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 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, account, false, targetContext) .execute(client, getStorageManager()) .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()); } }