/*
* Copyright 2012 The Stanford MobiSocial Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mobisocial.musubi.encoding;
import gnu.trove.map.hash.TLongLongHashMap;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import mobisocial.crypto.CorruptIdentity;
import mobisocial.crypto.IBEncryptionScheme;
import mobisocial.crypto.IBHashedIdentity;
import mobisocial.crypto.IBHashedIdentity.Authority;
import mobisocial.crypto.IBSignatureScheme;
import mobisocial.musubi.encoding.DiscardMessage.Corrupted;
import mobisocial.musubi.model.MEncodedMessage;
import mobisocial.musubi.model.MIdentity;
import mobisocial.musubi.model.MOutgoingSecret;
import mobisocial.musubi.protocol.Message;
import mobisocial.musubi.protocol.Recipient;
import mobisocial.musubi.protocol.Secret;
import mobisocial.musubi.protocol.Sender;
import mobisocial.musubi.util.Util;
import org.codehaus.jackson.map.ObjectMapper;
import de.undercouch.bson4jackson.BsonFactory;
//TODO: broadcast flag, app id, signature changes
public class MessageEncoder {
final IBEncryptionScheme mEncryptionScheme;
final IBSignatureScheme mSignatureScheme;
final long mDeviceName;
final TransportDataProvider mTdp;
ObjectMapper mMapper; // final but lazy
public MessageEncoder(TransportDataProvider tdp) {
mEncryptionScheme = tdp.getEncryptionScheme();
mSignatureScheme = tdp.getSignatureScheme();
mDeviceName = tdp.getDeviceName();
mTdp = tdp;
}
private ObjectMapper getObjectMapper() {
if (mMapper == null) {
mMapper = new ObjectMapper(new BsonFactory());
}
return mMapper;
}
byte[] encodeMessage(Message m) throws Corrupted {
try {
return getObjectMapper().writeValueAsBytes(m);
} catch (IOException e) {
throw new DiscardMessage.Corrupted("Failed to encode BSON of outer message", e);
}
}
byte[] encodeSecret(Secret s) throws Corrupted {
try {
return getObjectMapper().writeValueAsBytes(s);
} catch (IOException e) {
throw new DiscardMessage.Corrupted("Failed to encode BSON of inner recipient secret block", e);
}
}
byte[] encryptBody(byte[] messageKey, byte[] data, byte[] iv) {
Cipher cipher;
AlgorithmParameterSpec iv_spec;
SecretKeySpec sks;
try {
//since the length of the message is not included in the format, we have
//to use a normal padding scheme that preserves length
cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
} catch (Exception e) {
throw new RuntimeException("AES not supported on this platform", e);
}
try {
iv_spec = new IvParameterSpec(iv);
sks = new SecretKeySpec(messageKey, "AES");
cipher.init(Cipher.ENCRYPT_MODE, sks, iv_spec);
} catch (Exception e) {
throw new RuntimeException("bad iv or key on encode", e);
}
try {
return cipher.doFinal(data);
} catch (Exception e) {
throw new RuntimeException("body encryption failed", e);
}
}
private byte[] computeFullSignature(byte[] hash, byte[] app,
boolean blind, Recipient[] rs) {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("your platform does not support sha256", e);
}
md.update(hash);
md.update(app);
md.update(blind ? (byte)1 : (byte)0);
if(!blind) {
for(Recipient r : rs) {
md.update(r.i);
}
}
return md.digest();
}
byte[] encryptRecipientSecret(MOutgoingSecret secret, byte[] data, byte[] iv) {
Cipher cipher;
AlgorithmParameterSpec iv_spec;
SecretKeySpec sks;
try {
//TODO: do random byte padding
cipher = Cipher.getInstance("AES/CBC/ZeroBytePadding");
} catch (Exception e) {
throw new RuntimeException("AES not supported on this platform", e);
}
try {
iv_spec = new IvParameterSpec(iv);
sks = new SecretKeySpec(secret.key_, "AES");
cipher.init(Cipher.ENCRYPT_MODE, sks, iv_spec);
} catch (Exception e) {
throw new RuntimeException("bad iv or key on encode recip", e);
}
try {
return cipher.doFinal(data);
} catch (Exception e) {
throw new RuntimeException("recip secret encryption failed", e);
}
}
byte[] randomSymetricCipherBlock() {
byte[] b = new byte[16];
Random rand = new Random();
rand.nextBytes(b);
return b;
}
MOutgoingSecret addOutgoingSecret(MIdentity from, MIdentity to, IBHashedIdentity me, IBHashedIdentity you) throws NeedsKey.Signature {
//TODO: make sure not to waste time computing the same secret twice if someone uses
//this in a multi-threaded way
MOutgoingSecret os = mTdp.lookupOutgoingSecret(from, to, me, you);
if(os != null)
return os;
os = new MOutgoingSecret();
os.myIdentityId_ = from.id_;
os.otherIdentityId_ = to.id_;
IBEncryptionScheme.ConversationKey ck = mEncryptionScheme.randomConversationKey(you);
os.key_ = ck.key_;
os.encryptedKey_ = ck.encryptedKey_;
os.encryptionWhen_ = you.temporalFrame_;
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("your platform does not support sha256", e);
}
md.update(os.encryptedKey_);
ByteBuffer bb = ByteBuffer.wrap(new byte[8]);
bb.putLong(mDeviceName);
byte[] hash = md.digest(bb.array());
os.signatureWhen_ = me.temporalFrame_;
os.signature_ = mSignatureScheme.sign(me, mTdp.getSignatureKey(from, me), hash);
mTdp.insertOutgoingSecret(me, you, os);
return os;
}
long assignSequenceNumber(MIdentity to) {
long next = to.nextSequenceNumber_;
mTdp.incrementSequenceNumber(to);
return next;
}
public MEncodedMessage processMessage(OutgoingMessage om) throws DiscardMessage, NeedsKey {
Message m = new Message();
m.v = 0 /* version # */;
m.i = randomSymetricCipherBlock();
byte[] message_key = randomSymetricCipherBlock();
m.a = om.app_;
m.l = om.blind_;
m.s = new Sender();
IBHashedIdentity me = new IBHashedIdentity(om.fromIdentity_.type_, om.fromIdentity_.principalHash_, mTdp.getSignatureTime(om.fromIdentity_));
m.s.i = me.identity_;
ByteBuffer bb = ByteBuffer.wrap(new byte[8]);
bb.putLong(mDeviceName);
m.s.d = bb.array();
TLongLongHashMap sequence_numbers = new TLongLongHashMap();
m.r = new Recipient[om.recipients_.length];
for(int i = 0; i < om.recipients_.length; ++i) {
m.r[i] = new Recipient();
IBHashedIdentity other = new IBHashedIdentity(om.recipients_[i].type_, om.recipients_[i].principalHash_, mTdp.getEncryptionTime(om.recipients_[i]));
//don't let anyone try to send with a local authority
if(other.authority_ == Authority.Local)
throw new DiscardMessage.InvalidAuthority();
m.r[i].i = other.identity_;
}
assert(Arrays.equals(om.hash_, Util.sha256(om.data_)));
byte[] full_hash = computeFullSignature(om.hash_, om.app_, om.blind_, m.r);
mTdp.beginTransaction();
for(int i = 0; i < om.recipients_.length; ++i) {
long q = assignSequenceNumber(om.recipients_[i]);
sequence_numbers.put(om.recipients_[i].id_, q);
}
mTdp.setTransactionSuccessful();
mTdp.endTransaction();
for(int i = 0; i < om.recipients_.length; ++i) {
IBHashedIdentity other;
try {
other = new IBHashedIdentity(m.r[i].i);
} catch (CorruptIdentity e) {
throw new RuntimeException("impossible situation on encode", e);
}
MOutgoingSecret os = addOutgoingSecret(om.fromIdentity_, om.recipients_[i], me, other);
m.r[i].k = os.encryptedKey_;
m.r[i].s = os.signature_;
Secret s = new Secret();
s.h = full_hash;
s.k = message_key;
s.q = sequence_numbers.get(om.recipients_[i].id_);
m.r[i].d = encryptRecipientSecret(os, encodeSecret(s), m.i);
}
m.d = encryptBody(message_key, om.data_, m.i);
MEncodedMessage encoded = new MEncodedMessage();
encoded.encoded_ = encodeMessage(m);
encoded.hash_ = Util.sha256(encoded.encoded_);
encoded.processed_ = false;
//this table is used to decide whetehr or not to bother decoding, and we want to dedupe stuff from ourself
encoded.fromIdentityId_ = om.fromIdentity_.id_;
encoded.fromDevice_ = mTdp.addDevice(om.fromIdentity_, mDeviceName).id_;
encoded.shortHash_ = Util.shortHash(encoded.hash_);
encoded.outbound_ = true;
mTdp.beginTransaction();
mTdp.insertEncodedMessage(om, encoded);
mTdp.storeSequenceNumbers(encoded, sequence_numbers);
mTdp.setTransactionSuccessful();
mTdp.endTransaction();
return encoded;
}
}