gecko/mobile/android/base/browserid/DSACryptoImplementation.java

245 lines
10 KiB
Java

/* 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.
* <p>
* 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.
* <p>
* 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");
}
}
}