diff --git a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientManagerFactory.java b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientManagerFactory.java index 71227d156..a49de6ef1 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientManagerFactory.java +++ b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClientManagerFactory.java @@ -15,6 +15,8 @@ public class OwnCloudClientManagerFactory { private static String sUserAgent = "Mozilla/5.0 (Android) Nextcloud-android"; private static String proxyHost = ""; private static int proxyPort = -1; + private static boolean hashCheckEnable = false; + private static boolean hashCheckDownloadEnable = false; public static OwnCloudClientManager getDefaultSingleton() { if (sDefaultSingleton == null) { @@ -46,4 +48,19 @@ public static void setProxyPort(int port) { public static int getProxyPort() { return proxyPort; } + + public static void setHashCheck(boolean status) { + hashCheckEnable = status; + } + + public static boolean getHashCheck() { + return hashCheckEnable; + } + public static void setHashDownloadCheck(boolean status) { + hashCheckDownloadEnable = status; + } + + public static boolean getHashDownloadCheck() { + return hashCheckDownloadEnable; + } } diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.java b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.java index 818c4e0e4..29c6ad110 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.java +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/ChunkedFileUploadRemoteOperation.java @@ -9,6 +9,7 @@ import android.text.TextUtils; import com.owncloud.android.lib.common.OwnCloudClient; +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; import com.owncloud.android.lib.common.network.ChunkFromFileChannelRequestEntity; import com.owncloud.android.lib.common.network.ProgressiveDataTransfer; import com.owncloud.android.lib.common.network.WebdavEntry; @@ -36,6 +37,9 @@ import androidx.annotation.VisibleForTesting; +import java.security.MessageDigest; +import java.nio.ByteBuffer; +import java.math.BigInteger; public class ChunkedFileUploadRemoteOperation extends UploadFileRemoteOperation { @@ -216,6 +220,12 @@ protected RemoteOperationResult run(OwnCloudClient client) { moveMethod = new MoveMethod(originUri, destinationUri, true); moveMethod.addRequestHeader(OC_X_OC_MTIME_HEADER, String.valueOf(lastModificationTimestamp)); + File localFile = new File(localPath); + String hash = FileUtils.getHashFromFile(this, localFile, "SHA-256"); + if(hash != null) { + putMethod.addRequestHeader("X-Content-Hash", hash); + } + if (creationTimestamp != null && creationTimestamp > 0) { moveMethod.addRequestHeader(OC_X_OC_CTIME_HEADER, String.valueOf(creationTimestamp)); } @@ -291,6 +301,24 @@ private RemoteOperationResult uploadChunk(OwnCloudClient client, Chunk chunk) th putMethod.addRequestHeader(E2E_TOKEN, token); } + if (OwnCloudClientManagerFactory.getHashCheck()) { + try (RandomAccessFile hashRaf = new RandomAccessFile(file, "r")) { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + + FileChannel hashChannel = hashRaf.getChannel(); + ByteBuffer buf = ByteBuffer.allocate((int) chunk.getLength()); + hashChannel.position(chunk.getStart()); + hashChannel.read(buf); + md.update(buf.array()); + + String chunkHash = String.format("%064x", new BigInteger(1, md.digest())); + + putMethod.addRequestHeader("X-Content-Hash", chunkHash); + } catch (Exception e) { + Log_OC.w(TAG, "Could not compute chunk hash"); + } + } + status = client.executeMethod(putMethod); result = new RemoteOperationResult(isSuccess(status), putMethod); diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.java b/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.java index b5de6ebf9..79e9115cf 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.java +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.java @@ -6,7 +6,10 @@ */ package com.owncloud.android.lib.resources.files; +import android.os.Build; + import com.owncloud.android.lib.common.OwnCloudClient; +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; import com.owncloud.android.lib.common.network.OnDatatransferProgressListener; import com.owncloud.android.lib.common.network.WebdavUtils; import com.owncloud.android.lib.common.operations.OperationCancelledException; @@ -28,6 +31,10 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.Locale; + +import androidx.annotation.RequiresApi; + /** * Remote operation performing the download of a remote file in the ownCloud server. * @@ -82,6 +89,7 @@ protected RemoteOperationResult run(OwnCloudClient client) { } + @RequiresApi(api = Build.VERSION_CODES.GINGERBREAD) private int downloadFile(OwnCloudClient client, File targetFile) throws IOException, OperationCancelledException, CreateLocalFileException { int status; boolean savedFile = false; @@ -135,8 +143,60 @@ private int downloadFile(OwnCloudClient client, File targetFile) throws IOExcept transferEncoding = "chunked".equals(transferEncodingHeader.getValue()); } - if (transferred == totalToTransfer || transferEncoding) { - savedFile = true; + if (transferred == totalToTransfer || transferEncoding) { + if (OwnCloudClientManagerFactory.getHashDownloadCheck()){ + Header hashHeader = getMethod.getResponseHeader("X-Content-Hash"); + String expectedHash = hashHeader != null ? hashHeader.getValue() : null; + if (expectedHash != null) { + try { + String[] entries = expectedHash.split(","); + for (String entry : entries) { + String[] parts = entry.split(";", 2); + if (parts.length != 2) continue; + String Algorithm = parts[0].trim().toLowerCase(Locale.ROOT); + String hash = parts[1].trim(); + + String digestAlgorithm = null; + + switch (Algorithm) { + case "sha256": + digestAlgorithm = "SHA-256"; + break; + case "sha1": + digestAlgorithm = "SHA-1"; + break; + case "md5": + digestAlgorithm = "MD5"; + break; + default: + // Skip unknown algorithm + continue; + } + + String fileHash = FileUtils.getHashFromFile(this, targetFile, digestAlgorithm); + + if (!hash.equalsIgnoreCase(fileHash)) { + // Hash is incorrect: delete file and abort + Log_OC.w(TAG, "Hash mismatch: expected="+ hash +" actual="+ fileHash); + status = 418; + savedFile = false; + }else{ + savedFile = true; + break; + } + } + } catch (Exception e) { + Log_OC.w(TAG, "Could not compute chunk hash"); + status = 418; + savedFile = false; + } + }else { + savedFile = true; + } + }else { + savedFile = true; + } + Header modificationTime = getMethod.getResponseHeader("Last-Modified"); if (modificationTime == null) { modificationTime = getMethod.getResponseHeader("last-modified"); diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/FileUtils.java b/library/src/main/java/com/owncloud/android/lib/resources/files/FileUtils.java index fa9f79460..f2fc9b56d 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/FileUtils.java +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/FileUtils.java @@ -7,19 +7,42 @@ package com.owncloud.android.lib.resources.files; import java.io.File; +import java.io.FileInputStream; import java.math.BigInteger; +import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; +import com.owncloud.android.lib.common.utils.Log_OC; + public class FileUtils { private static final String TAG = FileUtils.class.getSimpleName(); public static final String PATH_SEPARATOR = "/"; + public static String getHashFromFile(Object thi, File f, String digestAlgorithm) { + if (OwnCloudClientManagerFactory.getHashCheck()) { + try { + MessageDigest md = MessageDigest.getInstance(digestAlgorithm); + try (FileInputStream fis = new FileInputStream(f); + DigestInputStream dis = new DigestInputStream(fis, md)) { + byte[] buffer = new byte[8192]; + while (dis.read(buffer) != -1) { + // digest is updated by reading + } + } + return String.format("%064x", new BigInteger(1, md.digest())); + } catch (Exception e) { + Log_OC.w(thi, "Could not compute chunk hash"); + } + } + return null; + } public static String getParentPath(String remotePath) { String parentPath = new File(remotePath).getParent(); parentPath = parentPath.endsWith(PATH_SEPARATOR) ? parentPath : parentPath + PATH_SEPARATOR; diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperation.java b/library/src/main/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperation.java index 652e22f1f..5801c1b7d 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperation.java +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/UploadFileRemoteOperation.java @@ -211,6 +211,12 @@ protected RemoteOperationResult uploadFile(OwnCloudClient client) throws } putMethod.setRequestEntity(entity); + + String Hash = FileUtils.getHashFromFile(this, f, "SHA-256"); + if(Hash != null){ + putMethod.addRequestHeader("X-Content-Hash", Hash); + } + status = client.executeMethod(putMethod); result = new RemoteOperationResult<>(isSuccess(status), putMethod);