Skip to content

SHA-256 Hash #1725

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {

Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
*
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible, could you apply the fail fast here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by that?

Copy link
Contributor

@alperozturk96 alperozturk96 May 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having nested if-else blocks, you can return early. However, I think the downloadFile() function is too complex for that. We can skip it, if it’s too much work to refactor.

e.g.

Before:

public void checkAge(int age) {
    if (age >= 0) {
        if (age >= 18) {
            System.out.println("User is an adult.");
        } else {
            System.out.println("User is a minor.");
        }
    } else {
        System.out.println("Invalid age.");
    }
}

After:

public void checkAge(int age) {
    if (age < 0) {
        System.out.println("Invalid age.");
        return;
    }

    if (age >= 18) {
        System.out.println("User is an adult.");
        return;
    }
    
    System.out.println("User is a minor.");
}

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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ protected RemoteOperationResult<String> 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);
Expand Down