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
5package org.chromium.chrome.browser;
6
7import android.content.Context;
8import android.os.AsyncTask;
9import android.util.Log;
10
11import java.io.File;
12import java.io.FileInputStream;
13import java.io.FileOutputStream;
14import java.io.IOException;
15import java.security.GeneralSecurityException;
16import java.security.SecureRandom;
17import java.util.concurrent.Callable;
18import java.util.concurrent.ExecutionException;
19import java.util.concurrent.FutureTask;
20
21import javax.crypto.KeyGenerator;
22import javax.crypto.Mac;
23import javax.crypto.SecretKey;
24import 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 */
35public 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 (IOException 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        FileOutputStream output = null;
128        if (MAC_KEY_BYTE_COUNT != keyBytes.length) {
129            Log.e(TAG, "writeKeyToFile got key encoded bytes length " + keyBytes.length +
130                       "; expected " + MAC_KEY_BYTE_COUNT);
131            return false;
132        }
133
134        try {
135            output = new FileOutputStream(file);
136            output.write(keyBytes);
137            return true;
138        } catch (Exception e) {
139            Log.e(TAG, "Could not write key to '" + file + "': " + e);
140            return false;
141        } finally {
142            try {
143                if (output != null) {
144                    output.close();
145                }
146            } catch (IOException e) {
147                Log.e(TAG, "Could not close key output stream '" + file + "': " + e);
148            }
149        }
150    }
151
152    private static SecretKey getKey(Context context) {
153        synchronized (sLock) {
154            if (sKey == null) {
155                SecretKey key = readKeyFromFile(context, MAC_KEY_BASENAME, MAC_ALGORITHM_NAME);
156                if (key != null) {
157                    sKey = key;
158                    return sKey;
159                }
160
161                triggerMacKeyGeneration();
162                try {
163                    sKey = sMacKeyGenerator.get();
164                    sMacKeyGenerator = null;
165                    if (!writeKeyToFile(context, MAC_KEY_BASENAME, sKey)) {
166                        sKey = null;
167                        return null;
168                    }
169                    return sKey;
170                } catch (InterruptedException e) {
171                    throw new RuntimeException(e);
172                } catch (ExecutionException e) {
173                    throw new RuntimeException(e);
174                }
175            }
176            return sKey;
177        }
178    }
179
180    /**
181     * Generates the authentication encryption key in a background thread (if necessary).
182     */
183    private static void triggerMacKeyGeneration() {
184        synchronized (sLock) {
185            if (sKey != null || sMacKeyGenerator != null) {
186                return;
187            }
188
189            sMacKeyGenerator = new FutureTask<SecretKey>(new Callable<SecretKey>() {
190                @Override
191                public SecretKey call() throws Exception {
192                    KeyGenerator generator = KeyGenerator.getInstance(MAC_ALGORITHM_NAME);
193                    SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
194
195                    // Versions of SecureRandom from Android <= 4.3 do not seed themselves as
196                    // securely as possible. This workaround should suffice until the fixed version
197                    // is deployed to all users. getRandomBytes, which reads from /dev/urandom,
198                    // which is as good as the platform can get.
199                    //
200                    // TODO(palmer): Consider getting rid of this once the updated platform has
201                    // shipped to everyone. Alternately, leave this in as a defense against other
202                    // bugs in SecureRandom.
203                    byte[] seed = getRandomBytes(MAC_KEY_BYTE_COUNT);
204                    if (seed == null) {
205                        return null;
206                    }
207                    random.setSeed(seed);
208                    generator.init(MAC_KEY_BYTE_COUNT * 8, random);
209                    return generator.generateKey();
210                }
211            });
212            AsyncTask.THREAD_POOL_EXECUTOR.execute(sMacKeyGenerator);
213        }
214    }
215
216    private static byte[] getRandomBytes(int count) {
217        FileInputStream fis = null;
218        try {
219            fis = new FileInputStream("/dev/urandom");
220            byte[] bytes = new byte[count];
221            if (bytes.length != fis.read(bytes)) {
222                return null;
223            }
224            return bytes;
225        } catch (Throwable t) {
226            // This causes the ultimate caller, i.e. getMac, to fail.
227            return null;
228        } finally {
229            try {
230                if (fis != null) {
231                    fis.close();
232                }
233            } catch (IOException e) {
234                // Nothing we can do.
235            }
236        }
237    }
238
239    /**
240     * @return A Mac, or null if it is not possible to instantiate one.
241     */
242    private static Mac getMac(Context context) {
243        try {
244            SecretKey key = getKey(context);
245            if (key == null) {
246                // getKey should have invoked triggerMacKeyGeneration, which should have set the
247                // random seed and generated a key from it. If not, there is a problem with the
248                // random number generator, and we must not claim that authentication can work.
249                return null;
250            }
251            Mac mac = Mac.getInstance(MAC_ALGORITHM_NAME);
252            mac.init(key);
253            return mac;
254        } catch (GeneralSecurityException e) {
255            Log.w(TAG, "Error in creating MAC instance", e);
256            return null;
257        }
258    }
259}
260