XmppAxolotlMessage.java

  1package eu.siacs.conversations.crypto.axolotl;
  2
  3import android.support.annotation.Nullable;
  4import android.util.Base64;
  5
  6import java.security.InvalidAlgorithmParameterException;
  7import java.security.InvalidKeyException;
  8import java.security.NoSuchAlgorithmException;
  9import java.security.NoSuchProviderException;
 10import java.security.SecureRandom;
 11import java.util.HashSet;
 12import java.util.Set;
 13
 14import javax.crypto.BadPaddingException;
 15import javax.crypto.Cipher;
 16import javax.crypto.IllegalBlockSizeException;
 17import javax.crypto.KeyGenerator;
 18import javax.crypto.NoSuchPaddingException;
 19import javax.crypto.SecretKey;
 20import javax.crypto.spec.IvParameterSpec;
 21import javax.crypto.spec.SecretKeySpec;
 22
 23import eu.siacs.conversations.xml.Element;
 24import eu.siacs.conversations.xmpp.jid.Jid;
 25
 26public class XmppAxolotlMessage {
 27	private byte[] innerKey;
 28	private byte[] ciphertext;
 29	private byte[] iv;
 30	private final Set<XmppAxolotlMessageHeader> headers;
 31	private final Jid from;
 32	private final int sourceDeviceId;
 33
 34	public static class XmppAxolotlMessageHeader {
 35		private final int recipientDeviceId;
 36		private final byte[] content;
 37
 38		public XmppAxolotlMessageHeader(int deviceId, byte[] content) {
 39			this.recipientDeviceId = deviceId;
 40			this.content = content;
 41		}
 42
 43		public XmppAxolotlMessageHeader(Element header) {
 44			if("header".equals(header.getName())) {
 45				this.recipientDeviceId = Integer.parseInt(header.getAttribute("rid"));
 46				this.content = Base64.decode(header.getContent(),Base64.DEFAULT);
 47			} else {
 48				throw new IllegalArgumentException("Argument not a <header> Element!");
 49			}
 50		}
 51
 52		public int getRecipientDeviceId() {
 53			return recipientDeviceId;
 54		}
 55
 56		public byte[] getContents() {
 57			return content;
 58		}
 59
 60		public Element toXml() {
 61			Element headerElement = new Element("header");
 62			// TODO: generate XML
 63			headerElement.setAttribute("rid", getRecipientDeviceId());
 64			headerElement.setContent(Base64.encodeToString(getContents(), Base64.DEFAULT));
 65			return headerElement;
 66		}
 67	}
 68
 69	public static class XmppAxolotlPlaintextMessage {
 70		private final AxolotlService.XmppAxolotlSession session;
 71		private final String plaintext;
 72		private final String fingerprint;
 73
 74		public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext, String fingerprint) {
 75			this.session = session;
 76			this.plaintext = plaintext;
 77			this.fingerprint = fingerprint;
 78		}
 79
 80		public String getPlaintext() {
 81			return plaintext;
 82		}
 83
 84		public AxolotlService.XmppAxolotlSession getSession() {
 85			return session;
 86		}
 87
 88		public String getFingerprint() {
 89			return fingerprint;
 90		}
 91	}
 92
 93	public XmppAxolotlMessage(Jid from, Element axolotlMessage) {
 94		this.from = from;
 95		this.sourceDeviceId = Integer.parseInt(axolotlMessage.getAttribute("id"));
 96		this.headers = new HashSet<>();
 97		for(Element child:axolotlMessage.getChildren()) {
 98			switch(child.getName()) {
 99				case "header":
100					headers.add(new XmppAxolotlMessageHeader(child));
101					break;
102				case "message":
103					iv = Base64.decode(child.getAttribute("iv"),Base64.DEFAULT);
104					ciphertext = Base64.decode(child.getContent(),Base64.DEFAULT);
105					break;
106				default:
107					break;
108			}
109		}
110	}
111
112	public XmppAxolotlMessage(Jid from, int sourceDeviceId, String plaintext) throws CryptoFailedException{
113		this.from = from;
114		this.sourceDeviceId = sourceDeviceId;
115		this.headers = new HashSet<>();
116		this.encrypt(plaintext);
117	}
118
119	private void encrypt(String plaintext) throws CryptoFailedException {
120		try {
121			KeyGenerator generator = KeyGenerator.getInstance("AES");
122			generator.init(128);
123			SecretKey secretKey = generator.generateKey();
124			SecureRandom random = new SecureRandom();
125			this.iv = new byte[16];
126			random.nextBytes(iv);
127			IvParameterSpec ivSpec = new IvParameterSpec(iv);
128			Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
129			cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
130			this.innerKey = secretKey.getEncoded();
131			this.ciphertext = cipher.doFinal(plaintext.getBytes());
132		} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
133				| IllegalBlockSizeException | BadPaddingException | NoSuchProviderException
134				| InvalidAlgorithmParameterException e) {
135			throw new CryptoFailedException(e);
136		}
137	}
138
139	public Jid getFrom() {
140		return this.from;
141	}
142
143	public int getSenderDeviceId() {
144		return sourceDeviceId;
145	}
146
147	public byte[] getCiphertext() {
148		return ciphertext;
149	}
150
151	public Set<XmppAxolotlMessageHeader> getHeaders() {
152		return headers;
153	}
154
155	public void addHeader(@Nullable XmppAxolotlMessageHeader header) {
156		if (header != null) {
157			headers.add(header);
158		}
159	}
160
161	public byte[] getInnerKey(){
162		return innerKey;
163	}
164
165	public byte[] getIV() {
166		return this.iv;
167	}
168
169	public Element toXml() {
170		// TODO: generate outer XML, add in header XML
171		Element message= new Element("axolotl_message", AxolotlService.PEP_PREFIX);
172		message.setAttribute("id", sourceDeviceId);
173		for(XmppAxolotlMessageHeader header: headers) {
174			message.addChild(header.toXml());
175		}
176		Element payload = message.addChild("message");
177		payload.setAttribute("iv",Base64.encodeToString(iv, Base64.DEFAULT));
178		payload.setContent(Base64.encodeToString(ciphertext,Base64.DEFAULT));
179		return message;
180	}
181
182
183	public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key, String fingerprint) throws CryptoFailedException {
184		XmppAxolotlPlaintextMessage plaintextMessage = null;
185		try {
186
187			Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
188			SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
189			IvParameterSpec ivSpec = new IvParameterSpec(iv);
190
191			cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
192
193			String plaintext = new String(cipher.doFinal(ciphertext));
194			plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext, fingerprint);
195
196		} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
197				| InvalidAlgorithmParameterException | IllegalBlockSizeException
198				| BadPaddingException | NoSuchProviderException e) {
199			throw new CryptoFailedException(e);
200		}
201		return plaintextMessage;
202	}
203}