mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-31 22:25:30 +00:00
245 lines
10 KiB
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");
|
|
}
|
|
}
|
|
}
|