- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / android / java / src / org / chromium / chrome / browser / WebappAuthenticator.java
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package org.chromium.chrome.browser;
6
7 import android.content.Context;
8 import android.os.AsyncTask;
9 import android.util.Log;
10
11 import java.io.File;
12 import java.io.FileInputStream;
13 import java.io.FileOutputStream;
14 import java.io.IOException;
15 import java.security.GeneralSecurityException;
16 import java.security.SecureRandom;
17 import java.util.concurrent.Callable;
18 import java.util.concurrent.ExecutionException;
19 import java.util.concurrent.FutureTask;
20
21 import javax.crypto.KeyGenerator;
22 import javax.crypto.Mac;
23 import javax.crypto.SecretKey;
24 import javax.crypto.spec.SecretKeySpec;
25
26 /**
27  * Authenticate the source of Intents to launch web apps (see e.g. {@link #FullScreenActivity}).
28  *
29  * Chrome does not keep a store of valid URLs for installed web apps (because it cannot know when
30  * any have been uninstalled). Therefore, upon installation, it tells the Launcher a message
31  * authentication code (MAC) along with the URL for the web app, and then Chrome can verify the MAC
32  * when starting e.g. {@link #FullScreenActivity}. Chrome can thus distinguish between legitimate,
33  * installed web apps and arbitrary other URLs.
34  */
35 public class WebappAuthenticator {
36     private static final String TAG = "WebappAuthenticator";
37     private static final String MAC_ALGORITHM_NAME = "HmacSHA256";
38     private static final String MAC_KEY_BASENAME = "webapp-authenticator";
39     private static final int MAC_KEY_BYTE_COUNT = 32;
40     private static final Object sLock = new Object();
41
42     private static FutureTask<SecretKey> sMacKeyGenerator;
43     private static SecretKey sKey = null;
44
45     /**
46      * @see #getMacForUrl
47      *
48      * @param url The URL to validate.
49      * @param mac The bytes of a previously-calculated MAC.
50      *
51      * @return true if the MAC is a valid MAC for the URL, false otherwise.
52      */
53     public static boolean isUrlValid(Context context, String url, byte[] mac) {
54         byte[] goodMac = getMacForUrl(context, url);
55         if (goodMac == null) {
56             return false;
57         }
58         return constantTimeAreArraysEqual(goodMac, mac);
59     }
60
61     /**
62      * @see #isUrlValid
63      *
64      * @param url A URL for which to calculate a MAC.
65      *
66      * @return The bytes of a MAC for the URL, or null if a secure MAC was not available.
67      */
68     public static byte[] getMacForUrl(Context context, String url) {
69         Mac mac = getMac(context);
70         if (mac == null) {
71             return null;
72         }
73         return mac.doFinal(url.getBytes());
74     }
75
76     // TODO(palmer): Put this method, and as much of this class as possible, in a utility class.
77     private static boolean constantTimeAreArraysEqual(byte[] a, byte[] b) {
78         if (a.length != b.length) {
79             return false;
80         }
81
82         int result = 0;
83         for (int i = 0; i < a.length; i++) {
84             result |= a[i] ^ b[i];
85         }
86         return result == 0;
87     }
88
89     private static SecretKey readKeyFromFile(
90             Context context, String basename, String algorithmName) {
91         FileInputStream input = null;
92         File file = context.getFileStreamPath(basename);
93         try {
94             if (file.length() != MAC_KEY_BYTE_COUNT) {
95                 Log.w(TAG, "Could not read key from '" + file + "': invalid file contents");
96                 return null;
97             }
98
99             byte[] keyBytes = new byte[MAC_KEY_BYTE_COUNT];
100             input = new FileInputStream(file);
101             if (MAC_KEY_BYTE_COUNT != input.read(keyBytes)) {
102                 return null;
103             }
104
105             try {
106                 return new SecretKeySpec(keyBytes, algorithmName);
107             } catch (IllegalArgumentException e) {
108                 return null;
109             }
110         } catch (Exception e) {
111             Log.w(TAG, "Could not read key from '" + file + "': " + e);
112             return null;
113         } finally {
114             try {
115                 if (input != null) {
116                     input.close();
117                 }
118             } catch (Exception e) {
119                 Log.e(TAG, "Could not close key input stream '" + file + "': " + e);
120             }
121         }
122     }
123
124     private static boolean writeKeyToFile(Context context, String basename, SecretKey key) {
125         File file = context.getFileStreamPath(basename);
126         byte[] keyBytes = key.getEncoded();
127         if (MAC_KEY_BYTE_COUNT != keyBytes.length) {
128             Log.e(TAG, "writeKeyToFile got key encoded bytes length " + keyBytes.length +
129                        "; expected " + MAC_KEY_BYTE_COUNT);
130             return false;
131         }
132
133         try {
134             FileOutputStream output = new FileOutputStream(file);
135             output.write(keyBytes);
136             output.close();
137             return true;
138         } catch (Exception e) {
139             Log.e(TAG, "Could not write key to '" + file + "': " + e);
140             return false;
141         }
142     }
143
144     private static SecretKey getKey(Context context) {
145         synchronized (sLock) {
146             if (sKey == null) {
147                 SecretKey key = readKeyFromFile(context, MAC_KEY_BASENAME, MAC_ALGORITHM_NAME);
148                 if (key != null) {
149                     sKey = key;
150                     return sKey;
151                 }
152
153                 triggerMacKeyGeneration();
154                 try {
155                     sKey = sMacKeyGenerator.get();
156                     sMacKeyGenerator = null;
157                     if (!writeKeyToFile(context, MAC_KEY_BASENAME, sKey)) {
158                         sKey = null;
159                         return null;
160                     }
161                     return sKey;
162                 } catch (InterruptedException e) {
163                     throw new RuntimeException(e);
164                 } catch (ExecutionException e) {
165                     throw new RuntimeException(e);
166                 }
167             }
168             return sKey;
169         }
170     }
171
172     /**
173      * Generates the authentication encryption key in a background thread (if necessary).
174      */
175     private static void triggerMacKeyGeneration() {
176         synchronized (sLock) {
177             if (sKey != null || sMacKeyGenerator != null) {
178                 return;
179             }
180
181             sMacKeyGenerator = new FutureTask<SecretKey>(new Callable<SecretKey>() {
182                 @Override
183                 public SecretKey call() throws Exception {
184                     KeyGenerator generator = KeyGenerator.getInstance(MAC_ALGORITHM_NAME);
185                     SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
186
187                     // Versions of SecureRandom from Android <= 4.3 do not seed themselves as
188                     // securely as possible. This workaround should suffice until the fixed version
189                     // is deployed to all users. getRandomBytes, which reads from /dev/urandom,
190                     // which is as good as the platform can get.
191                     //
192                     // TODO(palmer): Consider getting rid of this once the updated platform has
193                     // shipped to everyone. Alternately, leave this in as a defense against other
194                     // bugs in SecureRandom.
195                     byte[] seed = getRandomBytes(MAC_KEY_BYTE_COUNT);
196                     if (seed == null) {
197                         return null;
198                     }
199                     random.setSeed(seed);
200                     generator.init(MAC_KEY_BYTE_COUNT * 8, random);
201                     return generator.generateKey();
202                 }
203             });
204             AsyncTask.THREAD_POOL_EXECUTOR.execute(sMacKeyGenerator);
205         }
206     }
207
208     private static byte[] getRandomBytes(int count) {
209         FileInputStream fis = null;
210         try {
211             fis = new FileInputStream("/dev/urandom");
212             byte[] bytes = new byte[count];
213             if (bytes.length != fis.read(bytes)) {
214                 return null;
215             }
216             return bytes;
217         } catch (Throwable t) {
218             // This causes the ultimate caller, i.e. getMac, to fail.
219             return null;
220         } finally {
221             try {
222                 if (fis != null) {
223                     fis.close();
224                 }
225             } catch (IOException e) {
226                 // Nothing we can do.
227             }
228         }
229     }
230
231     /**
232      * @return A Mac, or null if it is not possible to instantiate one.
233      */
234     private static Mac getMac(Context context) {
235         try {
236             SecretKey key = getKey(context);
237             if (key == null) {
238                 // getKey should have invoked triggerMacKeyGeneration, which should have set the
239                 // random seed and generated a key from it. If not, there is a problem with the
240                 // random number generator, and we must not claim that authentication can work.
241                 return null;
242             }
243             Mac mac = Mac.getInstance(MAC_ALGORITHM_NAME);
244             mac.init(key);
245             return mac;
246         } catch (GeneralSecurityException e) {
247             Log.w(TAG, "Error in creating MAC instance", e);
248             return null;
249         }
250     }
251 }