michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.browserid; michael@0: michael@0: import java.io.IOException; michael@0: import java.io.UnsupportedEncodingException; michael@0: import java.security.GeneralSecurityException; michael@0: import java.util.ArrayList; michael@0: import java.util.Map; michael@0: import java.util.TreeMap; michael@0: michael@0: import org.json.simple.JSONObject; michael@0: import org.json.simple.parser.ParseException; michael@0: import org.mozilla.apache.commons.codec.binary.Base64; michael@0: import org.mozilla.apache.commons.codec.binary.StringUtils; michael@0: import org.mozilla.gecko.sync.ExtendedJSONObject; michael@0: import org.mozilla.gecko.sync.NonObjectJSONException; michael@0: import org.mozilla.gecko.sync.Utils; michael@0: michael@0: /** michael@0: * Encode and decode JSON Web Tokens. michael@0: *

michael@0: * Reverse-engineered from the Node.js jwcrypto library at michael@0: * https://github.com/mozilla/jwcrypto michael@0: * and informed by the informal draft standard "JSON Web Token (JWT)" at michael@0: * http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html. michael@0: */ michael@0: public class JSONWebTokenUtils { michael@0: public static final long DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS = 60 * 60 * 1000; michael@0: public static final long DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS = 60 * 60 * 1000; michael@0: public static final long DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS = 9999999999999L; michael@0: public static final String DEFAULT_CERTIFICATE_ISSUER = "127.0.0.1"; michael@0: public static final String DEFAULT_ASSERTION_ISSUER = "127.0.0.1"; michael@0: michael@0: public static String encode(String payload, SigningPrivateKey privateKey) throws UnsupportedEncodingException, GeneralSecurityException { michael@0: return encode(payload, privateKey, null); michael@0: } michael@0: michael@0: protected static String encode(String payload, SigningPrivateKey privateKey, Map headerFields) throws UnsupportedEncodingException, GeneralSecurityException { michael@0: ExtendedJSONObject header = new ExtendedJSONObject(); michael@0: if (headerFields != null) { michael@0: header.putAll(headerFields); michael@0: } michael@0: header.put("alg", privateKey.getAlgorithm()); michael@0: String encodedHeader = Base64.encodeBase64URLSafeString(header.toJSONString().getBytes("UTF-8")); michael@0: String encodedPayload = Base64.encodeBase64URLSafeString(payload.getBytes("UTF-8")); michael@0: ArrayList segments = new ArrayList(); michael@0: segments.add(encodedHeader); michael@0: segments.add(encodedPayload); michael@0: byte[] message = Utils.toDelimitedString(".", segments).getBytes("UTF-8"); michael@0: byte[] signature = privateKey.signMessage(message); michael@0: segments.add(Base64.encodeBase64URLSafeString(signature)); michael@0: return Utils.toDelimitedString(".", segments); michael@0: } michael@0: michael@0: public static String decode(String token, VerifyingPublicKey publicKey) throws GeneralSecurityException, UnsupportedEncodingException { michael@0: if (token == null) { michael@0: throw new IllegalArgumentException("token must not be null"); michael@0: } michael@0: String[] segments = token.split("\\."); michael@0: if (segments == null || segments.length != 3) { michael@0: throw new GeneralSecurityException("malformed token"); michael@0: } michael@0: byte[] message = (segments[0] + "." + segments[1]).getBytes("UTF-8"); michael@0: byte[] signature = Base64.decodeBase64(segments[2]); michael@0: boolean verifies = publicKey.verifyMessage(message, signature); michael@0: if (!verifies) { michael@0: throw new GeneralSecurityException("bad signature"); michael@0: } michael@0: String payload = StringUtils.newStringUtf8(Base64.decodeBase64(segments[1])); michael@0: return payload; michael@0: } michael@0: michael@0: /** michael@0: * Public for testing. michael@0: */ michael@0: @SuppressWarnings("unchecked") michael@0: public static String getPayloadString(String payloadString, String audience, String issuer, michael@0: Long issuedAt, long expiresAt) throws NonObjectJSONException, michael@0: IOException, ParseException { michael@0: ExtendedJSONObject payload; michael@0: if (payloadString != null) { michael@0: payload = new ExtendedJSONObject(payloadString); michael@0: } else { michael@0: payload = new ExtendedJSONObject(); michael@0: } michael@0: if (audience != null) { michael@0: payload.put("aud", audience); michael@0: } michael@0: payload.put("iss", issuer); michael@0: if (issuedAt != null) { michael@0: payload.put("iat", issuedAt); michael@0: } michael@0: payload.put("exp", expiresAt); michael@0: // TreeMap so that keys are sorted. A small attempt to keep output stable over time. michael@0: return JSONObject.toJSONString(new TreeMap(payload.object)); michael@0: } michael@0: michael@0: protected static String getCertificatePayloadString(VerifyingPublicKey publicKeyToSign, String email) throws NonObjectJSONException, IOException, ParseException { michael@0: ExtendedJSONObject payload = new ExtendedJSONObject(); michael@0: ExtendedJSONObject principal = new ExtendedJSONObject(); michael@0: principal.put("email", email); michael@0: payload.put("principal", principal); michael@0: payload.put("public-key", publicKeyToSign.toJSONObject()); michael@0: return payload.toJSONString(); michael@0: } michael@0: michael@0: public static String createCertificate(VerifyingPublicKey publicKeyToSign, String email, michael@0: String issuer, long issuedAt, long expiresAt, SigningPrivateKey privateKey) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException { michael@0: String certificatePayloadString = getCertificatePayloadString(publicKeyToSign, email); michael@0: String payloadString = getPayloadString(certificatePayloadString, null, issuer, issuedAt, expiresAt); michael@0: return JSONWebTokenUtils.encode(payloadString, privateKey); michael@0: } michael@0: michael@0: /** michael@0: * Create a Browser ID assertion. michael@0: * michael@0: * @param privateKeyToSignWith michael@0: * private key to sign assertion with. michael@0: * @param certificate michael@0: * to include in assertion; no attempt is made to ensure the michael@0: * certificate is valid, or corresponds to the private key, or any michael@0: * other condition. michael@0: * @param audience michael@0: * to produce assertion for. michael@0: * @param issuer michael@0: * to produce assertion for. michael@0: * @param issuedAt michael@0: * timestamp for assertion, in milliseconds since the epoch; if null, michael@0: * no timestamp is included. michael@0: * @param expiresAt michael@0: * expiration timestamp for assertion, in milliseconds since the epoch. michael@0: * @return assertion. michael@0: * @throws NonObjectJSONException michael@0: * @throws IOException michael@0: * @throws ParseException michael@0: * @throws GeneralSecurityException michael@0: */ michael@0: public static String createAssertion(SigningPrivateKey privateKeyToSignWith, String certificate, String audience, michael@0: String issuer, Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException { michael@0: String emptyAssertionPayloadString = "{}"; michael@0: String payloadString = getPayloadString(emptyAssertionPayloadString, audience, issuer, issuedAt, expiresAt); michael@0: String signature = JSONWebTokenUtils.encode(payloadString, privateKeyToSignWith); michael@0: return certificate + "~" + signature; michael@0: } michael@0: michael@0: /** michael@0: * For debugging only! michael@0: * michael@0: * @param input michael@0: * certificate to dump. michael@0: * @return non-null object with keys header, payload, signature if the michael@0: * certificate is well-formed. michael@0: */ michael@0: public static ExtendedJSONObject parseCertificate(String input) { michael@0: try { michael@0: String[] parts = input.split("\\."); michael@0: if (parts.length != 3) { michael@0: return null; michael@0: } michael@0: String cHeader = new String(Base64.decodeBase64(parts[0])); michael@0: String cPayload = new String(Base64.decodeBase64(parts[1])); michael@0: String cSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2])); michael@0: ExtendedJSONObject o = new ExtendedJSONObject(); michael@0: o.put("header", new ExtendedJSONObject(cHeader)); michael@0: o.put("payload", new ExtendedJSONObject(cPayload)); michael@0: o.put("signature", cSignature); michael@0: return o; michael@0: } catch (Exception e) { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * For debugging only! michael@0: * michael@0: * @param input certificate to dump. michael@0: * @return true if the certificate is well-formed. michael@0: */ michael@0: public static boolean dumpCertificate(String input) { michael@0: ExtendedJSONObject c = parseCertificate(input); michael@0: try { michael@0: if (c == null) { michael@0: System.out.println("Malformed certificate -- got exception trying to dump contents."); michael@0: return false; michael@0: } michael@0: System.out.println("certificate header: " + c.getString("header")); michael@0: System.out.println("certificate payload: " + c.getString("payload")); michael@0: System.out.println("certificate signature: " + c.getString("signature")); michael@0: return true; michael@0: } catch (Exception e) { michael@0: System.out.println("Malformed certificate -- got exception trying to dump contents."); michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * For debugging only! michael@0: * michael@0: * @param input assertion to dump. michael@0: * @return true if the assertion is well-formed. michael@0: */ michael@0: public static ExtendedJSONObject parseAssertion(String input) { michael@0: try { michael@0: String[] parts = input.split("~"); michael@0: if (parts.length != 2) { michael@0: return null; michael@0: } michael@0: String certificate = parts[0]; michael@0: String assertion = parts[1]; michael@0: parts = assertion.split("\\."); michael@0: if (parts.length != 3) { michael@0: return null; michael@0: } michael@0: String aHeader = new String(Base64.decodeBase64(parts[0])); michael@0: String aPayload = new String(Base64.decodeBase64(parts[1])); michael@0: String aSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2])); michael@0: // We do all the assertion parsing *before* dumping the certificate in michael@0: // case there's a malformed assertion. michael@0: ExtendedJSONObject o = new ExtendedJSONObject(); michael@0: o.put("header", new ExtendedJSONObject(aHeader)); michael@0: o.put("payload", new ExtendedJSONObject(aPayload)); michael@0: o.put("signature", aSignature); michael@0: o.put("certificate", certificate); michael@0: return o; michael@0: } catch (Exception e) { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * For debugging only! michael@0: * michael@0: * @param input assertion to dump. michael@0: * @return true if the assertion is well-formed. michael@0: */ michael@0: public static boolean dumpAssertion(String input) { michael@0: ExtendedJSONObject a = parseAssertion(input); michael@0: try { michael@0: if (a == null) { michael@0: System.out.println("Malformed assertion -- got exception trying to dump contents."); michael@0: return false; michael@0: } michael@0: dumpCertificate(a.getString("certificate")); michael@0: System.out.println("assertion header: " + a.getString("header")); michael@0: System.out.println("assertion payload: " + a.getString("payload")); michael@0: System.out.println("assertion signature: " + a.getString("signature")); michael@0: return true; michael@0: } catch (Exception e) { michael@0: System.out.println("Malformed assertion -- got exception trying to dump contents."); michael@0: return false; michael@0: } michael@0: } michael@0: }