[liberationtech] Trsst Encryption (was: About Telegram)
Michael Powers
michael at mpowers.net
Wed Mar 19 09:01:51 PDT 2014
This seems as good a time as any to try to get some eyes on the encryption part of Trsst. I've been soliciting feedback on this from the various crypto lists as we're expanding our testing in the next day or so, but haven't yet gotten any takers.
Background:
Trsst is a convention for using Atom and the Atom Publishing Protocol with XML-Signatures and XML-Encryption as a foundation for building interoperable microblogging services (think: open replacements for Twitter/Facebook) that support decentralized account creation (just generate a keypair) and signed and optionally encrypted public entries.
FAQ is here: https://github.com/TrsstProject/trsst/wiki/Frequently-Asked-Questions
Complete reference implementation is here: https://github.com/TrsstProject/trsst
For a private message, we generate a random 256-bit key and encrypt with AES. Then for each recipient, we use a hash of the shared ECDH secret and the message-id to encrypt the key and append it to the message. We're xor ciphering because the inputs are random-ish keys and we need it to be simple and fast. All public keys are static, and all public and private messages are viewable by anyone.
Things we'd like some feedback on:
(1) An account is permanently associated with one EC keypair for signing, and another optional keypair for encryption which can changed but probably not often if ever. How much of a problem would it be to use the same permanent keypair for both signing and encryption? Moving/copying your content to a new account is the recourse for a lost or compromised key, and isn't terribly costly.
(2) Private messages are not always addressed outside the encryption envelope. If unaddressed, you have to try each of the keys on a message to see if it is intended for you, which is why we hash the key. Could the pattern of first half of the encrypted data always being a random key and the second half always being a hash of that key be exploited?
(3) To ensure you can decrypt your own message, you encode one of the keys for yourself. Might your private key be compromised when used with your own public key moreso than someone else's public key?
The relevant implementation in Java using Bouncy Castle is below.
Many thanks for pointing out issues and errors, glaring or otherwise.
- Michael
-----
/**
* Takes the specified 32 bytes, appends its sha-256 digest, and xor
* encrypts those 64 bytes with the sha-512 hash of the ECDH shared secret
* and the entry id.
*
* @param input 32 byte key to be encrypted
* @param publicKey
* @param privateKey
* @return
* @throws SecurityException if unexpected error
*/
public static byte[] encryptKeyWithECDH(byte[] input, long entryId,
PublicKey publicKey, PrivateKey privateKey)
throws SecurityException {
assert input.length == 32; // 256 bit key
byte[] result = null;
try {
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH", "BC");
keyAgreement.init(privateKey);
keyAgreement.doPhase(publicKey, true);
byte[] sharedSecret = keyAgreement.generateSecret();
// generate 512 bits using shared secret and entry id
MessageDigest sha512 = MessageDigest.getInstance("SHA-512");
sha512.update(sharedSecret);
sha512.update(ByteBuffer.allocate(8).putLong(entryId));
byte[] sharedHash = sha512.digest();
// calculate a digest of the input
byte[] digest = MessageDigest.getInstance("SHA-256").digest(input);
// xor the key and the digest against the shared hash
int i;
result = new byte[64];
for (i = 0; i < 32; i++) {
result[i] = (byte) (input[i] ^ sharedHash[i]);
}
for (i = 0; i < 32; i++) {
result[i + 32] = (byte) (digest[i] ^ sharedHash[i + 32]);
}
} catch (Exception e) {
log.error("Error while encrypting element", e);
throw new SecurityException(e);
}
return result;
}
/**
* Takes the specified 64 byte encoded input and xor decrypts it with the
* sha-512 hash of the ECDH shared secret and the entry id. Then checks to
* see if the last 32 bytes is the sha-256 hash of the first 32 bytes. If
* so, returns the first 32 bytes of the decrypted content. Otherwise, then
* this key was not intended for us, and returns null.
*
* @param input 64 byte input to be decrypted
* @param publicKey
* @param privateKey
* @return the original 32 byte input, or null if unintended recipient.
* @throws SecurityException if unexpected error
*/
public static byte[] decryptKeyWithECDH(byte[] input, long entryId,
PublicKey publicKey, PrivateKey privateKey)
throws SecurityException {
assert input.length == 64; // 512 bit encrypted key
try {
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH", "BC");
keyAgreement.init(privateKey);
keyAgreement.doPhase(publicKey, true);
byte[] sharedSecret = keyAgreement.generateSecret();
// generate 512 bits using shared secret and entry id
MessageDigest sha512 = MessageDigest.getInstance("SHA-512");
sha512.update(sharedSecret);
sha512.update(ByteBuffer.allocate(8).putLong(entryId));
byte[] sharedHash = sha512.digest();
// xor the key and the digest against the shared hash
int i;
byte[] decoded = new byte[64];
for (i = 0; i < 64; i++) {
decoded[i] = (byte) (input[i] ^ sharedHash[i]);
}
// calculate digest of the decoded key
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
sha256.update(decoded, 0, 32);
byte[] digest = sha256.digest();
// verify that the digest of first 32 bytes matches last 32 bytes
for (i = 0; i < 32; i++) {
if (digest[i] != decoded[i + 32]) {
// incorrectly decoded: we're not the intended recipient
return null;
}
}
return Arrays.copyOfRange(decoded, 0, 32);
} catch (Exception e) {
log.error("Error while decrypting element", e);
throw new SecurityException(e);
}
}
public static byte[] generateAESKey() {
byte[] result = new byte[32];
new SecureRandom().nextBytes(result);
return result;
}
public static byte[] encryptAES(byte[] input, byte[] key)
throws InvalidCipherTextException {
return _cryptBytesAES(input, key, true);
}
public static byte[] decryptAES(byte[] input, byte[] key)
throws InvalidCipherTextException {
return _cryptBytesAES(input, key, false);
}
private static byte[] _cryptBytesAES(byte[] input, byte[] key,
boolean forEncryption) throws InvalidCipherTextException {
assert key.length == 32; // 32 bytes == 256 bits
CipherParameters cipherParameters = new KeyParameter(key);
BlockCipher blockCipher = new AESEngine();
BlockCipherPadding blockCipherPadding = new ZeroBytePadding();
BufferedBlockCipher bufferedBlockCipher = new PaddedBufferedBlockCipher(
blockCipher, blockCipherPadding);
return process(input, bufferedBlockCipher, cipherParameters,
forEncryption);
}
private static byte[] process(byte[] input,
BufferedBlockCipher bufferedBlockCipher,
CipherParameters cipherParameters, boolean forEncryption)
throws InvalidCipherTextException {
bufferedBlockCipher.init(forEncryption, cipherParameters);
int inputOffset = 0;
int inputLength = input.length;
int maximumOutputLength = bufferedBlockCipher
.getOutputSize(inputLength);
byte[] output = new byte[maximumOutputLength];
int outputOffset = 0;
int outputLength = 0;
int bytesProcessed;
bytesProcessed = bufferedBlockCipher.processBytes(input, inputOffset,
inputLength, output, outputOffset);
outputOffset += bytesProcessed;
outputLength += bytesProcessed;
bytesProcessed = bufferedBlockCipher.doFinal(output, outputOffset);
outputOffset += bytesProcessed;
outputLength += bytesProcessed;
if (outputLength == output.length) {
return output;
} else {
byte[] truncatedOutput = new byte[outputLength];
System.arraycopy(output, 0, truncatedOutput, 0, outputLength);
return truncatedOutput;
}
}
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mailman.stanford.edu/pipermail/liberationtech/attachments/20140319/a5cc5c2a/attachment.html>
More information about the liberationtech
mailing list