mobile/android/base/browserid/JSONWebTokenUtils.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

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 }

mercurial