|
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/. */ |
|
4 |
|
5 package org.mozilla.gecko.browserid; |
|
6 |
|
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; |
|
21 |
|
22 import org.mozilla.gecko.sync.ExtendedJSONObject; |
|
23 import org.mozilla.gecko.sync.NonObjectJSONException; |
|
24 import org.mozilla.gecko.sync.Utils; |
|
25 |
|
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. |
|
29 |
|
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; |
|
37 |
|
38 protected static class DSAVerifyingPublicKey implements VerifyingPublicKey { |
|
39 protected final DSAPublicKey publicKey; |
|
40 |
|
41 public DSAVerifyingPublicKey(DSAPublicKey publicKey) { |
|
42 this.publicKey = publicKey; |
|
43 } |
|
44 |
|
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 } |
|
62 |
|
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)); |
|
85 |
|
86 final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM); |
|
87 signer.initVerify(publicKey); |
|
88 signer.update(bytes); |
|
89 return signer.verify(encoded); |
|
90 } |
|
91 } |
|
92 |
|
93 protected static class DSASigningPrivateKey implements SigningPrivateKey { |
|
94 protected final DSAPrivateKey privateKey; |
|
95 |
|
96 public DSASigningPrivateKey(DSAPrivateKey privateKey) { |
|
97 this.privateKey = privateKey; |
|
98 } |
|
99 |
|
100 @Override |
|
101 public String getAlgorithm() { |
|
102 return "DS" + (privateKey.getParams().getP().bitLength() + 7)/8; |
|
103 } |
|
104 |
|
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 } |
|
122 |
|
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(); |
|
133 |
|
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 } |
|
144 |
|
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 } |
|
154 |
|
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 } |
|
173 |
|
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 } |
|
192 |
|
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 } |
|
210 |
|
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 } |
|
228 |
|
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 } |