From d2a0d23380a2751d82f9d1f955a812f94a301e2a Mon Sep 17 00:00:00 2001 From: Makoto Onuki Date: Wed, 14 Apr 2010 16:49:35 -0700 Subject: [PATCH] Use consistent device-id even the device is wiped. Use hash of device id (TelephonyManager.getDeviceId()) instead of a random value. Bug 2596537 Change-Id: I22303f7287ee6e9edccec349d03f14adbd33f6f7 --- src/com/android/email/Utility.java | 45 +++++++++++++++++++ src/com/android/exchange/SyncManager.java | 26 +++++++---- .../com/android/email/UtilityUnitTests.java | 35 +++++++++++++++ 3 files changed, 97 insertions(+), 9 deletions(-) diff --git a/src/com/android/email/Utility.java b/src/com/android/email/Utility.java index 7ed2b1323..343ff8aa3 100644 --- a/src/com/android/email/Utility.java +++ b/src/com/android/email/Utility.java @@ -32,8 +32,11 @@ import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.drawable.Drawable; import android.os.AsyncTask; +import android.security.MessageDigest; +import android.telephony.TelephonyManager; import android.text.Editable; import android.util.Base64; +import android.util.Log; import android.widget.TextView; import java.io.IOException; @@ -43,6 +46,7 @@ import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; +import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.GregorianCalendar; import java.util.TimeZone; @@ -527,4 +531,45 @@ public class Utility { task.cancel(mayInterruptIfRunning); } } + + /** + * @return Device's unique ID if available. null if the device has no unique ID. + */ + public static String getConsistentDeviceId(Context context) { + final String deviceId; + try { + TelephonyManager tm = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + if (tm == null) { + return null; + } + deviceId = tm.getDeviceId(); + if (deviceId == null) { + return null; + } + } catch (Exception e) { + Log.d(Email.LOG_TAG, "Error in TelephonyManager.getDeviceId(): " + e.getMessage()); + return null; + } + final MessageDigest sha; + try { + sha = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException impossible) { + return null; + } + sha.update(Utility.toUtf8(deviceId)); + final int hash = getSmallHashFromSha1(sha.digest()); + return Integer.toString(hash); + } + + /** + * @return a non-negative integer generated from 20 byte SHA-1 hash. + */ + /* package for testing */ static int getSmallHashFromSha1(byte[] sha1) { + final int offset = sha1[19] & 0xf; // SHA1 is 20 bytes. + return ((sha1[offset] & 0x7f) << 24) + | ((sha1[offset + 1] & 0xff) << 16) + | ((sha1[offset + 2] & 0xff) << 8) + | ((sha1[offset + 3] & 0xff)); + } } diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java index 1089d9bc5..4d33f0f9f 100644 --- a/src/com/android/exchange/SyncManager.java +++ b/src/com/android/exchange/SyncManager.java @@ -20,6 +20,7 @@ package com.android.exchange; import com.android.email.AccountBackupRestore; import com.android.email.Email; import com.android.email.SecurityPolicy; +import com.android.email.Utility; import com.android.email.mail.MessagingException; import com.android.email.mail.transport.SSLUtils; import com.android.email.provider.EmailContent; @@ -968,16 +969,19 @@ public class SyncManager extends Service implements Runnable { } static public synchronized String getDeviceId(Context context) throws IOException { - SyncManager syncManager = INSTANCE; - if (sDeviceId != null) { - return sDeviceId; - } else if (syncManager == null && context == null) { + if (sDeviceId == null) { + sDeviceId = getDeviceIdInternal(context); + } + return sDeviceId; + } + + static private String getDeviceIdInternal(Context context) throws IOException { + if (INSTANCE == null && context == null) { throw new IOException("No context for getDeviceId"); } else if (context == null) { - context = syncManager; + context = INSTANCE; } - // Otherwise, we'll read the id file or create one if it's not found try { File f = context.getFileStreamPath("deviceName"); BufferedReader rdr = null; @@ -986,14 +990,18 @@ public class SyncManager extends Service implements Runnable { rdr = new BufferedReader(new FileReader(f), 128); id = rdr.readLine(); rdr.close(); - sDeviceId = id; return id; } else if (f.createNewFile()) { BufferedWriter w = new BufferedWriter(new FileWriter(f), 128); - id = "android" + System.currentTimeMillis(); + final String consistentDeviceId = Utility.getConsistentDeviceId(context); + if (consistentDeviceId != null) { + // Use different prefix from random IDs. + id = "androidc" + consistentDeviceId; + } else { + id = "android" + System.currentTimeMillis(); + } w.write(id); w.close(); - sDeviceId = id; return id; } } catch (IOException e) { diff --git a/tests/src/com/android/email/UtilityUnitTests.java b/tests/src/com/android/email/UtilityUnitTests.java index 7766f4afe..94df04ae2 100644 --- a/tests/src/com/android/email/UtilityUnitTests.java +++ b/tests/src/com/android/email/UtilityUnitTests.java @@ -20,9 +20,11 @@ import com.android.email.provider.EmailContent.Mailbox; import android.content.Context; import android.graphics.drawable.Drawable; +import android.telephony.TelephonyManager; import android.test.AndroidTestCase; import android.test.MoreAsserts; import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; import java.util.HashSet; import java.util.Set; @@ -170,4 +172,37 @@ public class UtilityUnitTests extends AndroidTestCase { assertEquals("\r\n\r\n\r\n", Utility.replaceBareLfWithCrlf("\n\n\n")); assertEquals("A\r\nB\r\nC\r\nD", Utility.replaceBareLfWithCrlf("A\nB\r\nC\nD")); } + + public void testGetConsistentDeviceId() { + TelephonyManager tm = + (TelephonyManager) getContext().getSystemService(Context.TELEPHONY_SERVICE); + if (tm == null) { + Log.w(Email.LOG_TAG, "TelephonyManager not supported. Skipping."); + return; + } + final String deviceId = Utility.getConsistentDeviceId(getContext()); + assertNotNull(deviceId); + + final String deviceId2 = Utility.getConsistentDeviceId(getContext()); + // Should be consistent. + assertEquals(deviceId, deviceId2); + } + + public void testGetSmallSha1() { + byte[] sha1 = new byte[20]; + + // White box test. Not so great, but to make sure it may detect careless mistakes... + assertEquals(0, Utility.getSmallHashFromSha1(sha1)); + + for (int i = 0; i < sha1.length; i++) { + sha1[i] = (byte) 0xFF; + } + assertEquals(Integer.MAX_VALUE, Utility.getSmallHashFromSha1(sha1)); + + // Boundary check + for (int i = 0; i < 16; i++) { + sha1[19] = (byte) i; + Utility.getSmallHashFromSha1(sha1); + } + } }