android: verify downloaded data against digests from blobrefs

This commit is contained in:
Daniel Erat 2011-03-19 17:06:33 -07:00
parent 94bd0ec0c8
commit 7262d8a736
4 changed files with 123 additions and 9 deletions

View File

@ -0,0 +1,78 @@
/*
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.camlistore;
import android.util.Log;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
// Used to verify that the digest of a blob's contents match the digest from its blobref.
class BlobVerifier {
private static final String TAG = "BlobVerifier";
private static final String SHA1_PREFIX = "sha1-";
private static final String MD5_PREFIX = "md5-";
private final String mBlobRef;
// Effectively-final members assigned either in the c'tor or in a method that it calls.
private String mExpectedDigest;
private MessageDigest mDigester;
// Initializes a verifier for |blobRef|.
BlobVerifier(String blobRef) {
mBlobRef = blobRef;
if (mBlobRef.startsWith(SHA1_PREFIX)) {
initForAlgorithm(SHA1_PREFIX, "SHA-1");
} else if (mBlobRef.startsWith(MD5_PREFIX)) {
initForAlgorithm(MD5_PREFIX, "MD5");
}
}
// Update the digest using the blob's bytes.
// Can be invoked repeatedly with successive chunks as the blob is being downloaded.
public void processBytes(byte[] bytes, int offset, int length) {
if (mDigester != null)
mDigester.update(bytes, offset, length);
}
// Do the blob's contents match its blobref?
// Returns true if the contents are valid or if the blob is using an unknown algorithm.
public boolean isBlobValid() {
if (mDigester == null)
return true;
final String actualDigest = Util.getHex(mDigester.digest());
return actualDigest.equals(mExpectedDigest);
}
// Helper method called by the constructor.
// Initializes |mExpectedDigest| and |mDigester| for a blobref starting with |blobRefPrefix|
// and that's using |algorithmName| (an algorithm known by MessageDigest).
private void initForAlgorithm(String blobRefPrefix, String algorithmName) {
if (!mBlobRef.startsWith(blobRefPrefix))
throw new RuntimeException("blobref " + mBlobRef + " doesn't start with " + blobRefPrefix);
mExpectedDigest = mBlobRef.substring(blobRefPrefix.length(), mBlobRef.length()).toLowerCase();
try {
mDigester = MessageDigest.getInstance(algorithmName);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -121,23 +121,37 @@ class DownloadCache {
return file; return file;
} }
// Status passed to handleDoneWritingTempFile().
public enum WriteStatus {
// |tempFile| is renamed to indicate that it's fully downloaded and the file's final location is returned.
SUCCESS,
// |tempFile| is kept on-disk and NULL is returned.
FAILURE_KEEP,
// |tempFile| is deleted.
FAILURE_DELETE,
}
// Handle the completion (either successful or not) of a download to a file returned by getTempFileForDownload(). // Handle the completion (either successful or not) of a download to a file returned by getTempFileForDownload().
// If |successfullyWritten| is true, |tempFile| will be renamed to indicate that it's fully downloaded and the // The exact behavior depends on the value of |status|.
// file's final location will be returned. public File handleDoneWritingTempFile(File tempFile, WriteStatus status) {
public File handleDoneWritingTempFile(File tempFile, boolean successfullyWritten) {
Util.assertNotMainThread(); Util.assertNotMainThread();
mLock.lock(); mLock.lock();
try { try {
while (!mIsReady) while (!mIsReady)
try { mIsReadyCondition.await(); } catch (InterruptedException e) {} try { mIsReadyCondition.await(); } catch (InterruptedException e) {}
mUsedBytes += tempFile.length();
if (!mPinnedPaths.remove(tempFile.getAbsolutePath())) if (!mPinnedPaths.remove(tempFile.getAbsolutePath()))
throw new RuntimeException("unknown temp file " + tempFile.getPath()); throw new RuntimeException("unknown temp file " + tempFile.getPath());
if (status == WriteStatus.FAILURE_DELETE) {
tempFile.delete();
} else {
mUsedBytes += tempFile.length();
}
} finally { } finally {
mLock.unlock(); mLock.unlock();
} }
if (!successfullyWritten) if (status != WriteStatus.SUCCESS)
return null; return null;
final String name = tempFile.getName(); final String name = tempFile.getName();

View File

@ -230,6 +230,10 @@ public class DownloadService extends Service {
// Temporary location in the cache where we write the blob. // Temporary location in the cache where we write the blob.
File tempFile = null; File tempFile = null;
// Set to true if the downloaded data didn't match the digest from the blobref.
// If |tempFile| exists, we need to throw it away.
boolean dataIsCorrupt = false;
try { try {
final HttpResponse response = httpClient.execute(req); final HttpResponse response = httpClient.execute(req);
final HttpEntity entity = response.getEntity(); final HttpEntity entity = response.getEntity();
@ -266,16 +270,29 @@ public class DownloadService extends Service {
throw new RuntimeException("blob " + mBlobRef + " can't be cached but has a file listener"); throw new RuntimeException("blob " + mBlobRef + " can't be cached but has a file listener");
} }
BlobVerifier verifier = new BlobVerifier(mBlobRef);
int bytesRead = 0; int bytesRead = 0;
long totalBytesWritten = 0; long totalBytesWritten = 0;
byte[] buffer = new byte[BUFFER_SIZE]; byte[] buffer = new byte[BUFFER_SIZE];
while ((bytesRead = inputStream.read(buffer)) != -1) { while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead); outputStream.write(buffer, 0, bytesRead);
totalBytesWritten += bytesRead; totalBytesWritten += bytesRead;
verifier.processBytes(buffer, 0, bytesRead);
} }
// This is unnecessary since we verify the digest, but I'm leaving it in since it'll be needed after
// support for resuming downloads is added (we don't want to delete a partial download as a result of it
// not matching the expected digest).
if (contentLength > 0 && totalBytesWritten != contentLength) { if (contentLength > 0 && totalBytesWritten != contentLength) {
Log.e(TAG, "got " + totalBytesWritten + " byte(s) for " + mBlobRef + Log.e(TAG, "got " + totalBytesWritten + " byte(s) for " + mBlobRef +
" but Content-Length header claimed " + contentLength); " but Content-Length header claimed " + contentLength);
if (totalBytesWritten > contentLength)
dataIsCorrupt = true;
return;
}
if (!verifier.isBlobValid()) {
Log.e(TAG, "blob " + mBlobRef + "'s data doesn't match expected digest");
dataIsCorrupt = true;
return; return;
} }
@ -301,7 +318,7 @@ public class DownloadService extends Service {
} }
if (tempFile != null) { if (tempFile != null) {
mBlobFile = mCache.handleDoneWritingTempFile(tempFile, true); mBlobFile = mCache.handleDoneWritingTempFile(tempFile, DownloadCache.WriteStatus.SUCCESS);
tempFile = null; tempFile = null;
} }
} catch (ClientProtocolException e) { } catch (ClientProtocolException e) {
@ -315,8 +332,13 @@ public class DownloadService extends Service {
try { outputStream.close(); } catch (IOException e) {} try { outputStream.close(); } catch (IOException e) {}
// Report failure to the cache. // Report failure to the cache.
if (tempFile != null) if (tempFile != null) {
mCache.handleDoneWritingTempFile(tempFile, false); DownloadCache.WriteStatus status =
dataIsCorrupt ?
DownloadCache.WriteStatus.FAILURE_DELETE :
DownloadCache.WriteStatus.FAILURE_KEEP;
mCache.handleDoneWritingTempFile(tempFile, status);
}
} }
} }

View File

@ -94,7 +94,7 @@ public class Util {
private static final String HEX = "0123456789abcdef"; private static final String HEX = "0123456789abcdef";
private static String getHex(byte[] raw) { public static String getHex(byte[] raw) {
if (raw == null) { if (raw == null) {
return null; return null;
} }