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 +}