CTLogStoreImpl.java revision bea3563621a1b743812e387b3783b070fb9f9fbb
1f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar/*
2f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar * Copyright (C) 2015 The Android Open Source Project
3f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar *
4f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar * Licensed under the Apache License, Version 2.0 (the "License");
5f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar * you may not use this file except in compliance with the License.
6f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar * You may obtain a copy of the License at
7f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar *
8f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar *      http://www.apache.org/licenses/LICENSE-2.0
9f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar *
10f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar * Unless required by applicable law or agreed to in writing, software
11f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar * distributed under the License is distributed on an "AS IS" BASIS,
12f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar * See the License for the specific language governing permissions and
14f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar * limitations under the License.
15f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar */
16f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar
17f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietarpackage org.conscrypt.ct;
18f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar
192693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietarimport java.io.File;
202693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietarimport java.io.FileInputStream;
212693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietarimport java.io.FileNotFoundException;
22d0687b8c0d5aa6e88853fff774afd24ec331964cKenny Rootimport java.io.InputStream;
232693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietarimport java.io.StringBufferInputStream;
242693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietarimport java.nio.ByteBuffer;
252693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietarimport java.security.InvalidKeyException;
26f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietarimport java.security.NoSuchAlgorithmException;
27d0687b8c0d5aa6e88853fff774afd24ec331964cKenny Rootimport java.security.PublicKey;
28f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietarimport java.util.Arrays;
292693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietarimport java.util.Collections;
30bea3563621a1b743812e387b3783b070fb9f9fbbKenny Rootimport java.util.HashMap;
312693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietarimport java.util.HashSet;
322693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietarimport java.util.Scanner;
33d0687b8c0d5aa6e88853fff774afd24ec331964cKenny Rootimport java.util.Set;
34d0687b8c0d5aa6e88853fff774afd24ec331964cKenny Rootimport org.conscrypt.NativeCrypto;
35d0687b8c0d5aa6e88853fff774afd24ec331964cKenny Rootimport org.conscrypt.OpenSSLKey;
36f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar
37f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietarpublic class CTLogStoreImpl implements CTLogStore {
382693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    /**
392693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar     * Thrown when parsing of a log file fails.
402693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar     */
412693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    public static class InvalidLogFileException extends Exception {
422693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        public InvalidLogFileException() {
432693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        }
442693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
452693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        public InvalidLogFileException(String message) {
462693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            super(message);
472693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        }
482693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
492693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        public InvalidLogFileException(String message, Throwable cause) {
502693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            super(message, cause);
512693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        }
522693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
532693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        public InvalidLogFileException(Throwable cause) {
542693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            super(cause);
552693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        }
562693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    }
572693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
582693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    private static final File defaultUserLogDir;
592693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    private static final File defaultSystemLogDir;
602693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    // Lazy loaded by CTLogStoreImpl()
612693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    private static volatile CTLogInfo[] defaultFallbackLogs = null;
622693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    static {
632693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        String ANDROID_DATA = System.getenv("ANDROID_DATA");
642693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        String ANDROID_ROOT = System.getenv("ANDROID_ROOT");
65518ca8c6c8d88daa55d4ea0d0ca83a4654bd1071Chad Brubaker        defaultUserLogDir = new File(ANDROID_DATA + "/misc/keychain/trusted_ct_logs/current/");
662693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        defaultSystemLogDir = new File(ANDROID_ROOT + "/etc/security/ct_known_logs/");
672693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    }
682693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
69ca253dc11efbb040c39eaa49ef9e06c344d7e9f2Kenny Root    private File userLogDir;
70ca253dc11efbb040c39eaa49ef9e06c344d7e9f2Kenny Root    private File systemLogDir;
71ca253dc11efbb040c39eaa49ef9e06c344d7e9f2Kenny Root    private CTLogInfo[] fallbackLogs;
722693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
73bea3563621a1b743812e387b3783b070fb9f9fbbKenny Root    private HashMap<ByteBuffer, CTLogInfo> logCache = new HashMap<>();
74bea3563621a1b743812e387b3783b070fb9f9fbbKenny Root    private Set<ByteBuffer> missingLogCache = Collections.synchronizedSet(new HashSet<ByteBuffer>());
752693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
762693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    public CTLogStoreImpl() {
772693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        this(defaultUserLogDir,
782693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar             defaultSystemLogDir,
792693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar             getDefaultFallbackLogs());
802693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    }
812693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
822693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    public CTLogStoreImpl(File userLogDir, File systemLogDir, CTLogInfo[] fallbackLogs) {
832693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        this.userLogDir = userLogDir;
842693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        this.systemLogDir = systemLogDir;
852693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        this.fallbackLogs = fallbackLogs;
862693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    }
87f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar
88f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar    @Override
89f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar    public CTLogInfo getKnownLog(byte[] logId) {
902693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        ByteBuffer buf = ByteBuffer.wrap(logId);
912693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        CTLogInfo log = logCache.get(buf);
922693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        if (log != null) {
932693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            return log;
942693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        }
952693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        if (missingLogCache.contains(buf)) {
962693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            return null;
972693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        }
982693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
992693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        log = findKnownLog(logId);
1002693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        if (log != null) {
1012693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            logCache.put(buf, log);
1022693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        } else {
1032693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            missingLogCache.add(buf);
104f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar        }
1052693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
1062693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        return log;
1072693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    }
1082693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
1092693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    private CTLogInfo findKnownLog(byte[] logId) {
1102693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        String filename = hexEncode(logId);
1112693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        try {
1122693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            return loadLog(new File(userLogDir, filename));
1132693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        } catch (InvalidLogFileException e) {
1142693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            return null;
1152693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        } catch (FileNotFoundException e) {}
1162693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
1172693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        try {
1182693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            return loadLog(new File(systemLogDir, filename));
1192693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        } catch (InvalidLogFileException e) {
1202693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            return null;
1212693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        } catch (FileNotFoundException e) {}
1222693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
123518ca8c6c8d88daa55d4ea0d0ca83a4654bd1071Chad Brubaker        // If the updateable logs dont exist then use the fallback logs.
124518ca8c6c8d88daa55d4ea0d0ca83a4654bd1071Chad Brubaker        if (!userLogDir.exists()) {
125518ca8c6c8d88daa55d4ea0d0ca83a4654bd1071Chad Brubaker            for (CTLogInfo log: fallbackLogs) {
126518ca8c6c8d88daa55d4ea0d0ca83a4654bd1071Chad Brubaker                if (Arrays.equals(logId, log.getID())) {
127518ca8c6c8d88daa55d4ea0d0ca83a4654bd1071Chad Brubaker                    return log;
128518ca8c6c8d88daa55d4ea0d0ca83a4654bd1071Chad Brubaker                }
129f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar            }
130f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar        }
131f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar        return null;
132f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar    }
133f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar
1342693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    public static CTLogInfo[] getDefaultFallbackLogs() {
1352693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        CTLogInfo[] result = defaultFallbackLogs;
1362693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        if (result == null) {
1372693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            // single-check idiom
1382693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            defaultFallbackLogs = result = createDefaultFallbackLogs();
1392693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        }
1402693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        return result;
1412693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    }
1422693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
1432693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    private static CTLogInfo[] createDefaultFallbackLogs() {
1444dc3e45f010e805b5b7fb9ac2e40cc05244209b0Kenny Root        CTLogInfo[] logs = new CTLogInfo[KnownLogs.LOG_COUNT];
145f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar        for (int i = 0; i < KnownLogs.LOG_COUNT; i++) {
146f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar            try {
147f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar                PublicKey key = new OpenSSLKey(NativeCrypto.d2i_PUBKEY(KnownLogs.LOG_KEYS[i]))
148f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar                                .getPublicKey();
149f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar
150f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar                logs[i] = new CTLogInfo(key,
151f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar                                        KnownLogs.LOG_DESCRIPTIONS[i],
152f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar                                        KnownLogs.LOG_URLS[i]);
153f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar            } catch (NoSuchAlgorithmException e) {
154f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar                throw new RuntimeException(e);
155f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar            }
156f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar        }
1572693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
1582693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        defaultFallbackLogs = logs;
159f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar        return logs;
160f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar    }
1612693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
1622693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    /**
1632693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar     * Load a CTLogInfo from a file.
1642693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar     * @throws FileNotFoundException if the file does not exist
1652693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar     * @throws InvalidLogFileException if the file could not be parsed properly
1662693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar     * @return a CTLogInfo or null if the file is empty
1672693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar     */
1682693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    public static CTLogInfo loadLog(File file) throws FileNotFoundException,
1692693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar                                                      InvalidLogFileException {
1702693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        return loadLog(new FileInputStream(file));
1712693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    }
1722693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
1732693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    /**
1742693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar     * Load a CTLogInfo from a textual representation.
1752693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar     * @throws InvalidLogFileException if the input could not be parsed properly
1762693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar     * @return a CTLogInfo or null if the input is empty
1772693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar     */
1782693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    public static CTLogInfo loadLog(InputStream input) throws InvalidLogFileException {
179518ca8c6c8d88daa55d4ea0d0ca83a4654bd1071Chad Brubaker        Scanner scan = new Scanner(input, "UTF-8").useDelimiter("\n");
1802693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        // If the scanner can't even read one token then the file must be empty/blank
1812693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        if (!scan.hasNext()) {
1822693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            return null;
1832693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        }
1842693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
1852693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        String description = null, url = null, key = null;
1862693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        while (scan.hasNext()) {
1872693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            String[] parts = scan.next().split(":", 2);
1882693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            if (parts.length < 2) {
1892693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar                continue;
1902693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            }
1912693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
1922693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            String name = parts[0];
1932693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            String value = parts[1];
1942693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            switch (name) {
1952693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar                case "description": description = value; break;
1962693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar                case "url": url = value; break;
1972693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar                case "key": key = value; break;
1982693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            }
1992693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        }
2002693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
2012693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        if (description == null || url == null || key == null) {
2022693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            throw new InvalidLogFileException("Missing one of 'description', 'url' or 'key'");
2032693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        }
2042693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
2052693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        PublicKey pubkey;
2062693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        try {
2072693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            pubkey = OpenSSLKey.fromPublicKeyPemInputStream(new StringBufferInputStream(
2082693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar                        "-----BEGIN PUBLIC KEY-----\n" +
2092693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar                        key + "\n" +
2102693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar                        "-----END PUBLIC KEY-----")).getPublicKey();
2112693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        } catch (InvalidKeyException e) {
2122693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            throw new InvalidLogFileException(e);
2132693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        } catch (NoSuchAlgorithmException e) {
2142693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            throw new InvalidLogFileException(e);
2152693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        }
2162693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
2172693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        return new CTLogInfo(pubkey, description, url);
2182693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    }
2192693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
2202693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    private final static char[] HEX_DIGITS = new char[] {
2212693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        '0', '1', '2', '3', '4', '5', '6', '7',
2222693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
2232693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    };
2242693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar
2252693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    private static String hexEncode(byte[] data) {
2262693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        StringBuffer sb = new StringBuffer(data.length * 2);
2272693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        for (byte b: data) {
2282693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            sb.append(HEX_DIGITS[(b >> 4) & 0x0f]);
2292693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar            sb.append(HEX_DIGITS[b & 0x0f]);
2302693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        }
2312693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar        return sb.toString();
2322693d203bf976ff2c5167df13e382d4e10024ad7Paul Lietar    }
233f714bf65e491155e7837ddb3242e3ee6be173943Paul Lietar}
234