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.math.BigInteger; michael@0: import java.security.GeneralSecurityException; michael@0: import java.security.KeyFactory; michael@0: import java.security.KeyPair; michael@0: import java.security.KeyPairGenerator; michael@0: import java.security.NoSuchAlgorithmException; michael@0: import java.security.Signature; michael@0: import java.security.interfaces.DSAParams; michael@0: import java.security.interfaces.DSAPrivateKey; michael@0: import java.security.interfaces.DSAPublicKey; michael@0: import java.security.spec.DSAPrivateKeySpec; michael@0: import java.security.spec.DSAPublicKeySpec; michael@0: import java.security.spec.InvalidKeySpecException; michael@0: import java.security.spec.KeySpec; michael@0: 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: public class DSACryptoImplementation { michael@0: public static final String SIGNATURE_ALGORITHM = "SHA1withDSA"; michael@0: public static final int SIGNATURE_LENGTH_BYTES = 40; // DSA signatures are always 40 bytes long. michael@0: michael@0: /** michael@0: * Parameters are serialized as hex strings. Hex-versus-decimal was michael@0: * reverse-engineered from what the Persona public verifier accepted. We michael@0: * expect to follow the JOSE/JWT spec as it solidifies, and that will probably michael@0: * mean unifying this base. michael@0: */ michael@0: protected static final int SERIALIZATION_BASE = 16; michael@0: michael@0: protected static class DSAVerifyingPublicKey implements VerifyingPublicKey { michael@0: protected final DSAPublicKey publicKey; michael@0: michael@0: public DSAVerifyingPublicKey(DSAPublicKey publicKey) { michael@0: this.publicKey = publicKey; michael@0: } michael@0: michael@0: /** michael@0: * Serialize to a JSON object. michael@0: *

michael@0: * Parameters are serialized as hex strings. Hex-versus-decimal was michael@0: * reverse-engineered from what the Persona public verifier accepted. michael@0: */ michael@0: @Override michael@0: public ExtendedJSONObject toJSONObject() { michael@0: DSAParams params = publicKey.getParams(); michael@0: ExtendedJSONObject o = new ExtendedJSONObject(); michael@0: o.put("algorithm", "DS"); michael@0: o.put("y", publicKey.getY().toString(SERIALIZATION_BASE)); michael@0: o.put("g", params.getG().toString(SERIALIZATION_BASE)); michael@0: o.put("p", params.getP().toString(SERIALIZATION_BASE)); michael@0: o.put("q", params.getQ().toString(SERIALIZATION_BASE)); michael@0: return o; michael@0: } michael@0: michael@0: @Override michael@0: public boolean verifyMessage(byte[] bytes, byte[] signature) michael@0: throws GeneralSecurityException { michael@0: if (bytes == null) { michael@0: throw new IllegalArgumentException("bytes must not be null"); michael@0: } michael@0: if (signature == null) { michael@0: throw new IllegalArgumentException("signature must not be null"); michael@0: } michael@0: if (signature.length != SIGNATURE_LENGTH_BYTES) { michael@0: return false; michael@0: } michael@0: byte[] first = new byte[signature.length / 2]; michael@0: byte[] second = new byte[signature.length / 2]; michael@0: System.arraycopy(signature, 0, first, 0, first.length); michael@0: System.arraycopy(signature, first.length, second, 0, second.length); michael@0: BigInteger r = new BigInteger(Utils.byte2Hex(first), 16); michael@0: BigInteger s = new BigInteger(Utils.byte2Hex(second), 16); michael@0: // This is awful, but encoding an extra 0 byte works better on devices. michael@0: byte[] encoded = ASNUtils.encodeTwoArraysToASN1( michael@0: Utils.hex2Byte(r.toString(16), 1 + SIGNATURE_LENGTH_BYTES / 2), michael@0: Utils.hex2Byte(s.toString(16), 1 + SIGNATURE_LENGTH_BYTES / 2)); michael@0: michael@0: final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM); michael@0: signer.initVerify(publicKey); michael@0: signer.update(bytes); michael@0: return signer.verify(encoded); michael@0: } michael@0: } michael@0: michael@0: protected static class DSASigningPrivateKey implements SigningPrivateKey { michael@0: protected final DSAPrivateKey privateKey; michael@0: michael@0: public DSASigningPrivateKey(DSAPrivateKey privateKey) { michael@0: this.privateKey = privateKey; michael@0: } michael@0: michael@0: @Override michael@0: public String getAlgorithm() { michael@0: return "DS" + (privateKey.getParams().getP().bitLength() + 7)/8; michael@0: } michael@0: michael@0: /** michael@0: * Serialize to a JSON object. michael@0: *

michael@0: * Parameters are serialized as decimal strings. Hex-versus-decimal was michael@0: * reverse-engineered from what the Persona public verifier accepted. michael@0: */ michael@0: @Override michael@0: public ExtendedJSONObject toJSONObject() { michael@0: DSAParams params = privateKey.getParams(); michael@0: ExtendedJSONObject o = new ExtendedJSONObject(); michael@0: o.put("algorithm", "DS"); michael@0: o.put("x", privateKey.getX().toString(SERIALIZATION_BASE)); michael@0: o.put("g", params.getG().toString(SERIALIZATION_BASE)); michael@0: o.put("p", params.getP().toString(SERIALIZATION_BASE)); michael@0: o.put("q", params.getQ().toString(SERIALIZATION_BASE)); michael@0: return o; michael@0: } michael@0: michael@0: @Override michael@0: public byte[] signMessage(byte[] bytes) michael@0: throws GeneralSecurityException { michael@0: if (bytes == null) { michael@0: throw new IllegalArgumentException("bytes must not be null"); michael@0: } michael@0: final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM); michael@0: signer.initSign(privateKey); michael@0: signer.update(bytes); michael@0: final byte[] signature = signer.sign(); michael@0: michael@0: final byte[][] arrays = ASNUtils.decodeTwoArraysFromASN1(signature); michael@0: BigInteger r = new BigInteger(arrays[0]); michael@0: BigInteger s = new BigInteger(arrays[1]); michael@0: // This is awful, but signatures are always 40 bytes long. michael@0: byte[] decoded = Utils.concatAll( michael@0: Utils.hex2Byte(r.toString(16), SIGNATURE_LENGTH_BYTES / 2), michael@0: Utils.hex2Byte(s.toString(16), SIGNATURE_LENGTH_BYTES / 2)); michael@0: return decoded; michael@0: } michael@0: } michael@0: michael@0: public static BrowserIDKeyPair generateKeyPair(int keysize) michael@0: throws NoSuchAlgorithmException { michael@0: final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); michael@0: keyPairGenerator.initialize(keysize); michael@0: final KeyPair keyPair = keyPairGenerator.generateKeyPair(); michael@0: DSAPrivateKey privateKey = (DSAPrivateKey) keyPair.getPrivate(); michael@0: DSAPublicKey publicKey = (DSAPublicKey) keyPair.getPublic(); michael@0: return new BrowserIDKeyPair(new DSASigningPrivateKey(privateKey), new DSAVerifyingPublicKey(publicKey)); michael@0: } michael@0: michael@0: public static SigningPrivateKey createPrivateKey(BigInteger x, BigInteger p, BigInteger q, BigInteger g) throws NoSuchAlgorithmException, InvalidKeySpecException { michael@0: if (x == null) { michael@0: throw new IllegalArgumentException("x must not be null"); michael@0: } michael@0: if (p == null) { michael@0: throw new IllegalArgumentException("p must not be null"); michael@0: } michael@0: if (q == null) { michael@0: throw new IllegalArgumentException("q must not be null"); michael@0: } michael@0: if (g == null) { michael@0: throw new IllegalArgumentException("g must not be null"); michael@0: } michael@0: KeySpec keySpec = new DSAPrivateKeySpec(x, p, q, g); michael@0: KeyFactory keyFactory = KeyFactory.getInstance("DSA"); michael@0: DSAPrivateKey privateKey = (DSAPrivateKey) keyFactory.generatePrivate(keySpec); michael@0: return new DSASigningPrivateKey(privateKey); michael@0: } michael@0: michael@0: public static VerifyingPublicKey createPublicKey(BigInteger y, BigInteger p, BigInteger q, BigInteger g) throws NoSuchAlgorithmException, InvalidKeySpecException { michael@0: if (y == null) { michael@0: throw new IllegalArgumentException("n must not be null"); michael@0: } michael@0: if (p == null) { michael@0: throw new IllegalArgumentException("p must not be null"); michael@0: } michael@0: if (q == null) { michael@0: throw new IllegalArgumentException("q must not be null"); michael@0: } michael@0: if (g == null) { michael@0: throw new IllegalArgumentException("g must not be null"); michael@0: } michael@0: KeySpec keySpec = new DSAPublicKeySpec(y, p, q, g); michael@0: KeyFactory keyFactory = KeyFactory.getInstance("DSA"); michael@0: DSAPublicKey publicKey = (DSAPublicKey) keyFactory.generatePublic(keySpec); michael@0: return new DSAVerifyingPublicKey(publicKey); michael@0: } michael@0: michael@0: public static SigningPrivateKey createPrivateKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { michael@0: String algorithm = o.getString("algorithm"); michael@0: if (!"DS".equals(algorithm)) { michael@0: throw new InvalidKeySpecException("algorithm must equal DS, was " + algorithm); michael@0: } michael@0: try { michael@0: BigInteger x = new BigInteger(o.getString("x"), SERIALIZATION_BASE); michael@0: BigInteger p = new BigInteger(o.getString("p"), SERIALIZATION_BASE); michael@0: BigInteger q = new BigInteger(o.getString("q"), SERIALIZATION_BASE); michael@0: BigInteger g = new BigInteger(o.getString("g"), SERIALIZATION_BASE); michael@0: return createPrivateKey(x, p, q, g); michael@0: } catch (NullPointerException e) { michael@0: throw new InvalidKeySpecException("x, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE); michael@0: } catch (NumberFormatException e) { michael@0: throw new InvalidKeySpecException("x, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE); michael@0: } michael@0: } michael@0: michael@0: public static VerifyingPublicKey createPublicKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { michael@0: String algorithm = o.getString("algorithm"); michael@0: if (!"DS".equals(algorithm)) { michael@0: throw new InvalidKeySpecException("algorithm must equal DS, was " + algorithm); michael@0: } michael@0: try { michael@0: BigInteger y = new BigInteger(o.getString("y"), SERIALIZATION_BASE); michael@0: BigInteger p = new BigInteger(o.getString("p"), SERIALIZATION_BASE); michael@0: BigInteger q = new BigInteger(o.getString("q"), SERIALIZATION_BASE); michael@0: BigInteger g = new BigInteger(o.getString("g"), SERIALIZATION_BASE); michael@0: return createPublicKey(y, p, q, g); michael@0: } catch (NullPointerException e) { michael@0: throw new InvalidKeySpecException("y, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE); michael@0: } catch (NumberFormatException e) { michael@0: throw new InvalidKeySpecException("y, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE); michael@0: } michael@0: } michael@0: michael@0: public static BrowserIDKeyPair fromJSONObject(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { michael@0: try { michael@0: ExtendedJSONObject privateKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PRIVATEKEY); michael@0: ExtendedJSONObject publicKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PUBLICKEY); michael@0: if (privateKey == null) { michael@0: throw new InvalidKeySpecException("privateKey must not be null"); michael@0: } michael@0: if (publicKey == null) { michael@0: throw new InvalidKeySpecException("publicKey must not be null"); michael@0: } michael@0: return new BrowserIDKeyPair(createPrivateKey(privateKey), createPublicKey(publicKey)); michael@0: } catch (NonObjectJSONException e) { michael@0: throw new InvalidKeySpecException("privateKey and publicKey must be JSON objects"); michael@0: } michael@0: } michael@0: }