XmppAxolotlMessage.java

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