diff -r 000000000000 -r 6474c204b198 mobile/android/base/browserid/DSACryptoImplementation.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/browserid/DSACryptoImplementation.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,244 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.browserid; + +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.DSAPublicKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; + +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.NonObjectJSONException; +import org.mozilla.gecko.sync.Utils; + +public class DSACryptoImplementation { + public static final String SIGNATURE_ALGORITHM = "SHA1withDSA"; + public static final int SIGNATURE_LENGTH_BYTES = 40; // DSA signatures are always 40 bytes long. + + /** + * Parameters are serialized as hex strings. Hex-versus-decimal was + * reverse-engineered from what the Persona public verifier accepted. We + * expect to follow the JOSE/JWT spec as it solidifies, and that will probably + * mean unifying this base. + */ + protected static final int SERIALIZATION_BASE = 16; + + protected static class DSAVerifyingPublicKey implements VerifyingPublicKey { + protected final DSAPublicKey publicKey; + + public DSAVerifyingPublicKey(DSAPublicKey publicKey) { + this.publicKey = publicKey; + } + + /** + * Serialize to a JSON object. + *

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

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