/*
* 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 java.io.IOException;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
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.BadSignature;
import mobisocial.musubi.encoding.DiscardMessage.Corrupted;
import mobisocial.musubi.encoding.DiscardMessage.Duplicate;
import mobisocial.musubi.model.MDevice;
import mobisocial.musubi.model.MEncodedMessage;
import mobisocial.musubi.model.MIdentity;
import mobisocial.musubi.model.MIncomingSecret;
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 android.util.Base64;
import de.undercouch.bson4jackson.BsonFactory;
import de.undercouch.bson4jackson.BsonParser.Feature;
public class MessageDecoder {
final IBEncryptionScheme mEncryptionScheme;
final IBSignatureScheme mSignatureScheme;
final TransportDataProvider mTdp;
//since we aren't trying to do streaming large object processing, we enforce
//the document length. this lets us easily ignore the data at the end
//with increasing the secret block size with a funky padding scheme.
ObjectMapper mMapper; // final but lazy
public MessageDecoder(TransportDataProvider tdp) {
mEncryptionScheme = tdp.getEncryptionScheme();
mSignatureScheme = tdp.getSignatureScheme();
mTdp = tdp;
}
private ObjectMapper getObjectMapper() {
if (mMapper == null) {
mMapper = new ObjectMapper(new BsonFactory().enable(Feature.HONOR_DOCUMENT_LENGTH));
}
return mMapper;
}
Message decodeMessage(byte[] raw) throws Corrupted {
try {
return getObjectMapper().readValue(raw, Message.class);
} catch (IOException e) {
throw new DiscardMessage.Corrupted("Failed to parse BSON of outer message", e);
}
}
Secret decodeSecret(byte[] raw) throws Corrupted {
try {
return getObjectMapper().readValue(raw, Secret.class);
} catch (IOException e) {
throw new DiscardMessage.Corrupted("Failed to parse BSON of inner recipient secret block", e);
}
}
byte[] decryptBody(byte[] data, byte[] messageKey, byte[] iv) throws Corrupted {
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.DECRYPT_MODE, sks, iv_spec);
} catch (Exception e) {
throw new DiscardMessage.Corrupted("bad iv or key", e);
}
try {
return cipher.doFinal(data);
} catch (Exception e) {
throw new DiscardMessage.Corrupted("body decryption failed", e);
}
}
void checkBodySignature(byte[] expected, byte[] hash, byte[] app, boolean blind, Recipient[] rs) throws BadSignature {
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);
}
}
byte[] full_hash = md.digest();
if(!Arrays.equals(full_hash, expected)) {
throw new BadSignature("signature mismatch for message data was " + Base64.encodeToString(expected, Base64.DEFAULT) + " should be " + Base64.encodeToString(full_hash, Base64.DEFAULT));
}
}
void checkDuplicate(MDevice from, byte[] raw_hash) throws Duplicate {
if(mTdp.haveHash(raw_hash)) {
throw new DiscardMessage.Duplicate(from, raw_hash);
}
}
boolean isBlacklisted(MIdentity from) {
return mTdp.isBlacklisted(from);
}
void updateMissingMessages(MDevice from, long sequenceNumber) {
mTdp.receivedSequenceNumber(from, sequenceNumber);
}
byte[] decryptRecipientSecret(MIncomingSecret secret, byte[] data, byte[] iv) throws Corrupted {
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.DECRYPT_MODE, sks, iv_spec);
} catch (Exception e) {
throw new DiscardMessage.Corrupted("bad iv or key for recip", e);
}
try {
return cipher.doFinal(data);
} catch (Exception e) {
throw new DiscardMessage.Corrupted("recip secret decryption failed", e);
}
}
boolean isMe(Recipient r) throws CorruptIdentity {
return mTdp.isMe(new IBHashedIdentity(r.i));
}
MIdentity addIdentity(byte[] id) throws CorruptIdentity {
IBHashedIdentity hid = new IBHashedIdentity(id);
return mTdp.addClaimedIdentity(hid);
}
MIdentity addUnclaimedIdentity(IBHashedIdentity hid) {
return mTdp.addUnclaimedIdentity(hid);
}
MDevice addDevice(MIdentity ident, byte[] device) {
return mTdp.addDevice(ident, ByteBuffer.wrap(device).getLong());
}
MIncomingSecret addIncomingSecret(MIdentity from, MDevice device, MIdentity to, Sender s, Recipient me) throws BadSignature, NeedsKey.Encryption, CorruptIdentity {
IBHashedIdentity me_timed = new IBHashedIdentity(me.i);
IBHashedIdentity sid = new IBHashedIdentity(s.i);
//TODO: make sure not to waste time computing the same secret twice if someone uses
//this in a multi-threaded way
MIncomingSecret is = mTdp.lookupIncomingSecret(from, device, to, me.s, sid, me_timed);
if(is != null)
return is;
is = new MIncomingSecret();
is.myIdentityId_ = to.id_;
is.otherIdentityId_ = from.id_;
is.deviceId_ = device.id_;
is.key_ = mEncryptionScheme.decryptConversationKey(mTdp.getEncryptionKey(to, me_timed), me.k);
is.encryptedKey_ = me.k;
is.encryptionWhen_ = me_timed.temporalFrame_;
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("your platform does not support sha256", e);
}
md.update(is.encryptedKey_);
ByteBuffer deviceId = ByteBuffer.wrap(new byte[8]);
deviceId.putLong(device.deviceName_);
byte[] hash = md.digest(deviceId.array());
is.signatureWhen_ = sid.temporalFrame_;
is.signature_ = me.s;
if(!mSignatureScheme.verify(sid, is.signature_, hash)) {
throw new DiscardMessage.BadSignature("message failed to have a valid signature for my recipient key");
}
mTdp.insertIncomingSecret(sid, me_timed, is);
return is;
}
public IncomingMessage processMessage(MEncodedMessage encoded) throws DiscardMessage, NeedsKey {
try {
return processMessageInternal(encoded);
} catch (CorruptIdentity e) {
throw new DiscardMessage("corrupt identity data in message", e);
}
}
private IncomingMessage processMessageInternal(MEncodedMessage encoded) throws DiscardMessage, NeedsKey, CorruptIdentity {
IncomingMessage im = new IncomingMessage();
Message m = decodeMessage(encoded.encoded_);
ArrayList<Recipient> mine = new ArrayList<Recipient>(8);
for(Recipient r : m.r) {
//TODO: dedupe?
if(isMe(r)) {
mine.add(r);
}
}
if(mine.size() == 0)
throw new DiscardMessage.NotToMe("Couldn't find a recipient that matches me");
//this will add all of the relevant identities and devices to the tables
im.fromIdentity_ = addIdentity(m.s.i);
if(isBlacklisted(im.fromIdentity_)) {
throw new DiscardMessage.Blacklist("received message from blacklisted identity " + im.fromIdentity_.id_);
}
im.fromDevice_ = addDevice(im.fromIdentity_, m.s.d);
byte[] raw_hash = Util.sha256(encoded.encoded_);
checkDuplicate(im.fromDevice_, raw_hash);
im.app_ = m.a;
im.blind_ = m.l;
im.personas_ = new MIdentity[mine.size()];
for(int i = 0; i < im.personas_.length; ++i) {
im.personas_[i] = addIdentity(mine.get(i).i);
}
if(im.blind_) {
//TODO: the server was supposed to strip these out.
im.recipients_ = im.personas_;
} else {
im.recipients_ = new MIdentity[m.r.length];
for(int i = 0; i < m.r.length; ++i) {
IBHashedIdentity hid = new IBHashedIdentity(m.r[i].i);
//don't accept messages from clients that erroneously include
//a local user in the group
if(hid.authority_ == Authority.Local)
throw new DiscardMessage.InvalidAuthority();
im.recipients_[i] = addUnclaimedIdentity(hid);
}
}
for(int i = 0; i < im.personas_.length; ++i) {
Recipient me = mine.get(i);
MIdentity persona = im.personas_[i];
//checks the secret if it is actually added
MIncomingSecret inSecret = addIncomingSecret(im.fromIdentity_, im.fromDevice_, persona, m.s, me);
byte[] recipient_secret = decryptRecipientSecret(inSecret, me.d, m.i);
Secret secret = decodeSecret(recipient_secret);
im.sequenceNumber_ = secret.q;
//This makes it so that we only compute
//the data and the hash once per message
if(im.data_ == null) {
im.data_ = decryptBody(m.d, secret.k, m.i);
}
if(im.hash_ == null) {
im.hash_ = Util.sha256(im.data_);
}
checkBodySignature(secret.h, im.hash_, im.app_, im.blind_, m.r);
updateMissingMessages(im.fromDevice_, secret.q);
}
encoded.fromDevice_ = im.fromDevice_.id_;
encoded.fromIdentityId_ = im.fromIdentity_.id_;
encoded.hash_ = raw_hash;
encoded.shortHash_ = Util.shortHash(raw_hash);
//TODO:XXX this appears to be a race condition if the processing crashes in the obj phase
//in a non-repeatable way. ideally, we would only update this encoded row once in the caller.
mTdp.updateEncodedMetadata(encoded);
return im;
}
}