summaryrefslogtreecommitdiffstats
path: root/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/DSACryptoImplementation.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/services/src/main/java/org/mozilla/gecko/browserid/DSACryptoImplementation.java')
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/browserid/DSACryptoImplementation.java255
1 files changed, 255 insertions, 0 deletions
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/DSACryptoImplementation.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/DSACryptoImplementation.java
new file mode 100644
index 000000000..a04a89c8e
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/DSACryptoImplementation.java
@@ -0,0 +1,255 @@
+/* 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 android.annotation.SuppressLint;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.util.PRNGFixes;
+
+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;
+
+public class DSACryptoImplementation {
+ private static final String LOG_TAG = DSACryptoImplementation.class.getSimpleName();
+
+ 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;
+ }
+
+ @SuppressLint("TrulyRandom")
+ @Override
+ public byte[] signMessage(byte[] bytes)
+ throws GeneralSecurityException {
+ if (bytes == null) {
+ throw new IllegalArgumentException("bytes must not be null");
+ }
+
+ try {
+ PRNGFixes.apply();
+ } catch (Exception e) {
+ // Not much to be done here: it was weak before, and we couldn't patch it, so it's weak now. Not worth aborting.
+ Logger.error(LOG_TAG, "Got exception applying PRNGFixes! Cryptographic data produced on this device may be weak. Ignoring.", e);
+ }
+
+ 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 | 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 | 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");
+ }
+ }
+}