From 6950085b56947eb200d0eddfe2011b9686b690c4 Mon Sep 17 00:00:00 2001 From: Henrik Tunedal Date: Wed, 16 Mar 2011 20:45:25 +0100 Subject: [PATCH] Support SHA-256 in addition to MD5 The client will now check the SHA-256 hash of an APK if it's provided in the index but otherwise falls back to the MD5 hash. --- src/org/fdroid/fdroid/AppDetails.java | 26 ++--- src/org/fdroid/fdroid/DB.java | 14 ++- src/org/fdroid/fdroid/Hasher.java | 115 ++++++++++++++++++++++ src/org/fdroid/fdroid/Md5Handler.java | 47 --------- src/org/fdroid/fdroid/RepoXMLHandler.java | 16 ++- 5 files changed, 147 insertions(+), 71 deletions(-) create mode 100644 src/org/fdroid/fdroid/Hasher.java delete mode 100644 src/org/fdroid/fdroid/Md5Handler.java diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index ccc6aa4bf..443561759 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -22,9 +22,7 @@ import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; -import java.math.BigInteger; import java.net.URL; -import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; @@ -238,16 +236,8 @@ public class AppDetails extends ListActivity { PackageInfo pi = pm.getPackageInfo(appid, PackageManager.GET_SIGNATURES); mInstalledSignature = pi.signatures[0]; - MessageDigest md; - md = MessageDigest.getInstance("MD5"); - byte[] md5sum = new byte[32]; - md.update(mInstalledSignature.toCharsString().getBytes()); - md5sum = md.digest(); - BigInteger bigInt = new BigInteger(1, md5sum); - String md5hash = bigInt.toString(16); - while (md5hash.length() < 32) - md5hash = "0" + md5hash; - mInstalledSigID = md5hash; + Hasher hash = new Hasher("MD5", mInstalledSignature); + mInstalledSigID = hash.getHash(); } catch (NameNotFoundException e) { Log.d("FDroid", "Failed to get installed signature"); } catch (NoSuchAlgorithmException e) { @@ -443,9 +433,8 @@ public class AppDetails extends ListActivity { f = new File(localfile); if (f.exists()) { // We do - if its hash matches, we'll use it... - Md5Handler hash = new Md5Handler(); - String calcedhash = hash.md5Calc(f); - if (curapk.hash.equalsIgnoreCase(calcedhash)) { + Hasher hash = new Hasher(curapk.hashType, f); + if (hash.match(curapk.hash)) { apk_file = localfile; Log.d("FDroid", "Using cached apk at " + localfile); Message msg = new Message(); @@ -515,16 +504,15 @@ public class AppDetails extends ListActivity { msg.sendToTarget(); return; } - Md5Handler hash = new Md5Handler(); - String calcedhash = hash.md5Calc(f); - if (curapk.hash.equalsIgnoreCase(calcedhash)) { + Hasher hash = new Hasher(curapk.hashType, f); + if (hash.match(curapk.hash)) { apk_file = localfile; } else { msg = new Message(); msg.obj = getString(R.string.corrupt_download); download_error_handler.sendMessage(msg); Log.d("FDroid", "Downloaded file hash of " - + calcedhash + " did not match repo's " + + hash.getHash() + " did not match repo's " + curapk.hash); // No point keeping a bad file, whether we're // caching or diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java index c8f48228b..86e850bf4 100644 --- a/src/org/fdroid/fdroid/DB.java +++ b/src/org/fdroid/fdroid/DB.java @@ -171,6 +171,7 @@ public class DB { public int size; // Size in bytes - 0 means we don't know! public String server; public String hash; + public String hashType; public int minSdkVersion; // 0 if unknown public CommaSeparatedList permissions; // null if empty or unknown public CommaSeparatedList features; // null if empty or unknown @@ -281,9 +282,9 @@ public class DB { // private static final String[][] DB_UPGRADES = { - // Version 2... + // Version 2... { "alter table " + TABLE_APP + " add marketVersion text", - "alter table " + TABLE_APP + " add marketVercode integer" }, + "alter table " + TABLE_APP + " add marketVercode integer" }, // Version 3... { "alter table " + TABLE_APK + " add apkSource text" }, @@ -312,7 +313,11 @@ public class DB { "alter table " + TABLE_APK + " add features string" }, // Version 11... - { "alter table " + TABLE_APP + " add requirements string" }}; + { "alter table " + TABLE_APP + " add requirements string" }, + + // Version 12... + { "alter table " + TABLE_APK + " add hashType string", + "update " + TABLE_APK + " set hashType = 'MD5'" }}; private class DBHelper extends SQLiteOpenHelper { @@ -504,6 +509,8 @@ public class DB { apk.vercode = c2.getInt(c2.getColumnIndex("vercode")); apk.server = c2.getString(c2.getColumnIndex("server")); apk.hash = c2.getString(c2.getColumnIndex("hash")); + apk.hashType = c2.getString(c2 + .getColumnIndex("hashType")); apk.sig = c2.getString(c2.getColumnIndex("sig")); apk.srcname = c2.getString(c2.getColumnIndex("srcname")); apk.size = c2.getInt(c2.getColumnIndex("size")); @@ -795,6 +802,7 @@ public class DB { values.put("vercode", upapk.vercode); values.put("server", upapk.server); values.put("hash", upapk.hash); + values.put("hashType", upapk.hashType); values.put("sig", upapk.sig); values.put("srcname", upapk.srcname); values.put("size", upapk.size); diff --git a/src/org/fdroid/fdroid/Hasher.java b/src/org/fdroid/fdroid/Hasher.java new file mode 100644 index 000000000..81d2eb686 --- /dev/null +++ b/src/org/fdroid/fdroid/Hasher.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2010-2011 Ciaran Gultnieks + * Copyright (C) 2011 Henrik Tunedal + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +package org.fdroid.fdroid; + +import java.io.File; +import java.io.FileInputStream; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import android.content.pm.Signature; + +public class Hasher { + + private MessageDigest digest; + private File f; + private Signature s; + private String hashCache; + + public Hasher(String type, File f) throws NoSuchAlgorithmException { + init(type); + this.f = f; + } + + public Hasher(String type, Signature s) throws NoSuchAlgorithmException { + init(type); + this.s = s; + } + + private void init(String type) throws NoSuchAlgorithmException { + try { + digest = MessageDigest.getInstance(type); + } catch (Exception e) { + throw new NoSuchAlgorithmException(e); + } + } + + // Calculate hash (as lowercase hexadecimal string) for the file + // specified in the constructor. This will return a cached value + // on subsequent invocations, unless reset() in called. Returns + // the empty string on failure. + public String getHash() { + if (hashCache != null) + return hashCache; + + String hash = null; + byte[] buffer = new byte[1024]; + int read = 0; + + try { + InputStream is; + if (s == null) + is = new FileInputStream(f); + else + is = new ByteArrayInputStream(s.toCharsString().getBytes()); + while ((read = is.read(buffer)) > 0) { + digest.update(buffer, 0, read); + } + byte[] checksum = digest.digest(); + BigInteger bigInt = new BigInteger(1, checksum); + hash = bigInt.toString(16).toLowerCase(); + + // Add leading zeros + int targetLength = digest.getDigestLength() * 2; + if (hash.length() < targetLength) { + StringBuilder sb = new StringBuilder(targetLength); + for (int i = hash.length(); i < targetLength; i++) { + sb.append('0'); + } + sb.append(hash); + hash = sb.toString(); + } + + } catch (Exception e) { + return hashCache = ""; + } + + return hashCache = hash; + } + + // Compare the calculated hash to another string, ignoring case, + // returning true if they are equal. The empty string and null are + // considered non-matching. + public boolean match(String otherHash) { + if (hashCache == null) getHash(); + if (otherHash == null || hashCache.equals("")) + return false; + return hashCache.equals(otherHash.toLowerCase()); + } + + public void reset() { + hashCache = null; + digest.reset(); + } + +} diff --git a/src/org/fdroid/fdroid/Md5Handler.java b/src/org/fdroid/fdroid/Md5Handler.java deleted file mode 100644 index 669f70cd1..000000000 --- a/src/org/fdroid/fdroid/Md5Handler.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.fdroid.fdroid; - -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.math.BigInteger; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -public class Md5Handler { - - private MessageDigest digest; - - public Md5Handler() { - try { - digest = MessageDigest.getInstance("MD5"); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - } - } - - public String md5Calc(File f) { - String md5hash = null; - byte[] buffer = new byte[1024]; - int read = 0; - - try { - InputStream is = new FileInputStream(f); - while ((read = is.read(buffer)) > 0) { - digest.update(buffer, 0, read); - } - byte[] md5sum = digest.digest(); - BigInteger bigInt = new BigInteger(1, md5sum); - md5hash = bigInt.toString(16); - - // We need 32 hex digits - add leading zeros in an inefficient and - // brute-force manner... - while (md5hash.length() < 32) - md5hash = "0" + md5hash; - - } catch (Exception e) { - } - - return md5hash; - } - -} diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java b/src/org/fdroid/fdroid/RepoXMLHandler.java index a081ca343..384848274 100644 --- a/src/org/fdroid/fdroid/RepoXMLHandler.java +++ b/src/org/fdroid/fdroid/RepoXMLHandler.java @@ -56,7 +56,8 @@ public class RepoXMLHandler extends DefaultHandler { private DB.Apk curapk = null; private String curchars = null; - public String pubkey; + private String pubkey; + private String hashType; public RepoXMLHandler(String srv, DB db) { mserver = srv; @@ -110,7 +111,15 @@ public class RepoXMLHandler extends DefaultHandler { curapk.size = 0; } } else if (curel.equals("hash")) { - curapk.hash = str; + if (hashType == null || hashType.equals("md5")) { + if (curapk.hash == null) { + curapk.hash = str; + curapk.hashType = "MD5"; + } + } else if (hashType.equals("sha256")) { + curapk.hash = str; + curapk.hashType = "SHA-256"; + } } else if (curel.equals("sig")) { curapk.sig = str; } else if (curel.equals("srcname")) { @@ -186,6 +195,9 @@ public class RepoXMLHandler extends DefaultHandler { curapk = new DB.Apk(); curapk.id = curapp.id; curapk.server = mserver; + hashType = null; + } else if (localName == "hash" && curapk != null) { + hashType = attributes.getValue("", "type"); } curchars = null; }