You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
148 lines
5.1 KiB
Java
148 lines
5.1 KiB
Java
/*
|
|
* Copyright (C) 2011 Whisper Systems
|
|
*
|
|
* 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/>.
|
|
*/
|
|
|
|
package org.thoughtcrime.redphone.crypto;
|
|
|
|
import org.thoughtcrime.securesms.util.Base64;
|
|
|
|
import java.io.IOException;
|
|
import java.security.InvalidAlgorithmParameterException;
|
|
import java.security.InvalidKeyException;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.util.Arrays;
|
|
|
|
import javax.crypto.BadPaddingException;
|
|
import javax.crypto.Cipher;
|
|
import javax.crypto.IllegalBlockSizeException;
|
|
import javax.crypto.Mac;
|
|
import javax.crypto.NoSuchPaddingException;
|
|
import javax.crypto.spec.IvParameterSpec;
|
|
import javax.crypto.spec.SecretKeySpec;
|
|
|
|
/**
|
|
* A class representing an encrypted "signal," typically a notification of
|
|
* an incoming call that's delivered over either SMS or C2DM.
|
|
*
|
|
* Message format is $IV:ciphertext:MAC
|
|
*
|
|
* Version : 1 Byte
|
|
* IV : Random.
|
|
* Ciphertext: AES-128 encrypted with CBC mode.
|
|
* MAC : Hmac-SHA1, truncated to 80 bits over everything preceding (encrypted then auth).
|
|
|
|
* @author Moxie Marlinspike
|
|
*
|
|
*/
|
|
|
|
public class EncryptedSignalMessage {
|
|
|
|
private static final int VERSION_OFFSET = 0;
|
|
private static final int IV_OFFSET = VERSION_OFFSET + 1;
|
|
private static final int CIPHERTEXT_OFFSET = IV_OFFSET + 16;
|
|
private static final int MAC_LENGTH = 10;
|
|
|
|
private final String message;
|
|
private final byte[] key;
|
|
|
|
public EncryptedSignalMessage(String message, String key) throws IOException {
|
|
this.message = message;
|
|
this.key = Base64.decode(key);
|
|
}
|
|
|
|
private byte[] getCipherKey() throws InvalidEncryptedSignalException, IOException {
|
|
byte[] cipherKeyBytes = new byte[16];
|
|
|
|
System.arraycopy(key, 0, cipherKeyBytes, 0, cipherKeyBytes.length);
|
|
return cipherKeyBytes;
|
|
}
|
|
|
|
private byte[] getMacKey() throws InvalidEncryptedSignalException, IOException {
|
|
byte[] macKeyBytes = new byte[20];
|
|
|
|
System.arraycopy(key, 16, macKeyBytes, 0, macKeyBytes.length);
|
|
return macKeyBytes;
|
|
}
|
|
|
|
private boolean verifyMac(byte[] messageBytes)
|
|
throws InvalidEncryptedSignalException, IOException,
|
|
NoSuchAlgorithmException, InvalidKeyException
|
|
{
|
|
byte[] macKey = getMacKey();
|
|
SecretKeySpec key = new SecretKeySpec(macKey, "HmacSHA1");
|
|
Mac mac = Mac.getInstance("HmacSHA1");
|
|
|
|
mac.init(key);
|
|
mac.update(messageBytes, 0, messageBytes.length-MAC_LENGTH);
|
|
|
|
byte[] ourDigestComplete = mac.doFinal();
|
|
byte[] ourDigest = new byte[10];
|
|
byte[] theirDigest = new byte[10];
|
|
|
|
System.arraycopy(ourDigestComplete, 10, ourDigest, 0, ourDigest.length);
|
|
System.arraycopy(messageBytes, messageBytes.length - MAC_LENGTH,
|
|
theirDigest, 0, theirDigest.length);
|
|
|
|
return Arrays.equals(ourDigest, theirDigest);
|
|
}
|
|
|
|
private boolean isValidVersion(byte[] messageBytes) {
|
|
return messageBytes[VERSION_OFFSET] == 0x00;
|
|
}
|
|
|
|
private Cipher getCipher(byte[] messageBytes)
|
|
throws InvalidEncryptedSignalException, InvalidKeyException,
|
|
InvalidAlgorithmParameterException, NoSuchAlgorithmException,
|
|
NoSuchPaddingException, IOException
|
|
{
|
|
SecretKeySpec cipherKey = new SecretKeySpec(getCipherKey(), "AES");
|
|
byte[] ivBytes = new byte[16];
|
|
|
|
if (messageBytes.length < ivBytes.length)
|
|
throw new InvalidEncryptedSignalException("Message shorter than IV length.");
|
|
|
|
System.arraycopy(messageBytes, IV_OFFSET, ivBytes, 0, ivBytes.length);
|
|
|
|
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
|
cipher.init(Cipher.DECRYPT_MODE, cipherKey, iv);
|
|
|
|
return cipher;
|
|
}
|
|
|
|
public byte[] getPlaintext() throws InvalidEncryptedSignalException {
|
|
try {
|
|
byte[] messageBytes = Base64.decode(this.message);
|
|
|
|
if (!isValidVersion(messageBytes))
|
|
throw new InvalidEncryptedSignalException("Unknown version: " +
|
|
(byte)messageBytes[VERSION_OFFSET]);
|
|
|
|
if (!verifyMac(messageBytes))
|
|
throw new InvalidEncryptedSignalException("Bad MAC");
|
|
|
|
Cipher cipher = getCipher(messageBytes);
|
|
return cipher.doFinal(messageBytes, CIPHERTEXT_OFFSET,
|
|
messageBytes.length - CIPHERTEXT_OFFSET - MAC_LENGTH);
|
|
} catch (IOException | BadPaddingException | IllegalBlockSizeException | InvalidKeyException e) {
|
|
throw new InvalidEncryptedSignalException(e);
|
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
|
|
}
|