Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | package org.mozilla.gecko.browserid; |
michael@0 | 6 | |
michael@0 | 7 | import java.io.IOException; |
michael@0 | 8 | import java.io.UnsupportedEncodingException; |
michael@0 | 9 | import java.security.GeneralSecurityException; |
michael@0 | 10 | import java.util.ArrayList; |
michael@0 | 11 | import java.util.Map; |
michael@0 | 12 | import java.util.TreeMap; |
michael@0 | 13 | |
michael@0 | 14 | import org.json.simple.JSONObject; |
michael@0 | 15 | import org.json.simple.parser.ParseException; |
michael@0 | 16 | import org.mozilla.apache.commons.codec.binary.Base64; |
michael@0 | 17 | import org.mozilla.apache.commons.codec.binary.StringUtils; |
michael@0 | 18 | import org.mozilla.gecko.sync.ExtendedJSONObject; |
michael@0 | 19 | import org.mozilla.gecko.sync.NonObjectJSONException; |
michael@0 | 20 | import org.mozilla.gecko.sync.Utils; |
michael@0 | 21 | |
michael@0 | 22 | /** |
michael@0 | 23 | * Encode and decode JSON Web Tokens. |
michael@0 | 24 | * <p> |
michael@0 | 25 | * Reverse-engineered from the Node.js jwcrypto library at |
michael@0 | 26 | * <a href="https://github.com/mozilla/jwcrypto">https://github.com/mozilla/jwcrypto</a> |
michael@0 | 27 | * and informed by the informal draft standard "JSON Web Token (JWT)" at |
michael@0 | 28 | * <a href="http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html">http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html</a>. |
michael@0 | 29 | */ |
michael@0 | 30 | public class JSONWebTokenUtils { |
michael@0 | 31 | public static final long DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS = 60 * 60 * 1000; |
michael@0 | 32 | public static final long DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS = 60 * 60 * 1000; |
michael@0 | 33 | public static final long DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS = 9999999999999L; |
michael@0 | 34 | public static final String DEFAULT_CERTIFICATE_ISSUER = "127.0.0.1"; |
michael@0 | 35 | public static final String DEFAULT_ASSERTION_ISSUER = "127.0.0.1"; |
michael@0 | 36 | |
michael@0 | 37 | public static String encode(String payload, SigningPrivateKey privateKey) throws UnsupportedEncodingException, GeneralSecurityException { |
michael@0 | 38 | return encode(payload, privateKey, null); |
michael@0 | 39 | } |
michael@0 | 40 | |
michael@0 | 41 | protected static String encode(String payload, SigningPrivateKey privateKey, Map<String, Object> headerFields) throws UnsupportedEncodingException, GeneralSecurityException { |
michael@0 | 42 | ExtendedJSONObject header = new ExtendedJSONObject(); |
michael@0 | 43 | if (headerFields != null) { |
michael@0 | 44 | header.putAll(headerFields); |
michael@0 | 45 | } |
michael@0 | 46 | header.put("alg", privateKey.getAlgorithm()); |
michael@0 | 47 | String encodedHeader = Base64.encodeBase64URLSafeString(header.toJSONString().getBytes("UTF-8")); |
michael@0 | 48 | String encodedPayload = Base64.encodeBase64URLSafeString(payload.getBytes("UTF-8")); |
michael@0 | 49 | ArrayList<String> segments = new ArrayList<String>(); |
michael@0 | 50 | segments.add(encodedHeader); |
michael@0 | 51 | segments.add(encodedPayload); |
michael@0 | 52 | byte[] message = Utils.toDelimitedString(".", segments).getBytes("UTF-8"); |
michael@0 | 53 | byte[] signature = privateKey.signMessage(message); |
michael@0 | 54 | segments.add(Base64.encodeBase64URLSafeString(signature)); |
michael@0 | 55 | return Utils.toDelimitedString(".", segments); |
michael@0 | 56 | } |
michael@0 | 57 | |
michael@0 | 58 | public static String decode(String token, VerifyingPublicKey publicKey) throws GeneralSecurityException, UnsupportedEncodingException { |
michael@0 | 59 | if (token == null) { |
michael@0 | 60 | throw new IllegalArgumentException("token must not be null"); |
michael@0 | 61 | } |
michael@0 | 62 | String[] segments = token.split("\\."); |
michael@0 | 63 | if (segments == null || segments.length != 3) { |
michael@0 | 64 | throw new GeneralSecurityException("malformed token"); |
michael@0 | 65 | } |
michael@0 | 66 | byte[] message = (segments[0] + "." + segments[1]).getBytes("UTF-8"); |
michael@0 | 67 | byte[] signature = Base64.decodeBase64(segments[2]); |
michael@0 | 68 | boolean verifies = publicKey.verifyMessage(message, signature); |
michael@0 | 69 | if (!verifies) { |
michael@0 | 70 | throw new GeneralSecurityException("bad signature"); |
michael@0 | 71 | } |
michael@0 | 72 | String payload = StringUtils.newStringUtf8(Base64.decodeBase64(segments[1])); |
michael@0 | 73 | return payload; |
michael@0 | 74 | } |
michael@0 | 75 | |
michael@0 | 76 | /** |
michael@0 | 77 | * Public for testing. |
michael@0 | 78 | */ |
michael@0 | 79 | @SuppressWarnings("unchecked") |
michael@0 | 80 | public static String getPayloadString(String payloadString, String audience, String issuer, |
michael@0 | 81 | Long issuedAt, long expiresAt) throws NonObjectJSONException, |
michael@0 | 82 | IOException, ParseException { |
michael@0 | 83 | ExtendedJSONObject payload; |
michael@0 | 84 | if (payloadString != null) { |
michael@0 | 85 | payload = new ExtendedJSONObject(payloadString); |
michael@0 | 86 | } else { |
michael@0 | 87 | payload = new ExtendedJSONObject(); |
michael@0 | 88 | } |
michael@0 | 89 | if (audience != null) { |
michael@0 | 90 | payload.put("aud", audience); |
michael@0 | 91 | } |
michael@0 | 92 | payload.put("iss", issuer); |
michael@0 | 93 | if (issuedAt != null) { |
michael@0 | 94 | payload.put("iat", issuedAt); |
michael@0 | 95 | } |
michael@0 | 96 | payload.put("exp", expiresAt); |
michael@0 | 97 | // TreeMap so that keys are sorted. A small attempt to keep output stable over time. |
michael@0 | 98 | return JSONObject.toJSONString(new TreeMap<Object, Object>(payload.object)); |
michael@0 | 99 | } |
michael@0 | 100 | |
michael@0 | 101 | protected static String getCertificatePayloadString(VerifyingPublicKey publicKeyToSign, String email) throws NonObjectJSONException, IOException, ParseException { |
michael@0 | 102 | ExtendedJSONObject payload = new ExtendedJSONObject(); |
michael@0 | 103 | ExtendedJSONObject principal = new ExtendedJSONObject(); |
michael@0 | 104 | principal.put("email", email); |
michael@0 | 105 | payload.put("principal", principal); |
michael@0 | 106 | payload.put("public-key", publicKeyToSign.toJSONObject()); |
michael@0 | 107 | return payload.toJSONString(); |
michael@0 | 108 | } |
michael@0 | 109 | |
michael@0 | 110 | public static String createCertificate(VerifyingPublicKey publicKeyToSign, String email, |
michael@0 | 111 | String issuer, long issuedAt, long expiresAt, SigningPrivateKey privateKey) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException { |
michael@0 | 112 | String certificatePayloadString = getCertificatePayloadString(publicKeyToSign, email); |
michael@0 | 113 | String payloadString = getPayloadString(certificatePayloadString, null, issuer, issuedAt, expiresAt); |
michael@0 | 114 | return JSONWebTokenUtils.encode(payloadString, privateKey); |
michael@0 | 115 | } |
michael@0 | 116 | |
michael@0 | 117 | /** |
michael@0 | 118 | * Create a Browser ID assertion. |
michael@0 | 119 | * |
michael@0 | 120 | * @param privateKeyToSignWith |
michael@0 | 121 | * private key to sign assertion with. |
michael@0 | 122 | * @param certificate |
michael@0 | 123 | * to include in assertion; no attempt is made to ensure the |
michael@0 | 124 | * certificate is valid, or corresponds to the private key, or any |
michael@0 | 125 | * other condition. |
michael@0 | 126 | * @param audience |
michael@0 | 127 | * to produce assertion for. |
michael@0 | 128 | * @param issuer |
michael@0 | 129 | * to produce assertion for. |
michael@0 | 130 | * @param issuedAt |
michael@0 | 131 | * timestamp for assertion, in milliseconds since the epoch; if null, |
michael@0 | 132 | * no timestamp is included. |
michael@0 | 133 | * @param expiresAt |
michael@0 | 134 | * expiration timestamp for assertion, in milliseconds since the epoch. |
michael@0 | 135 | * @return assertion. |
michael@0 | 136 | * @throws NonObjectJSONException |
michael@0 | 137 | * @throws IOException |
michael@0 | 138 | * @throws ParseException |
michael@0 | 139 | * @throws GeneralSecurityException |
michael@0 | 140 | */ |
michael@0 | 141 | public static String createAssertion(SigningPrivateKey privateKeyToSignWith, String certificate, String audience, |
michael@0 | 142 | String issuer, Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException { |
michael@0 | 143 | String emptyAssertionPayloadString = "{}"; |
michael@0 | 144 | String payloadString = getPayloadString(emptyAssertionPayloadString, audience, issuer, issuedAt, expiresAt); |
michael@0 | 145 | String signature = JSONWebTokenUtils.encode(payloadString, privateKeyToSignWith); |
michael@0 | 146 | return certificate + "~" + signature; |
michael@0 | 147 | } |
michael@0 | 148 | |
michael@0 | 149 | /** |
michael@0 | 150 | * For debugging only! |
michael@0 | 151 | * |
michael@0 | 152 | * @param input |
michael@0 | 153 | * certificate to dump. |
michael@0 | 154 | * @return non-null object with keys header, payload, signature if the |
michael@0 | 155 | * certificate is well-formed. |
michael@0 | 156 | */ |
michael@0 | 157 | public static ExtendedJSONObject parseCertificate(String input) { |
michael@0 | 158 | try { |
michael@0 | 159 | String[] parts = input.split("\\."); |
michael@0 | 160 | if (parts.length != 3) { |
michael@0 | 161 | return null; |
michael@0 | 162 | } |
michael@0 | 163 | String cHeader = new String(Base64.decodeBase64(parts[0])); |
michael@0 | 164 | String cPayload = new String(Base64.decodeBase64(parts[1])); |
michael@0 | 165 | String cSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2])); |
michael@0 | 166 | ExtendedJSONObject o = new ExtendedJSONObject(); |
michael@0 | 167 | o.put("header", new ExtendedJSONObject(cHeader)); |
michael@0 | 168 | o.put("payload", new ExtendedJSONObject(cPayload)); |
michael@0 | 169 | o.put("signature", cSignature); |
michael@0 | 170 | return o; |
michael@0 | 171 | } catch (Exception e) { |
michael@0 | 172 | return null; |
michael@0 | 173 | } |
michael@0 | 174 | } |
michael@0 | 175 | |
michael@0 | 176 | /** |
michael@0 | 177 | * For debugging only! |
michael@0 | 178 | * |
michael@0 | 179 | * @param input certificate to dump. |
michael@0 | 180 | * @return true if the certificate is well-formed. |
michael@0 | 181 | */ |
michael@0 | 182 | public static boolean dumpCertificate(String input) { |
michael@0 | 183 | ExtendedJSONObject c = parseCertificate(input); |
michael@0 | 184 | try { |
michael@0 | 185 | if (c == null) { |
michael@0 | 186 | System.out.println("Malformed certificate -- got exception trying to dump contents."); |
michael@0 | 187 | return false; |
michael@0 | 188 | } |
michael@0 | 189 | System.out.println("certificate header: " + c.getString("header")); |
michael@0 | 190 | System.out.println("certificate payload: " + c.getString("payload")); |
michael@0 | 191 | System.out.println("certificate signature: " + c.getString("signature")); |
michael@0 | 192 | return true; |
michael@0 | 193 | } catch (Exception e) { |
michael@0 | 194 | System.out.println("Malformed certificate -- got exception trying to dump contents."); |
michael@0 | 195 | return false; |
michael@0 | 196 | } |
michael@0 | 197 | } |
michael@0 | 198 | |
michael@0 | 199 | /** |
michael@0 | 200 | * For debugging only! |
michael@0 | 201 | * |
michael@0 | 202 | * @param input assertion to dump. |
michael@0 | 203 | * @return true if the assertion is well-formed. |
michael@0 | 204 | */ |
michael@0 | 205 | public static ExtendedJSONObject parseAssertion(String input) { |
michael@0 | 206 | try { |
michael@0 | 207 | String[] parts = input.split("~"); |
michael@0 | 208 | if (parts.length != 2) { |
michael@0 | 209 | return null; |
michael@0 | 210 | } |
michael@0 | 211 | String certificate = parts[0]; |
michael@0 | 212 | String assertion = parts[1]; |
michael@0 | 213 | parts = assertion.split("\\."); |
michael@0 | 214 | if (parts.length != 3) { |
michael@0 | 215 | return null; |
michael@0 | 216 | } |
michael@0 | 217 | String aHeader = new String(Base64.decodeBase64(parts[0])); |
michael@0 | 218 | String aPayload = new String(Base64.decodeBase64(parts[1])); |
michael@0 | 219 | String aSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2])); |
michael@0 | 220 | // We do all the assertion parsing *before* dumping the certificate in |
michael@0 | 221 | // case there's a malformed assertion. |
michael@0 | 222 | ExtendedJSONObject o = new ExtendedJSONObject(); |
michael@0 | 223 | o.put("header", new ExtendedJSONObject(aHeader)); |
michael@0 | 224 | o.put("payload", new ExtendedJSONObject(aPayload)); |
michael@0 | 225 | o.put("signature", aSignature); |
michael@0 | 226 | o.put("certificate", certificate); |
michael@0 | 227 | return o; |
michael@0 | 228 | } catch (Exception e) { |
michael@0 | 229 | return null; |
michael@0 | 230 | } |
michael@0 | 231 | } |
michael@0 | 232 | |
michael@0 | 233 | /** |
michael@0 | 234 | * For debugging only! |
michael@0 | 235 | * |
michael@0 | 236 | * @param input assertion to dump. |
michael@0 | 237 | * @return true if the assertion is well-formed. |
michael@0 | 238 | */ |
michael@0 | 239 | public static boolean dumpAssertion(String input) { |
michael@0 | 240 | ExtendedJSONObject a = parseAssertion(input); |
michael@0 | 241 | try { |
michael@0 | 242 | if (a == null) { |
michael@0 | 243 | System.out.println("Malformed assertion -- got exception trying to dump contents."); |
michael@0 | 244 | return false; |
michael@0 | 245 | } |
michael@0 | 246 | dumpCertificate(a.getString("certificate")); |
michael@0 | 247 | System.out.println("assertion header: " + a.getString("header")); |
michael@0 | 248 | System.out.println("assertion payload: " + a.getString("payload")); |
michael@0 | 249 | System.out.println("assertion signature: " + a.getString("signature")); |
michael@0 | 250 | return true; |
michael@0 | 251 | } catch (Exception e) { |
michael@0 | 252 | System.out.println("Malformed assertion -- got exception trying to dump contents."); |
michael@0 | 253 | return false; |
michael@0 | 254 | } |
michael@0 | 255 | } |
michael@0 | 256 | } |