1/**
2 * @license
3 * Copyright 2016 Google Inc. All rights reserved.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *   http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.security.wycheproof;
18
19import java.nio.ByteBuffer;
20import java.security.GeneralSecurityException;
21import java.security.KeyPair;
22import java.security.KeyPairGenerator;
23import java.security.NoSuchAlgorithmException;
24import java.security.PrivateKey;
25import java.security.PublicKey;
26import java.security.interfaces.ECPrivateKey;
27import java.security.interfaces.ECPublicKey;
28import java.security.spec.ECGenParameterSpec;
29import java.util.Arrays;
30import java.util.HashSet;
31import javax.crypto.Cipher;
32import junit.framework.TestCase;
33
34/**
35 * Testing ECIES.
36 *
37 * @author bleichen@google.com (Daniel Bleichenbacher)
38 */
39// Tested providers:
40// BouncyCastle v 1.52: IESCipher is amazingly buggy, both from a crypto
41// viewpoint and from an engineering viewpoint. It uses encryption modes that are completely
42// inapproriate for ECIES or DHIES (i.e. ECB), the CBC implementation distinguishes between
43// padding and MAC failures allowing adaptive chosen-ciphertext attacks. The implementation
44// allows to specify paddings, but ignores them, encryption using ByteBuffers doesn't even work
45// without exceptions, indicating that this hasn't even tested.
46//
47// <p>TODO(bleichen):
48// - compressed points,
49// - maybe again CipherInputStream, CipherOutputStream,
50// - BouncyCastle has a KeyPairGenerator for ECIES. Is this one different from EC?
51public class EciesTest extends TestCase {
52
53  int expectedCiphertextLength(String algorithm, int coordinateSize, int messageLength)
54      throws Exception {
55    switch (algorithm.toUpperCase()) {
56      case "ECIESWITHAES-CBC":
57        // Uses the encoding
58        // 0x04 || coordinate x || coordinate y || PKCS5 padded ciphertext || 20-byte HMAC-digest.
59        return 1 + (2 * coordinateSize) + (messageLength - messageLength % 16 + 16) + 20;
60      default:
61        fail("Not implemented");
62    }
63    return -1;
64  }
65
66  /**
67   * Check that key agreement using ECIES works. This example does not specify an IESParametersSpec.
68   * BouncyCastle v.1.52 uses the following algorithms: KDF2 with SHA1 for the key derivation
69   * AES-CBC with PKCS #5 padding. HMAC-SHA1 with a 20 byte digest. The AES and the HMAC key are
70   * both 128 bits.
71   */
72  @SuppressWarnings("InsecureCryptoUsage")
73  public void testEciesBasic() throws Exception {
74    ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
75    KeyPairGenerator kf = KeyPairGenerator.getInstance("EC");
76    kf.initialize(ecSpec);
77    KeyPair keyPair = kf.generateKeyPair();
78    PrivateKey priv = keyPair.getPrivate();
79    PublicKey pub = keyPair.getPublic();
80    byte[] message = "Hello".getBytes("UTF-8");
81    Cipher ecies = Cipher.getInstance("ECIESwithAES-CBC");
82    ecies.init(Cipher.ENCRYPT_MODE, pub);
83    byte[] ciphertext = ecies.doFinal(message);
84    System.out.println("testEciesBasic:" + TestUtil.bytesToHex(ciphertext));
85    ecies.init(Cipher.DECRYPT_MODE, priv, ecies.getParameters());
86    byte[] decrypted = ecies.doFinal(ciphertext);
87    assertEquals(TestUtil.bytesToHex(message), TestUtil.bytesToHex(decrypted));
88  }
89
90  /**
91   * ECIES does not allow encryption modes and paddings. If this test fails then we should add
92   * additional tests covering the new algorithms.
93   */
94  // TODO(bleichen): This test describes BouncyCastles behaviour, but not necessarily what we
95  // expect.
96  @SuppressWarnings("InsecureCryptoUsage")
97  public void testInvalidNames() throws Exception {
98    String[] invalidNames =
99        new String[] {
100          "ECIESWITHAES/CBC/PKCS5PADDING",
101          "ECIESWITHAES/CBC/PKCS7PADDING",
102          "ECIESWITHAES/DHAES/NOPADDING",
103          "ECIESWITHDESEDE/DHAES/NOPADDING",
104          "ECIESWITHAES/ECB/NOPADDING",
105          "ECIESWITHAES/CTR/NOPADDING",
106        };
107    for (String algorithm : invalidNames) {
108      try {
109        Cipher.getInstance(algorithm);
110        fail("unexpected algorithm:" + algorithm);
111      } catch (NoSuchAlgorithmException ex) {
112        // this is expected
113      }
114    }
115  }
116
117  /** Here are a few names that BouncyCastle accepts. */
118  // TODO(bleichen): This test describes BouncyCastles behaviour, but not necessarily what we
119  // expect.
120  @SuppressWarnings("InsecureCryptoUsage")
121  public void testValidNames() throws Exception {
122    String[] validNames =
123        new String[] {
124          "ECIES/DHAES/PKCS7PADDING",
125          "ECIESWITHAES-CBC/NONE/NOPADDING",
126        };
127    for (String algorithm : validNames) {
128      Cipher.getInstance(algorithm);
129    }
130  }
131
132  /**
133   * BouncyCastle has a key generation algorithm "ECIES". This test checks that the result are
134   * ECKeys in both cases.
135   */
136  public void testKeyGeneration() throws Exception {
137    ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
138    KeyPairGenerator kf = KeyPairGenerator.getInstance("ECIES");
139    kf.initialize(ecSpec);
140    KeyPair keyPair = kf.generateKeyPair();
141    ECPrivateKey priv = (ECPrivateKey) keyPair.getPrivate();
142    ECPublicKey pub = (ECPublicKey) keyPair.getPublic();
143  }
144
145  /**
146   * Tries to decrypt ciphertexts where the symmetric part has been randomized.
147   * If this randomization leads to distinguishable exceptions then this may indicate that the
148   * implementation is vulnerable to a padding attack.
149   *
150   * Problems detected:
151   * <ul>
152   * <li> CVE-2016-1000345 BouncyCastle before v.1.56 is vulnerable to a padding oracle attack.
153   * </ul>
154   */
155  @SuppressWarnings("InsecureCryptoUsage")
156  public void testExceptions(String algorithm) throws Exception {
157    Cipher ecies;
158    try {
159      ecies = Cipher.getInstance(algorithm);
160    } catch (NoSuchAlgorithmException ex) {
161      // Allowing to skip the algorithm
162      System.out.println("No implementation for:" + algorithm);
163      return;
164    }
165    ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
166    final int kemSize = 65;
167    KeyPairGenerator kf = KeyPairGenerator.getInstance("EC");
168    kf.initialize(ecSpec);
169    KeyPair keyPair = kf.generateKeyPair();
170    PrivateKey priv = keyPair.getPrivate();
171    PublicKey pub = keyPair.getPublic();
172    byte[] message = new byte[40];
173    ecies.init(Cipher.ENCRYPT_MODE, pub);
174    byte[] ciphertext = ecies.doFinal(message);
175    System.out.println(TestUtil.bytesToHex(ciphertext));
176    ecies.init(Cipher.DECRYPT_MODE, priv, ecies.getParameters());
177    HashSet<String> exceptions = new HashSet<String>();
178    for (int byteNr = kemSize; byteNr < ciphertext.length; byteNr++) {
179      for (int bit = 0; bit < 8; bit++) {
180        byte[] corrupt = Arrays.copyOf(ciphertext, ciphertext.length);
181        corrupt[byteNr] ^= (byte) (1 << bit);
182        ecies.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
183        try {
184          ecies.doFinal(corrupt);
185          fail("Decrypted:" + TestUtil.bytesToHex(corrupt));
186        } catch (Exception ex) {
187          String exception = ex.toString();
188          if (exceptions.add(exception)) {
189            System.out.println(algorithm + ":" + exception);
190          }
191        }
192      }
193    }
194    assertEquals(1, exceptions.size());
195  }
196
197  public void testEciesCorruptDefault() throws Exception {
198    testExceptions("ECIES");
199  }
200
201  @SuppressWarnings("InsecureCryptoUsage")
202  public void testModifyPoint() throws Exception {
203    ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
204    KeyPairGenerator kf = KeyPairGenerator.getInstance("EC");
205    kf.initialize(ecSpec);
206    KeyPair keyPair = kf.generateKeyPair();
207    PrivateKey priv = keyPair.getPrivate();
208    PublicKey pub = keyPair.getPublic();
209    byte[] message = "This is a long text since we need 32 bytes.".getBytes("UTF-8");
210    Cipher ecies = Cipher.getInstance("ECIESwithAES-CBC");
211    ecies.init(Cipher.ENCRYPT_MODE, pub);
212    byte[] ciphertext = ecies.doFinal(message);
213    ciphertext[2] ^= (byte) 1;
214    ecies.init(Cipher.DECRYPT_MODE, priv, ecies.getParameters());
215    try {
216      ecies.doFinal(ciphertext);
217      fail("This should not work");
218    } catch (GeneralSecurityException ex) {
219      // This is as expected
220      // Bouncy Castle 1.56 throws this exception
221    } catch (Exception ex) {
222      fail("Expected subclass of java.security.GeneralSecurityException, but got: "
223        + ex.getClass().getName());
224    }
225  }
226
227  /**
228   * This test tries to detect ECIES implementations using ECB. This is insecure and also violates
229   * the claims of ECIES, since ECIES is secure agains adaptive chosen-ciphertext attacks.
230   */
231  @SuppressWarnings("InsecureCryptoUsage")
232  public void testNotEcb(String algorithm) throws Exception {
233    Cipher ecies;
234    try {
235      ecies = Cipher.getInstance(algorithm);
236    } catch (NoSuchAlgorithmException ex) {
237      // This test is called with short algorithm names such as just "ECIES".
238      // Requiring full names is typically a good practice. Hence it is OK
239      // to not assigning default algorithms.
240      System.out.println("No implementation for:" + algorithm);
241      return;
242    }
243    ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
244    KeyPairGenerator kf = KeyPairGenerator.getInstance("EC");
245    kf.initialize(ecSpec);
246    KeyPair keyPair = kf.generateKeyPair();
247    PublicKey pub = keyPair.getPublic();
248    byte[] message = new byte[512];
249    ecies.init(Cipher.ENCRYPT_MODE, pub);
250    byte[] ciphertext = ecies.doFinal(message);
251    String block1 = TestUtil.bytesToHex(Arrays.copyOfRange(ciphertext, 241, 257));
252    String block2 = TestUtil.bytesToHex(Arrays.copyOfRange(ciphertext, 257, 273));
253    assertTrue("Ciphertext repeats:" + TestUtil.bytesToHex(ciphertext), !block1.equals(block2));
254  }
255
256  public void testDefaultEcies() throws Exception {
257    testNotEcb("ECIES");
258  }
259
260  /**
261   * Tests whether algorithmA is an alias of algorithmB by encrypting with algorithmA and decrypting
262   * with algorithmB.
263   */
264  @SuppressWarnings("InsecureCryptoUsage")
265  public void testIsAlias(String algorithmA, String algorithmB) throws Exception {
266    Cipher eciesA;
267    Cipher eciesB;
268    // Allowing tests to be skipped, because we don't want to encourage abbreviations.
269    try {
270      eciesA = Cipher.getInstance(algorithmA);
271    } catch (NoSuchAlgorithmException ex) {
272      System.out.println("Skipping because of:" + ex.toString());
273      return;
274    }
275    try {
276      eciesB = Cipher.getInstance(algorithmB);
277    } catch (NoSuchAlgorithmException ex) {
278      System.out.println("Skipping because of:" + ex.toString());
279      return;
280    }
281    ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
282    KeyPairGenerator kf = KeyPairGenerator.getInstance("EC");
283    kf.initialize(ecSpec);
284    KeyPair keyPair = kf.generateKeyPair();
285    byte[] message = "Hello".getBytes("UTF-8");
286    eciesA.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
287    byte[] ciphertext = eciesA.doFinal(message);
288    eciesB.init(Cipher.DECRYPT_MODE, keyPair.getPrivate(), eciesB.getParameters());
289    byte[] decrypted = eciesB.doFinal(ciphertext);
290    assertEquals(TestUtil.bytesToHex(message), TestUtil.bytesToHex(decrypted));
291  }
292
293  /** Tests whether two distinct algorithm names implement the same cipher */
294  public void testAlias() throws Exception {
295    testIsAlias("ECIESWITHAES-CBC", "ECIESWithAES-CBC");
296    testIsAlias("ECIESWITHAES", "ECIESWithAES");
297    // BouncyCastle v 1.52 ignores mode and padding and considers the following
298    // names as equivalent:
299    // testIsAlias("ECIES/DHAES/PKCS7PADDING", "ECIES");
300    testIsAlias("ECIESWITHAES-CBC/NONE/PKCS7PADDING", "ECIESWITHAES-CBC/NONE/NOPADDING");
301  }
302
303  /**
304   * Cipher.doFinal(ByteBuffer, ByteBuffer) should be copy-safe according to
305   * https://docs.oracle.com/javase/7/docs/api/javax/crypto/Cipher.html
306   *
307   * <p>This test tries to verify this.
308   */
309  /* TODO(bleichen): There's no point to run this test as long as the previous basic test fails.
310   public void testByteBufferAlias() throws Exception {
311     byte[] message = "Hello".getBytes("UTF-8");
312     String algorithm = "ECIESWithAES-CBC";
313     ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
314     KeyPairGenerator kf = KeyPairGenerator.getInstance("EC");
315     kf.initialize(ecSpec);
316     KeyPair keyPair = kf.generateKeyPair();
317     Cipher ecies = Cipher.getInstance(algorithm);
318
319     int ciphertextLength = expectedCiphertextLength(algorithm, 32, message.length);
320     byte[] backingArray = new byte[ciphertextLength];
321     ByteBuffer ptBuffer = ByteBuffer.wrap(backingArray);
322     ptBuffer.put(message);
323     ptBuffer.flip();
324
325     ecies.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
326     ByteBuffer ctBuffer = ByteBuffer.wrap(backingArray);
327     ecies.doFinal(ptBuffer, ctBuffer);
328     ctBuffer.flip();
329
330     ecies.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
331     byte[] decrypted = ecies.doFinal(backingArray, 0, ctBuffer.remaining());
332     assertEquals(TestUtil.bytesToHex(message), TestUtil.bytesToHex(decrypted));
333   }
334  */
335}
336