/*******************************************************************************
* Copyright (c) 2013 Lectorius, Inc.
* Authors:
* Vijay Pandurangan (vijayp@mitro.co)
* Evan Jones (ej@mitro.co)
* Adam Hilss (ahilss@mitro.co)
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can contact the authors at inbound@mitro.co.
*******************************************************************************/
package co.mitro.core.crypto;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Arrays;
import org.junit.Test;
import org.keyczar.exceptions.Base64DecodingException;
import org.keyczar.exceptions.KeyNotFoundException;
import org.keyczar.exceptions.KeyczarException;
import org.keyczar.exceptions.ShortSignatureException;
import org.keyczar.util.Base64Coder;
import co.mitro.core.crypto.KeyInterfaces.CryptoError;
import co.mitro.core.crypto.KeyInterfaces.KeyFactory;
import co.mitro.core.crypto.KeyInterfaces.PrivateKeyInterface;
import co.mitro.core.crypto.KeyInterfaces.PublicKeyInterface;
import com.google.common.io.CharStreams;
public class KeyInterfacesTest {
private static final String makeLong(String in) {
StringBuilder out = new StringBuilder(in);
if (out.length() == 0) {
out.append('a');
}
while (out.length() < 1024) {
out.append(out);
}
return out.toString();
}
public void testGenericCrypto(KeyFactory keyFactory) throws CryptoError {
PrivateKeyInterface privateKey = keyFactory.generate();
final String MESSAGE = "world";
String signature = privateKey.sign(MESSAGE);
PublicKeyInterface publicKey = privateKey.exportPublicKey();
assertTrue(publicKey.verify(MESSAGE, signature));
assertTrue(privateKey.verify(MESSAGE, signature));
// modified message or signature fails
assertFalse(publicKey.verify(MESSAGE + "a", signature));
char diffLastChar = signature.charAt(signature.length() - 1);
diffLastChar = (diffLastChar == '0') ? '1' : '0';
String modifiedSignature = signature.substring(0, signature.length()-1) + diffLastChar;
// either an exception or fail verification
assertFalse(publicKey.verify(MESSAGE, modifiedSignature));
// we should either get an exception or this should fail
try {
boolean result = publicKey.verify(MESSAGE, signature + "A");
assertFalse(result);
} catch (CryptoError e) {}
// verification with a different key should fail (exception or false)
PrivateKeyInterface privateKey2 = privateKey;
// in a loop because CrappyCrypto has 1/100 probability of generating the same key
while (privateKey2.toString().equals(privateKey.toString())) {
privateKey2 = keyFactory.generate();
}
PublicKeyInterface publicKey2 = privateKey2.exportPublicKey();
try {
boolean result = publicKey2.verify(MESSAGE, signature);
assertFalse(result);
} catch (CryptoError e) {}
// test encryption with long messages (sessions!)
String longMessage = makeLong(MESSAGE);
assertEquals(longMessage, privateKey.decrypt(publicKey.encrypt(longMessage)));
assertEquals(longMessage, privateKey2.decrypt(publicKey2.encrypt(longMessage)));
// Verify that round tripping the key to/from string works
PrivateKeyInterface roundTripped = keyFactory.loadPrivateKey(privateKey.toString());
assertEquals("message", roundTripped.decrypt(privateKey.encrypt("message")));
// Verify that exporting private keys works
String exported = privateKey.exportEncrypted("password");
String exported2 = privateKey.exportEncrypted("password2");
assertTrue(!exported.equals(exported2));
assertTrue(!exported.equals(privateKey.toString()));
roundTripped = keyFactory.loadEncryptedPrivateKey(exported2, "password2");
assertEquals("message", roundTripped.decrypt(privateKey.encrypt("message")));
}
public static String loadResource(String path) throws IOException {
Reader reader = new InputStreamReader(
KeyInterfacesTest.class.getResourceAsStream(path), "UTF-8");
return CharStreams.toString(reader);
}
public static PrivateKeyInterface loadTestKey() {
try {
KeyczarKeyFactory keyFactory = new KeyczarKeyFactory();
PrivateKeyInterface privateKey = keyFactory.loadPrivateKey(loadResource("privatekey.json"));
return privateKey;
} catch (IOException e) {
throw new RuntimeException(e);
} catch (CryptoError e) {
throw new RuntimeException(e);
}
}
@Test
public void testKeyczar() throws CryptoError, IOException {
KeyczarKeyFactory keyFactory = new KeyczarKeyFactory();
testGenericCrypto(keyFactory);
PrecomputingKeyczarKeyFactory pkf = new PrecomputingKeyczarKeyFactory();
testGenericCrypto(pkf);
// verify Unicode round-tripping to/from JavaScript
PublicKeyInterface publicKey = keyFactory.loadPublicKey(loadResource("publickey.json"));
String unicodeMessage = loadResource("utf8.txt");
String signature = loadResource("utf8_signature.txt");
assertTrue(publicKey.verify(unicodeMessage, signature));
PrivateKeyInterface privateKey = loadTestKey();
String sig2 = privateKey.sign(unicodeMessage);
assertEquals(signature, sig2);
String encrypted = loadResource("utf8_encrypted.txt");
assertEquals(unicodeMessage, privateKey.decrypt(encrypted));
// verify we can load encrypted keys from javascript and that it works
String encryptedKey = loadResource("privatekey_encrypted.json");
privateKey = keyFactory.loadEncryptedPrivateKey(encryptedKey, "hellopass");
assertEquals("message", privateKey.decrypt(privateKey.encrypt("message")));
assertTrue(privateKey.verify("message", privateKey.sign("message")));
}
@Test
public void testCrappy() throws CryptoError, IOException {
CrappyKeyFactory keyFactory = new CrappyKeyFactory();
testGenericCrypto(keyFactory);
// Keys and values from Javascript
PublicKeyInterface publicKey = keyFactory.loadPublicKey("{\"type\":\"PUB\",\"key\":\"8\"}");
assertTrue(publicKey.verify("this is a longish string wtf", "397913811"));
}
@Test
public void testKeyczarExceptions() throws CryptoError, IOException, Base64DecodingException {
// Keyczar throws a bunch of runtime exceptions under "error" conditions; check what happens
// TODO: Possibly some of these should be upstream bugs?
KeyFactory keyFactory = new KeyczarKeyFactory();
PrivateKeyInterface privateKey = keyFactory.loadPrivateKey(loadResource("privatekey.json"));
try {
privateKey.verify("message", null);
fail("expected exception");
} catch (NullPointerException ignored) {}
try {
privateKey.verify(null, "signature");
fail("expected exception");
} catch (NullPointerException ignored) {}
// verifying with an empty signature = ArrayIndexOutOfBounds
try {
privateKey.verify("message", "");
fail("expected exception");
} catch (ArrayIndexOutOfBoundsException ignored) {}
// Checked exceptions: maybe these should return false: signature is invalid?
try {
privateKey.verify("message", "A");
fail("expected exception");
} catch (CryptoError ignored) {
assertTrue(ignored.getCause() instanceof Base64DecodingException);
}
try {
privateKey.verify("message", "AA");
fail("expected exception");
} catch (CryptoError ignored) {
assertTrue(ignored.getCause() instanceof ShortSignatureException);
}
// wrong key: KeyNotFoundException (happens when we AddIdentity twice)
try {
privateKey.verify("message", "AAAAAAA");
fail("expected exception");
} catch (CryptoError ignored) {
assertTrue(ignored.getCause() instanceof KeyNotFoundException);
}
String valid = privateKey.sign("message");
byte[] decoded = Base64Coder.decodeWebSafe(valid);
// too short
try {
byte[] tooShort = Arrays.copyOf(decoded, decoded.length-1);
privateKey.verify("message", Base64Coder.encodeWebSafe(tooShort));
fail("expected exception");
} catch (CryptoError ignored) {
assertTrue(ignored.getCause() instanceof KeyczarException);
assertTrue(ignored.getCause().getCause() instanceof java.security.GeneralSecurityException);
}
// too long
try {
byte[] tooLong = Arrays.copyOf(decoded, decoded.length+1);
privateKey.verify("message", Base64Coder.encodeWebSafe(tooLong));
fail("expected exception");
} catch (CryptoError ignored) {
assertTrue(ignored.getCause() instanceof KeyczarException);
assertTrue(ignored.getCause().getCause() instanceof java.security.GeneralSecurityException);
}
// actual true/false values
decoded[decoded.length-1] += 1;
assertFalse(privateKey.verify("message", Base64Coder.encodeWebSafe(decoded)));
decoded[decoded.length-1] -= 1;
assertTrue(privateKey.verify("message", Base64Coder.encodeWebSafe(decoded)));
assertFalse(privateKey.verify("messag", Base64Coder.encodeWebSafe(decoded)));
}
}