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