Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.gecko.browserid;
7 import java.math.BigInteger;
8 import java.security.GeneralSecurityException;
9 import java.security.KeyFactory;
10 import java.security.KeyPair;
11 import java.security.KeyPairGenerator;
12 import java.security.NoSuchAlgorithmException;
13 import java.security.Signature;
14 import java.security.interfaces.DSAParams;
15 import java.security.interfaces.DSAPrivateKey;
16 import java.security.interfaces.DSAPublicKey;
17 import java.security.spec.DSAPrivateKeySpec;
18 import java.security.spec.DSAPublicKeySpec;
19 import java.security.spec.InvalidKeySpecException;
20 import java.security.spec.KeySpec;
22 import org.mozilla.gecko.sync.ExtendedJSONObject;
23 import org.mozilla.gecko.sync.NonObjectJSONException;
24 import org.mozilla.gecko.sync.Utils;
26 public class DSACryptoImplementation {
27 public static final String SIGNATURE_ALGORITHM = "SHA1withDSA";
28 public static final int SIGNATURE_LENGTH_BYTES = 40; // DSA signatures are always 40 bytes long.
30 /**
31 * Parameters are serialized as hex strings. Hex-versus-decimal was
32 * reverse-engineered from what the Persona public verifier accepted. We
33 * expect to follow the JOSE/JWT spec as it solidifies, and that will probably
34 * mean unifying this base.
35 */
36 protected static final int SERIALIZATION_BASE = 16;
38 protected static class DSAVerifyingPublicKey implements VerifyingPublicKey {
39 protected final DSAPublicKey publicKey;
41 public DSAVerifyingPublicKey(DSAPublicKey publicKey) {
42 this.publicKey = publicKey;
43 }
45 /**
46 * Serialize to a JSON object.
47 * <p>
48 * Parameters are serialized as hex strings. Hex-versus-decimal was
49 * reverse-engineered from what the Persona public verifier accepted.
50 */
51 @Override
52 public ExtendedJSONObject toJSONObject() {
53 DSAParams params = publicKey.getParams();
54 ExtendedJSONObject o = new ExtendedJSONObject();
55 o.put("algorithm", "DS");
56 o.put("y", publicKey.getY().toString(SERIALIZATION_BASE));
57 o.put("g", params.getG().toString(SERIALIZATION_BASE));
58 o.put("p", params.getP().toString(SERIALIZATION_BASE));
59 o.put("q", params.getQ().toString(SERIALIZATION_BASE));
60 return o;
61 }
63 @Override
64 public boolean verifyMessage(byte[] bytes, byte[] signature)
65 throws GeneralSecurityException {
66 if (bytes == null) {
67 throw new IllegalArgumentException("bytes must not be null");
68 }
69 if (signature == null) {
70 throw new IllegalArgumentException("signature must not be null");
71 }
72 if (signature.length != SIGNATURE_LENGTH_BYTES) {
73 return false;
74 }
75 byte[] first = new byte[signature.length / 2];
76 byte[] second = new byte[signature.length / 2];
77 System.arraycopy(signature, 0, first, 0, first.length);
78 System.arraycopy(signature, first.length, second, 0, second.length);
79 BigInteger r = new BigInteger(Utils.byte2Hex(first), 16);
80 BigInteger s = new BigInteger(Utils.byte2Hex(second), 16);
81 // This is awful, but encoding an extra 0 byte works better on devices.
82 byte[] encoded = ASNUtils.encodeTwoArraysToASN1(
83 Utils.hex2Byte(r.toString(16), 1 + SIGNATURE_LENGTH_BYTES / 2),
84 Utils.hex2Byte(s.toString(16), 1 + SIGNATURE_LENGTH_BYTES / 2));
86 final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
87 signer.initVerify(publicKey);
88 signer.update(bytes);
89 return signer.verify(encoded);
90 }
91 }
93 protected static class DSASigningPrivateKey implements SigningPrivateKey {
94 protected final DSAPrivateKey privateKey;
96 public DSASigningPrivateKey(DSAPrivateKey privateKey) {
97 this.privateKey = privateKey;
98 }
100 @Override
101 public String getAlgorithm() {
102 return "DS" + (privateKey.getParams().getP().bitLength() + 7)/8;
103 }
105 /**
106 * Serialize to a JSON object.
107 * <p>
108 * Parameters are serialized as decimal strings. Hex-versus-decimal was
109 * reverse-engineered from what the Persona public verifier accepted.
110 */
111 @Override
112 public ExtendedJSONObject toJSONObject() {
113 DSAParams params = privateKey.getParams();
114 ExtendedJSONObject o = new ExtendedJSONObject();
115 o.put("algorithm", "DS");
116 o.put("x", privateKey.getX().toString(SERIALIZATION_BASE));
117 o.put("g", params.getG().toString(SERIALIZATION_BASE));
118 o.put("p", params.getP().toString(SERIALIZATION_BASE));
119 o.put("q", params.getQ().toString(SERIALIZATION_BASE));
120 return o;
121 }
123 @Override
124 public byte[] signMessage(byte[] bytes)
125 throws GeneralSecurityException {
126 if (bytes == null) {
127 throw new IllegalArgumentException("bytes must not be null");
128 }
129 final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
130 signer.initSign(privateKey);
131 signer.update(bytes);
132 final byte[] signature = signer.sign();
134 final byte[][] arrays = ASNUtils.decodeTwoArraysFromASN1(signature);
135 BigInteger r = new BigInteger(arrays[0]);
136 BigInteger s = new BigInteger(arrays[1]);
137 // This is awful, but signatures are always 40 bytes long.
138 byte[] decoded = Utils.concatAll(
139 Utils.hex2Byte(r.toString(16), SIGNATURE_LENGTH_BYTES / 2),
140 Utils.hex2Byte(s.toString(16), SIGNATURE_LENGTH_BYTES / 2));
141 return decoded;
142 }
143 }
145 public static BrowserIDKeyPair generateKeyPair(int keysize)
146 throws NoSuchAlgorithmException {
147 final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
148 keyPairGenerator.initialize(keysize);
149 final KeyPair keyPair = keyPairGenerator.generateKeyPair();
150 DSAPrivateKey privateKey = (DSAPrivateKey) keyPair.getPrivate();
151 DSAPublicKey publicKey = (DSAPublicKey) keyPair.getPublic();
152 return new BrowserIDKeyPair(new DSASigningPrivateKey(privateKey), new DSAVerifyingPublicKey(publicKey));
153 }
155 public static SigningPrivateKey createPrivateKey(BigInteger x, BigInteger p, BigInteger q, BigInteger g) throws NoSuchAlgorithmException, InvalidKeySpecException {
156 if (x == null) {
157 throw new IllegalArgumentException("x must not be null");
158 }
159 if (p == null) {
160 throw new IllegalArgumentException("p must not be null");
161 }
162 if (q == null) {
163 throw new IllegalArgumentException("q must not be null");
164 }
165 if (g == null) {
166 throw new IllegalArgumentException("g must not be null");
167 }
168 KeySpec keySpec = new DSAPrivateKeySpec(x, p, q, g);
169 KeyFactory keyFactory = KeyFactory.getInstance("DSA");
170 DSAPrivateKey privateKey = (DSAPrivateKey) keyFactory.generatePrivate(keySpec);
171 return new DSASigningPrivateKey(privateKey);
172 }
174 public static VerifyingPublicKey createPublicKey(BigInteger y, BigInteger p, BigInteger q, BigInteger g) throws NoSuchAlgorithmException, InvalidKeySpecException {
175 if (y == null) {
176 throw new IllegalArgumentException("n must not be null");
177 }
178 if (p == null) {
179 throw new IllegalArgumentException("p must not be null");
180 }
181 if (q == null) {
182 throw new IllegalArgumentException("q must not be null");
183 }
184 if (g == null) {
185 throw new IllegalArgumentException("g must not be null");
186 }
187 KeySpec keySpec = new DSAPublicKeySpec(y, p, q, g);
188 KeyFactory keyFactory = KeyFactory.getInstance("DSA");
189 DSAPublicKey publicKey = (DSAPublicKey) keyFactory.generatePublic(keySpec);
190 return new DSAVerifyingPublicKey(publicKey);
191 }
193 public static SigningPrivateKey createPrivateKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
194 String algorithm = o.getString("algorithm");
195 if (!"DS".equals(algorithm)) {
196 throw new InvalidKeySpecException("algorithm must equal DS, was " + algorithm);
197 }
198 try {
199 BigInteger x = new BigInteger(o.getString("x"), SERIALIZATION_BASE);
200 BigInteger p = new BigInteger(o.getString("p"), SERIALIZATION_BASE);
201 BigInteger q = new BigInteger(o.getString("q"), SERIALIZATION_BASE);
202 BigInteger g = new BigInteger(o.getString("g"), SERIALIZATION_BASE);
203 return createPrivateKey(x, p, q, g);
204 } catch (NullPointerException e) {
205 throw new InvalidKeySpecException("x, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE);
206 } catch (NumberFormatException e) {
207 throw new InvalidKeySpecException("x, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE);
208 }
209 }
211 public static VerifyingPublicKey createPublicKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
212 String algorithm = o.getString("algorithm");
213 if (!"DS".equals(algorithm)) {
214 throw new InvalidKeySpecException("algorithm must equal DS, was " + algorithm);
215 }
216 try {
217 BigInteger y = new BigInteger(o.getString("y"), SERIALIZATION_BASE);
218 BigInteger p = new BigInteger(o.getString("p"), SERIALIZATION_BASE);
219 BigInteger q = new BigInteger(o.getString("q"), SERIALIZATION_BASE);
220 BigInteger g = new BigInteger(o.getString("g"), SERIALIZATION_BASE);
221 return createPublicKey(y, p, q, g);
222 } catch (NullPointerException e) {
223 throw new InvalidKeySpecException("y, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE);
224 } catch (NumberFormatException e) {
225 throw new InvalidKeySpecException("y, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE);
226 }
227 }
229 public static BrowserIDKeyPair fromJSONObject(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
230 try {
231 ExtendedJSONObject privateKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PRIVATEKEY);
232 ExtendedJSONObject publicKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PUBLICKEY);
233 if (privateKey == null) {
234 throw new InvalidKeySpecException("privateKey must not be null");
235 }
236 if (publicKey == null) {
237 throw new InvalidKeySpecException("publicKey must not be null");
238 }
239 return new BrowserIDKeyPair(createPrivateKey(privateKey), createPublicKey(publicKey));
240 } catch (NonObjectJSONException e) {
241 throw new InvalidKeySpecException("privateKey and publicKey must be JSON objects");
242 }
243 }
244 }