/* * Copyright (c) [2016] [ <ether.camp> ] * This file is part of the ethereumJ library. * * The ethereumJ library is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * The ethereumJ library 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>. */ package org.ethereum.net.shh; import org.ethereum.crypto.ECIESCoder; import org.ethereum.crypto.ECKey; import org.ethereum.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spongycastle.math.ec.ECPoint; import org.spongycastle.util.BigIntegers; import org.spongycastle.util.encoders.Hex; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.SignatureException; import java.util.*; import static org.ethereum.crypto.HashUtil.sha3; import static org.ethereum.net.swarm.Util.rlpDecodeInt; import static org.ethereum.util.ByteUtil.merge; import static org.ethereum.util.ByteUtil.xor; /** * Created by Anton Nashatyrev on 25.09.2015. */ public class WhisperMessage extends ShhMessage { private final static Logger logger = LoggerFactory.getLogger("net.shh"); public static final int SIGNATURE_FLAG = 1; public static final int SIGNATURE_LENGTH = 65; private Topic[] topics = new Topic[0]; private byte[] payload; private byte flags; private byte[] signature; private String to; private ECKey from; private int expire; private int ttl; int nonce = 0; private boolean encrypted = false; private long pow = 0; private byte[] messageBytes; public WhisperMessage() { setTtl(50); setWorkToProve(50); } public WhisperMessage(byte[] encoded) { super(encoded); encrypted = true; parse(); } public Topic[] getTopics() { return topics; } public byte[] getPayload() { return payload; } public int getExpire() { return expire; } public int getTtl() { return ttl; } public long getPow() { return pow; } public String getFrom() { return from == null ? null : WhisperImpl.toIdentity(from); } public String getTo() { return to; } /*********** Decode routines ************/ private void parse() { if (!parsed) { RLPList paramsList = (RLPList) RLP.decode2(encoded).get(0); this.expire = ByteUtil.byteArrayToInt(paramsList.get(0).getRLPData()); this.ttl = ByteUtil.byteArrayToInt(paramsList.get(1).getRLPData()); List<Topic> topics = new ArrayList<>(); RLPList topicsList = (RLPList) RLP.decode2(paramsList.get(2).getRLPData()).get(0); for (RLPElement e : topicsList) { topics.add(new Topic(e.getRLPData())); } this.topics = new Topic[topics.size()]; topics.toArray(this.topics); messageBytes = paramsList.get(3).getRLPData(); this.nonce = rlpDecodeInt(paramsList.get(4)); payload = messageBytes; pow = workProved(); this.parsed = true; } } private boolean processSignature() { flags = payload[0]; if ((flags & WhisperMessage.SIGNATURE_FLAG) != 0) { if (payload.length < WhisperMessage.SIGNATURE_LENGTH) { throw new RuntimeException("Unable to open the envelope. First bit set but len(data) < len(signature)"); } signature = new byte[WhisperMessage.SIGNATURE_LENGTH]; System.arraycopy(payload, payload.length - WhisperMessage.SIGNATURE_LENGTH, signature, 0, WhisperMessage.SIGNATURE_LENGTH); byte[] msg = new byte[payload.length - WhisperMessage.SIGNATURE_LENGTH - 1]; System.arraycopy(payload, 1, msg, 0, msg.length); payload = msg; from = recover(); return true; } else { byte[] msg = new byte[payload.length - 1]; System.arraycopy(payload, 1, msg, 0, msg.length); payload = msg; return true; } } public boolean decrypt(Collection<ECKey> identities, Collection<Topic> knownTopics) { boolean ok = false; for (ECKey key : identities) { ok = decrypt(key); if (ok) break; } if (!ok) { // decrypting as broadcast ok = openBroadcastMessage(knownTopics); } if (ok) { return processSignature(); } // the message might be either not-encrypted or encrypted but we have no receivers // now way to know so just assuming that the message is broadcast and not encrypted // setEncrypted(false); return false; } private boolean decrypt(ECKey privateKey) { try { payload = ECIESCoder.decryptSimple(privateKey.getPrivKey(), payload); to = WhisperImpl.toIdentity(privateKey); encrypted = false; return true; } catch (Exception e) { logger.trace("Message can't be opened with key: " + privateKey.getPubKeyPoint()); } catch (Throwable e) { } return false; } private boolean openBroadcastMessage(Collection<Topic> knownTopics) { for (Topic kTopic : knownTopics) { for (int i = 0; i < topics.length; i++) { if (kTopic.equals(topics[i])) { byte[] encryptedKey = Arrays.copyOfRange(payload, i * 2 * 32, i * 2 * 32 + 32); byte[] salt = Arrays.copyOfRange(payload, (i * 2 + 1) * 32, (i * 2 + 2) * 32); byte[] cipherText = Arrays.copyOfRange(payload, (topics.length * 2) * 32, payload.length); byte[] gamma = sha3(xor(kTopic.getFullTopic(), salt)); ECKey key = ECKey.fromPrivate(xor(gamma, encryptedKey)); try { payload = ECIESCoder.decryptSimple(key.getPrivKey(), cipherText); } catch (Exception e) { logger.warn("Error decrypting message with known topic: " + kTopic); // the abridged topic clash can potentially happen, so just continue with other topics continue; } encrypted = false; return true; } } } return false; } private ECKey.ECDSASignature decodeSignature() { if (signature == null) { return null; } byte[] r = new byte[32]; byte[] s = new byte[32]; byte v = signature[64]; if (v == 1) v = 28; if (v == 0) v = 27; System.arraycopy(signature, 0, r, 0, 32); System.arraycopy(signature, 32, s, 0, 32); return ECKey.ECDSASignature.fromComponents(r, s, v); } private ECKey recover() { ECKey.ECDSASignature signature = decodeSignature(); if (signature == null) return null; byte[] msgHash = hash(); ECKey outKey = null; try { outKey = ECKey.signatureToKey(msgHash, signature); } catch (SignatureException e) { logger.warn("Exception recovering signature: ", e); throw new RuntimeException(e); } return outKey; } public byte[] hash() { return sha3(payload); } private int workProved() { byte[] d = new byte[64]; System.arraycopy(sha3(encode(false)), 0, d, 0, 32); ByteBuffer.wrap(d).putInt(32, nonce); return getFirstBitSet(sha3(d)); } /*********** Encode routines ************/ public WhisperMessage setTopics(Topic ... topics) { this.topics = topics != null ? topics : new Topic[0]; return this; } public WhisperMessage setPayload(String payload) { this.payload = payload.getBytes(StandardCharsets.UTF_8); return this; } public WhisperMessage setPayload(byte[] payload) { this.payload = payload; return this; } /** * If set the message will be encrypted with the receiver public key * If not the message will be encrypted as broadcast with Topics * @param to public key */ public WhisperMessage setTo(String to) { this.to = to; return this; } /** * If set the message will be signed by the sender key * @param from sender key */ public WhisperMessage setFrom(ECKey from) { this.from = from; return this; } public WhisperMessage setFrom(String from) { this.from = WhisperImpl.fromIdentityToPub(from); return this; } public WhisperMessage setTtl(int ttl) { this.ttl = ttl; expire = (int) (Utils.toUnixTime(System.currentTimeMillis()) + ttl); return this; } public WhisperMessage setWorkToProve(long ms) { this.pow = ms; return this; } @Override public byte[] getEncoded() { if (encoded == null) { if (from != null) { sign(); } payload = getBytes(); encrypt(); byte[] withoutNonce = encode(false); nonce = seal(withoutNonce, pow); encoded = encode(true); } return encoded; } public byte[] encode(boolean withNonce) { byte[] expire = RLP.encode(this.expire); byte[] ttl = RLP.encode(this.ttl); List<byte[]> topics = new Vector<>(); for (Topic t : this.topics) { topics.add(RLP.encodeElement(t.getBytes())); } byte[][] topicsArray = topics.toArray(new byte[topics.size()][]); byte[] encodedTopics = RLP.encodeList(topicsArray); byte[] data = RLP.encodeElement(payload); byte[] nonce = RLP.encodeInt(this.nonce); return withNonce ? RLP.encodeList(expire, ttl, encodedTopics, data, nonce) : RLP.encodeList(expire, ttl, encodedTopics, data); } private int seal(byte[] encoded, long pow) { int ret = 0; byte[] d = new byte[64]; ByteBuffer byteBuffer = ByteBuffer.wrap(d); System.arraycopy(sha3(encoded), 0, d, 0, 32); long then = System.currentTimeMillis() + pow; int nonce = 0; for (int bestBit = 0; System.currentTimeMillis() < then;) { for (int i = 0; i < 1024; ++i, ++nonce) { byteBuffer.putInt(32, nonce); int fbs = getFirstBitSet(sha3(d)); if (fbs > bestBit) { ret = nonce; bestBit = fbs; } } } return ret; } private int getFirstBitSet(byte[] bytes) { BitSet b = BitSet.valueOf(bytes); for (int i = 0; i < b.length(); i++) { if (b.get(i)) { return i; } } return 0; } public byte[] getBytes() { if (signature != null) { return merge(new byte[]{flags}, payload, signature); } else { return merge(new byte[]{flags}, payload); } } private void encrypt() { try { if (to != null) { ECKey key = WhisperImpl.fromIdentityToPub(to); ECPoint pubKeyPoint = key.getPubKeyPoint(); payload = ECIESCoder.encryptSimple(pubKeyPoint, payload); } else if (topics.length > 0){ // encrypting as broadcast message byte[] topicKeys = new byte[topics.length * 64]; ECKey key = new ECKey(); Random rnd = new Random(); byte[] salt = new byte[32]; for (int i = 0; i < topics.length; i++) { rnd.nextBytes(salt); byte[] gamma = sha3(xor(topics[i].getFullTopic(), salt)); byte[] encodedKey = xor(gamma, key.getPrivKeyBytes()); System.arraycopy(encodedKey, 0, topicKeys, i * 64, 32); System.arraycopy(salt, 0, topicKeys, i * 64 + 32, 32); } ECPoint pubKeyPoint = key.getPubKeyPoint(); payload = ByteUtil.merge(topicKeys, ECIESCoder.encryptSimple(pubKeyPoint, payload)); } else { logger.debug("No 'to' or topics for outbound message. Will not be encrypted."); } } catch (Exception e) { logger.error("Unexpected error while encrypting: ", e); } encrypted = true; } private void sign() { flags |= SIGNATURE_FLAG; byte[] forSig = hash(); ECKey.ECDSASignature signature = from.sign(forSig); byte v; if (signature.v == 27) v = 0; else if (signature.v == 28) v = 1; else throw new RuntimeException("Invalid signature: " + signature); this.signature = merge(BigIntegers.asUnsignedByteArray(32, signature.r), BigIntegers.asUnsignedByteArray(32, signature.s), new byte[]{v}); } @Override public Class<?> getAnswerMessage() { return null; } @Override public String toString() { return "WhisperMessage[" + "topics=" + Arrays.toString(topics) + ", payload=" + (encrypted ? "<encrypted " + payload.length + " bytes>" : new String(payload)) + ", to=" + (to == null ? "null" : to.substring(0, 16) + "...") + ", from=" + (from == null ? "null" : Hex.toHexString(from.getPubKey()).substring(0,16) + "...") + ", expire=" + expire + ", ttl=" + ttl + ", nonce=" + nonce + ']'; } }