mobile/android/base/browserid/JSONWebTokenUtils.java

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/base/browserid/JSONWebTokenUtils.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,256 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +package org.mozilla.gecko.browserid;
     1.9 +
    1.10 +import java.io.IOException;
    1.11 +import java.io.UnsupportedEncodingException;
    1.12 +import java.security.GeneralSecurityException;
    1.13 +import java.util.ArrayList;
    1.14 +import java.util.Map;
    1.15 +import java.util.TreeMap;
    1.16 +
    1.17 +import org.json.simple.JSONObject;
    1.18 +import org.json.simple.parser.ParseException;
    1.19 +import org.mozilla.apache.commons.codec.binary.Base64;
    1.20 +import org.mozilla.apache.commons.codec.binary.StringUtils;
    1.21 +import org.mozilla.gecko.sync.ExtendedJSONObject;
    1.22 +import org.mozilla.gecko.sync.NonObjectJSONException;
    1.23 +import org.mozilla.gecko.sync.Utils;
    1.24 +
    1.25 +/**
    1.26 + * Encode and decode JSON Web Tokens.
    1.27 + * <p>
    1.28 + * Reverse-engineered from the Node.js jwcrypto library at
    1.29 + * <a href="https://github.com/mozilla/jwcrypto">https://github.com/mozilla/jwcrypto</a>
    1.30 + * and informed by the informal draft standard "JSON Web Token (JWT)" at
    1.31 + * <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>.
    1.32 + */
    1.33 +public class JSONWebTokenUtils {
    1.34 +  public static final long DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS = 60 * 60 * 1000;
    1.35 +  public static final long DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS = 60 * 60 * 1000;
    1.36 +  public static final long DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS = 9999999999999L;
    1.37 +  public static final String DEFAULT_CERTIFICATE_ISSUER = "127.0.0.1";
    1.38 +  public static final String DEFAULT_ASSERTION_ISSUER = "127.0.0.1";
    1.39 +
    1.40 +  public static String encode(String payload, SigningPrivateKey privateKey) throws UnsupportedEncodingException, GeneralSecurityException  {
    1.41 +    return encode(payload, privateKey, null);
    1.42 +  }
    1.43 +
    1.44 +  protected static String encode(String payload, SigningPrivateKey privateKey, Map<String, Object> headerFields) throws UnsupportedEncodingException, GeneralSecurityException  {
    1.45 +    ExtendedJSONObject header = new ExtendedJSONObject();
    1.46 +    if (headerFields != null) {
    1.47 +      header.putAll(headerFields);
    1.48 +    }
    1.49 +    header.put("alg", privateKey.getAlgorithm());
    1.50 +    String encodedHeader  = Base64.encodeBase64URLSafeString(header.toJSONString().getBytes("UTF-8"));
    1.51 +    String encodedPayload = Base64.encodeBase64URLSafeString(payload.getBytes("UTF-8"));
    1.52 +    ArrayList<String> segments = new ArrayList<String>();
    1.53 +    segments.add(encodedHeader);
    1.54 +    segments.add(encodedPayload);
    1.55 +    byte[] message = Utils.toDelimitedString(".", segments).getBytes("UTF-8");
    1.56 +    byte[] signature = privateKey.signMessage(message);
    1.57 +    segments.add(Base64.encodeBase64URLSafeString(signature));
    1.58 +    return Utils.toDelimitedString(".", segments);
    1.59 +  }
    1.60 +
    1.61 +  public static String decode(String token, VerifyingPublicKey publicKey) throws GeneralSecurityException, UnsupportedEncodingException  {
    1.62 +    if (token == null) {
    1.63 +      throw new IllegalArgumentException("token must not be null");
    1.64 +    }
    1.65 +    String[] segments = token.split("\\.");
    1.66 +    if (segments == null || segments.length != 3) {
    1.67 +      throw new GeneralSecurityException("malformed token");
    1.68 +    }
    1.69 +    byte[] message = (segments[0] + "." + segments[1]).getBytes("UTF-8");
    1.70 +    byte[] signature = Base64.decodeBase64(segments[2]);
    1.71 +    boolean verifies = publicKey.verifyMessage(message, signature);
    1.72 +    if (!verifies) {
    1.73 +      throw new GeneralSecurityException("bad signature");
    1.74 +    }
    1.75 +    String payload = StringUtils.newStringUtf8(Base64.decodeBase64(segments[1]));
    1.76 +    return payload;
    1.77 +  }
    1.78 +
    1.79 +  /**
    1.80 +   * Public for testing.
    1.81 +   */
    1.82 +  @SuppressWarnings("unchecked")
    1.83 +  public static String getPayloadString(String payloadString, String audience, String issuer,
    1.84 +      Long issuedAt, long expiresAt) throws NonObjectJSONException,
    1.85 +      IOException, ParseException {
    1.86 +    ExtendedJSONObject payload;
    1.87 +    if (payloadString != null) {
    1.88 +      payload = new ExtendedJSONObject(payloadString);
    1.89 +    } else {
    1.90 +      payload = new ExtendedJSONObject();
    1.91 +    }
    1.92 +    if (audience != null) {
    1.93 +      payload.put("aud", audience);
    1.94 +    }
    1.95 +    payload.put("iss", issuer);
    1.96 +    if (issuedAt != null) {
    1.97 +      payload.put("iat", issuedAt);
    1.98 +    }
    1.99 +    payload.put("exp", expiresAt);
   1.100 +    // TreeMap so that keys are sorted. A small attempt to keep output stable over time.
   1.101 +    return JSONObject.toJSONString(new TreeMap<Object, Object>(payload.object));
   1.102 +  }
   1.103 +
   1.104 +  protected static String getCertificatePayloadString(VerifyingPublicKey publicKeyToSign, String email) throws NonObjectJSONException, IOException, ParseException  {
   1.105 +    ExtendedJSONObject payload = new ExtendedJSONObject();
   1.106 +    ExtendedJSONObject principal = new ExtendedJSONObject();
   1.107 +    principal.put("email", email);
   1.108 +    payload.put("principal", principal);
   1.109 +    payload.put("public-key", publicKeyToSign.toJSONObject());
   1.110 +    return payload.toJSONString();
   1.111 +  }
   1.112 +
   1.113 +  public static String createCertificate(VerifyingPublicKey publicKeyToSign, String email,
   1.114 +      String issuer, long issuedAt, long expiresAt, SigningPrivateKey privateKey) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException  {
   1.115 +    String certificatePayloadString = getCertificatePayloadString(publicKeyToSign, email);
   1.116 +    String payloadString = getPayloadString(certificatePayloadString, null, issuer, issuedAt, expiresAt);
   1.117 +    return JSONWebTokenUtils.encode(payloadString, privateKey);
   1.118 +  }
   1.119 +
   1.120 +  /**
   1.121 +   * Create a Browser ID assertion.
   1.122 +   *
   1.123 +   * @param privateKeyToSignWith
   1.124 +   *          private key to sign assertion with.
   1.125 +   * @param certificate
   1.126 +   *          to include in assertion; no attempt is made to ensure the
   1.127 +   *          certificate is valid, or corresponds to the private key, or any
   1.128 +   *          other condition.
   1.129 +   * @param audience
   1.130 +   *          to produce assertion for.
   1.131 +   * @param issuer
   1.132 +   *          to produce assertion for.
   1.133 +   * @param issuedAt
   1.134 +   *          timestamp for assertion, in milliseconds since the epoch; if null,
   1.135 +   *          no timestamp is included.
   1.136 +   * @param expiresAt
   1.137 +   *          expiration timestamp for assertion, in milliseconds since the epoch.
   1.138 +   * @return assertion.
   1.139 +   * @throws NonObjectJSONException
   1.140 +   * @throws IOException
   1.141 +   * @throws ParseException
   1.142 +   * @throws GeneralSecurityException
   1.143 +   */
   1.144 +  public static String createAssertion(SigningPrivateKey privateKeyToSignWith, String certificate, String audience,
   1.145 +      String issuer, Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException  {
   1.146 +    String emptyAssertionPayloadString = "{}";
   1.147 +    String payloadString = getPayloadString(emptyAssertionPayloadString, audience, issuer, issuedAt, expiresAt);
   1.148 +    String signature = JSONWebTokenUtils.encode(payloadString, privateKeyToSignWith);
   1.149 +    return certificate + "~" + signature;
   1.150 +  }
   1.151 +
   1.152 +  /**
   1.153 +   * For debugging only!
   1.154 +   *
   1.155 +   * @param input
   1.156 +   *          certificate to dump.
   1.157 +   * @return non-null object with keys header, payload, signature if the
   1.158 +   *         certificate is well-formed.
   1.159 +   */
   1.160 +  public static ExtendedJSONObject parseCertificate(String input) {
   1.161 +    try {
   1.162 +      String[] parts = input.split("\\.");
   1.163 +      if (parts.length != 3) {
   1.164 +        return null;
   1.165 +      }
   1.166 +      String cHeader = new String(Base64.decodeBase64(parts[0]));
   1.167 +      String cPayload = new String(Base64.decodeBase64(parts[1]));
   1.168 +      String cSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2]));
   1.169 +      ExtendedJSONObject o = new ExtendedJSONObject();
   1.170 +      o.put("header", new ExtendedJSONObject(cHeader));
   1.171 +      o.put("payload", new ExtendedJSONObject(cPayload));
   1.172 +      o.put("signature", cSignature);
   1.173 +      return o;
   1.174 +    } catch (Exception e) {
   1.175 +      return null;
   1.176 +    }
   1.177 +  }
   1.178 +
   1.179 +  /**
   1.180 +   * For debugging only!
   1.181 +   *
   1.182 +   * @param input certificate to dump.
   1.183 +   * @return true if the certificate is well-formed.
   1.184 +   */
   1.185 +  public static boolean dumpCertificate(String input) {
   1.186 +    ExtendedJSONObject c = parseCertificate(input);
   1.187 +    try {
   1.188 +      if (c == null) {
   1.189 +        System.out.println("Malformed certificate -- got exception trying to dump contents.");
   1.190 +        return false;
   1.191 +      }
   1.192 +      System.out.println("certificate header:    " + c.getString("header"));
   1.193 +      System.out.println("certificate payload:   " + c.getString("payload"));
   1.194 +      System.out.println("certificate signature: " + c.getString("signature"));
   1.195 +      return true;
   1.196 +    } catch (Exception e) {
   1.197 +      System.out.println("Malformed certificate -- got exception trying to dump contents.");
   1.198 +      return false;
   1.199 +    }
   1.200 +  }
   1.201 +
   1.202 +  /**
   1.203 +   * For debugging only!
   1.204 +   *
   1.205 +   * @param input assertion to dump.
   1.206 +   * @return true if the assertion is well-formed.
   1.207 +   */
   1.208 +  public static ExtendedJSONObject parseAssertion(String input) {
   1.209 +    try {
   1.210 +      String[] parts = input.split("~");
   1.211 +      if (parts.length != 2) {
   1.212 +        return null;
   1.213 +      }
   1.214 +      String certificate = parts[0];
   1.215 +      String assertion = parts[1];
   1.216 +      parts = assertion.split("\\.");
   1.217 +      if (parts.length != 3) {
   1.218 +        return null;
   1.219 +      }
   1.220 +      String aHeader = new String(Base64.decodeBase64(parts[0]));
   1.221 +      String aPayload = new String(Base64.decodeBase64(parts[1]));
   1.222 +      String aSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2]));
   1.223 +      // We do all the assertion parsing *before* dumping the certificate in
   1.224 +      // case there's a malformed assertion.
   1.225 +      ExtendedJSONObject o = new ExtendedJSONObject();
   1.226 +      o.put("header", new ExtendedJSONObject(aHeader));
   1.227 +      o.put("payload", new ExtendedJSONObject(aPayload));
   1.228 +      o.put("signature", aSignature);
   1.229 +      o.put("certificate", certificate);
   1.230 +      return o;
   1.231 +    } catch (Exception e) {
   1.232 +      return null;
   1.233 +    }
   1.234 +  }
   1.235 +
   1.236 +  /**
   1.237 +   * For debugging only!
   1.238 +   *
   1.239 +   * @param input assertion to dump.
   1.240 +   * @return true if the assertion is well-formed.
   1.241 +   */
   1.242 +  public static boolean dumpAssertion(String input) {
   1.243 +    ExtendedJSONObject a = parseAssertion(input);
   1.244 +    try {
   1.245 +      if (a == null) {
   1.246 +        System.out.println("Malformed assertion -- got exception trying to dump contents.");
   1.247 +        return false;
   1.248 +      }
   1.249 +      dumpCertificate(a.getString("certificate"));
   1.250 +      System.out.println("assertion   header:    " + a.getString("header"));
   1.251 +      System.out.println("assertion   payload:   " + a.getString("payload"));
   1.252 +      System.out.println("assertion   signature: " + a.getString("signature"));
   1.253 +      return true;
   1.254 +    } catch (Exception e) {
   1.255 +      System.out.println("Malformed assertion -- got exception trying to dump contents.");
   1.256 +      return false;
   1.257 +    }
   1.258 +  }
   1.259 +}

mercurial