diff --git a/briar-core/src/org/briarproject/crypto/Blake2sDigest.java b/briar-core/src/org/briarproject/crypto/Blake2sDigest.java
new file mode 100644
index 0000000000000000000000000000000000000000..33f3545d5921434a3fced86ae72c9f9c49b8a7a0
--- /dev/null
+++ b/briar-core/src/org/briarproject/crypto/Blake2sDigest.java
@@ -0,0 +1,547 @@
+package org.briarproject.crypto;
+
+/*
+  The BLAKE2 cryptographic hash function was designed by Jean-
+  Philippe Aumasson, Samuel Neves, Zooko Wilcox-O'Hearn, and Christian
+  Winnerlein.
+
+  Reference Implementation and Description can be found at: https://blake2.net/
+  RFC: https://tools.ietf.org/html/rfc7693
+
+  This implementation does not support the Tree Hashing Mode.
+
+  For unkeyed hashing, developers adapting BLAKE2 to ASN.1 - based
+  message formats SHOULD use the OID tree at x = 1.3.6.1.4.1.1722.12.2.
+
+         Algorithm     | Target | Collision | Hash | Hash ASN.1 |
+            Identifier |  Arch  |  Security |  nn  | OID Suffix |
+        ---------------+--------+-----------+------+------------+
+         id-blake2s128 | 32-bit |   2**64   |  16  |   x.2.4    |
+         id-blake2s160 | 32-bit |   2**80   |  20  |   x.2.5    |
+         id-blake2s224 | 32-bit |   2**112  |  28  |   x.2.7    |
+         id-blake2s256 | 32-bit |   2**128  |  32  |   x.2.8    |
+        ---------------+--------+-----------+------+------------+
+
+  Based on the BouncyCastle implementation of BLAKE2b. License:
+
+  Copyright (c) 2000 - 2015 The Legion of the Bouncy Castle Inc.
+  (http://www.bouncycastle.org)
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+ */
+
+import org.spongycastle.crypto.ExtendedDigest;
+import org.spongycastle.util.Arrays;
+
+/**
+ * Implementation of the cryptographic hash function BLAKE2s.
+ * <p/>
+ * BLAKE2s offers a built-in keying mechanism to be used directly
+ * for authentication ("Prefix-MAC") rather than a HMAC construction.
+ * <p/>
+ * BLAKE2s offers a built-in support for a salt for randomized hashing
+ * and a personal string for defining a unique hash function for each application.
+ * <p/>
+ * BLAKE2s is optimized for 32-bit platforms and produces digests of any size
+ * between 1 and 32 bytes.
+ */
+public class Blake2sDigest implements ExtendedDigest {
+	/** BLAKE2s Initialization Vector **/
+	private static final int blake2s_IV[] =
+			// Produced from the square root of primes 2, 3, 5, 7, 11, 13, 17, 19.
+			// The same as SHA-256 IV.
+			{
+					0x6a09e667, 0xbb67ae85, 0x3c6ef372,
+					0xa54ff53a, 0x510e527f, 0x9b05688c,
+					0x1f83d9ab, 0x5be0cd19
+			};
+
+	/** Message word permutations **/
+	private static final byte[][] blake2s_sigma =
+			{
+					{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 },
+					{ 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 },
+					{ 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4 },
+					{ 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8 },
+					{ 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13 },
+					{ 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9 },
+					{ 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11 },
+					{ 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10 },
+					{ 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5 },
+					{ 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0 }
+			};
+
+	private static final int ROUNDS = 10; // to use for Catenas H'
+	private static final int BLOCK_LENGTH_BYTES = 64;// bytes
+
+	// General parameters:
+	private int digestLength = 32; // 1- 32 bytes
+	private int keyLength = 0; // 0 - 32 bytes for keyed hashing for MAC
+	private byte[] salt = null;
+	private byte[] personalization = null;
+	private byte[] key = null;
+
+	// Tree hashing parameters:
+	// Because this class does not implement the Tree Hashing Mode,
+	// these parameters can be treated as constants (see init() function)
+	/*
+	 * private int fanout = 1; // 0-255
+	 * private int depth = 1; // 1 - 255
+	 * private int leafLength= 0;
+	 * private long nodeOffset = 0L;
+	 * private int nodeDepth = 0;
+	 * private int innerHashLength = 0;
+	 */
+
+	/**
+	 * Whenever this buffer overflows, it will be processed in the compress()
+	 * function. For performance issues, long messages will not use this buffer.
+	 */
+	private byte[] buffer = null;
+	/** Position of last inserted byte **/
+	private int bufferPos = 0;// a value from 0 up to BLOCK_LENGTH_BYTES
+
+	/** Internal state, in the BLAKE2 paper it is called v **/
+	private int[] internalState = new int[16];
+	/** State vector, in the BLAKE2 paper it is called h **/
+	private int[] chainValue = null;
+
+	// counter (counts bytes): Length up to 2^64 are supported
+	/** holds least significant bits of counter **/
+	private int t0 = 0;
+	/** holds most significant bits of counter **/
+	private int t1 = 0;
+	/** finalization flag, for last block: ~0 **/
+	private int f0 = 0;
+
+	// For Tree Hashing Mode, not used here:
+	// private long f1 = 0L; // finalization flag, for last node: ~0L
+
+	/**
+	 * BLAKE2s-256 for hashing.
+	 */
+	public Blake2sDigest() {
+		this(256);
+	}
+
+	public Blake2sDigest(Blake2sDigest digest) {
+		this.bufferPos = digest.bufferPos;
+		this.buffer = Arrays.clone(digest.buffer);
+		this.keyLength = digest.keyLength;
+		this.key = Arrays.clone(digest.key);
+		this.digestLength = digest.digestLength;
+		this.chainValue = Arrays.clone(digest.chainValue);
+		this.personalization = Arrays.clone(digest.personalization);
+	}
+
+	/**
+	 * BLAKE2s for hashing.
+	 *
+	 * @param digestBits the desired digest length in bits. Must be one of
+	 *                     [128, 160, 224, 256].
+	 */
+	public Blake2sDigest(int digestBits) {
+		if (digestBits != 128 && digestBits != 160 &&
+				digestBits != 224 && digestBits != 256) {
+			throw new IllegalArgumentException(
+					"BLAKE2s digest restricted to one of [128, 160, 224, 256]");
+		}
+		buffer = new byte[BLOCK_LENGTH_BYTES];
+		keyLength = 0;
+		digestLength = digestBits / 8;
+		init();
+	}
+
+	/**
+	 * BLAKE2s for authentication ("Prefix-MAC mode").
+	 * <p/>
+	 * After calling the doFinal() method, the key will remain to be used for
+	 * further computations of this instance. The key can be overwritten using
+	 * the clearKey() method.
+	 *
+	 * @param key a key up to 32 bytes or null
+	 */
+	public Blake2sDigest(byte[] key) {
+		buffer = new byte[BLOCK_LENGTH_BYTES];
+		if (key != null) {
+			if (key.length > 32) {
+				throw new IllegalArgumentException(
+						"Keys > 32 are not supported");
+			}
+			this.key = new byte[key.length];
+			System.arraycopy(key, 0, this.key, 0, key.length);
+
+			keyLength = key.length;
+			System.arraycopy(key, 0, buffer, 0, key.length);
+			bufferPos = BLOCK_LENGTH_BYTES; // zero padding
+		}
+		digestLength = 32;
+		init();
+	}
+
+	/**
+	 * BLAKE2s with key, required digest length, salt and personalization.
+	 * <p/>
+	 * After calling the doFinal() method, the key, the salt and the personal
+	 * string will remain and might be used for further computations with this
+	 * instance. The key can be overwritten using the clearKey() method, the
+	 * salt (pepper) can be overwritten using the clearSalt() method.
+	 *
+	 * @param key a key up to 32 bytes or null
+	 * @param digestBytes from 1 up to 32 bytes
+	 * @param salt 8 bytes or null
+	 * @param personalization 8 bytes or null
+	 */
+	public Blake2sDigest(byte[] key, int digestBytes, byte[] salt,
+			byte[] personalization) {
+		buffer = new byte[BLOCK_LENGTH_BYTES];
+		if (digestBytes < 1 || digestBytes > 32) {
+			throw new IllegalArgumentException(
+					"Invalid digest length (required: 1 - 32)");
+		}
+		digestLength = digestBytes;
+		if (salt != null) {
+			if (salt.length != 8) {
+				throw new IllegalArgumentException(
+						"Salt length must be exactly 8 bytes");
+			}
+			this.salt = new byte[8];
+			System.arraycopy(salt, 0, this.salt, 0, salt.length);
+		}
+		if (personalization != null) {
+			if (personalization.length != 8) {
+				throw new IllegalArgumentException(
+						"Personalization length must be exactly 8 bytes");
+			}
+			this.personalization = new byte[8];
+			System.arraycopy(personalization, 0, this.personalization, 0,
+					personalization.length);
+		}
+		if (key != null) {
+			if (key.length > 32) {
+				throw new IllegalArgumentException(
+						"Keys > 32 bytes are not supported");
+			}
+			this.key = new byte[key.length];
+			System.arraycopy(key, 0, this.key, 0, key.length);
+
+			keyLength = key.length;
+			System.arraycopy(key, 0, buffer, 0, key.length);
+			bufferPos = BLOCK_LENGTH_BYTES; // zero padding
+		}
+		init();
+	}
+
+	// initialize chainValue
+	private void init() {
+		if (chainValue == null) {
+			chainValue = new int[8];
+
+			chainValue[0] = blake2s_IV[0]
+					^ (digestLength | (keyLength << 8) | 0x1010000);
+			// 0x1010000 = ((fanout << 16) | (depth << 24));
+			// with fanout = 1; depth = 0;
+			chainValue[1] = blake2s_IV[1];// ^ leafLength; with leafLength = 0;
+			chainValue[2] = blake2s_IV[2];// ^ nodeOffset; with nodeOffset = 0;
+			chainValue[3] = blake2s_IV[3];// ^ ( (nodeOffset << 32) |
+			// (nodeDepth << 16) | (innerHashLength << 24) );
+			// with nodeDepth = 0; innerHashLength = 0;
+
+			chainValue[4] = blake2s_IV[4];
+			chainValue[5] = blake2s_IV[5];
+			if (salt != null) {
+				chainValue[4] ^= (bytes2int(salt, 0));
+				chainValue[5] ^= (bytes2int(salt, 4));
+			}
+
+			chainValue[6] = blake2s_IV[6];
+			chainValue[7] = blake2s_IV[7];
+			if (personalization != null) {
+				chainValue[6] ^= (bytes2int(personalization, 0));
+				chainValue[7] ^= (bytes2int(personalization, 4));
+			}
+		}
+	}
+
+	private void initializeInternalState() {
+		// initialize v:
+		System.arraycopy(chainValue, 0, internalState, 0, chainValue.length);
+		System.arraycopy(blake2s_IV, 0, internalState, chainValue.length, 4);
+		internalState[12] = t0 ^ blake2s_IV[4];
+		internalState[13] = t1 ^ blake2s_IV[5];
+		internalState[14] = f0 ^ blake2s_IV[6];
+		internalState[15] = blake2s_IV[7];// ^ f1 with f1 = 0
+	}
+
+	/**
+	 * Update the message digest with a single byte.
+	 *
+	 * @param b the input byte to be entered.
+	 */
+	public void update(byte b) {
+		int remainingLength; // left bytes of buffer
+
+		// process the buffer if full else add to buffer:
+		remainingLength = BLOCK_LENGTH_BYTES - bufferPos;
+		if (remainingLength == 0) { // full buffer
+			t0 += BLOCK_LENGTH_BYTES;
+			if (t0 == 0) { // if message > 2^32
+				t1++;
+			}
+			compress(buffer, 0);
+			Arrays.fill(buffer, (byte)0);// clear buffer
+			buffer[0] = b;
+			bufferPos = 1;
+		} else {
+			buffer[bufferPos] = b;
+			bufferPos++;
+		}
+	}
+
+	/**
+	 * Update the message digest with a block of bytes.
+	 *
+	 * @param message the byte array containing the data.
+	 * @param offset the offset into the byte array where the data starts.
+	 * @param len the length of the data.
+	 */
+	public void update(byte[] message, int offset, int len) {
+		if (message == null || len == 0)
+			return;
+
+		int remainingLength = 0; // left bytes of buffer
+
+		if (bufferPos != 0) { // commenced, incomplete buffer
+
+			// complete the buffer:
+			remainingLength = BLOCK_LENGTH_BYTES - bufferPos;
+			if (remainingLength < len) { // full buffer + at least 1 byte
+				System.arraycopy(message, offset, buffer, bufferPos,
+						remainingLength);
+				t0 += BLOCK_LENGTH_BYTES;
+				if (t0 == 0) { // if message > 2^32
+					t1++;
+				}
+				compress(buffer, 0);
+				bufferPos = 0;
+				Arrays.fill(buffer, (byte) 0);// clear buffer
+			} else {
+				System.arraycopy(message, offset, buffer, bufferPos, len);
+				bufferPos += len;
+				return;
+			}
+		}
+
+		// process blocks except last block (also if last block is full)
+		int messagePos;
+		int blockWiseLastPos = offset + len - BLOCK_LENGTH_BYTES;
+		for (messagePos = offset + remainingLength;
+				messagePos < blockWiseLastPos;
+				messagePos += BLOCK_LENGTH_BYTES) { // block wise 64 bytes
+			// without buffer:
+			t0 += BLOCK_LENGTH_BYTES;
+			if (t0 == 0) {
+				t1++;
+			}
+			compress(message, messagePos);
+		}
+
+		// fill the buffer with left bytes, this might be a full block
+		System.arraycopy(message, messagePos, buffer, 0, offset + len
+				- messagePos);
+		bufferPos += offset + len - messagePos;
+	}
+
+	/**
+	 * Close the digest, producing the final digest value. The doFinal() call
+	 * leaves the digest reset. Key, salt and personal string remain.
+	 *
+	 * @param out the array the digest is to be copied into.
+	 * @param outOffset the offset into the out array the digest is to start at.
+	 */
+	public int doFinal(byte[] out, int outOffset) {
+		f0 = 0xFFFFFFFF;
+		t0 += bufferPos;
+		// bufferPos may be < 64, so (t0 == 0) does not work
+		// for 2^32 < message length > 2^32 - 63
+		if ((t0 < 0) && (bufferPos > -t0)) {
+			t1++;
+		}
+		compress(buffer, 0);
+		Arrays.fill(buffer, (byte) 0);// Holds eventually the key if input is null
+		Arrays.fill(internalState, 0);
+
+		for (int i = 0; i < chainValue.length && (i * 4 < digestLength); i++) {
+			byte[] bytes = int2bytes(chainValue[i]);
+
+			if (i * 4 < digestLength - 4) {
+				System.arraycopy(bytes, 0, out, outOffset + i * 4, 4);
+			} else {
+				System.arraycopy(bytes, 0, out, outOffset + i * 4,
+						digestLength - (i * 4));
+			}
+		}
+
+		Arrays.fill(chainValue, 0);
+
+		reset();
+
+		return digestLength;
+	}
+
+	/**
+	 * Reset the digest back to its initial state. The key, the salt and the
+	 * personal string will remain for further computations.
+	 */
+	public void reset() {
+		bufferPos = 0;
+		f0 = 0;
+		t0 = 0;
+		t1 = 0;
+		chainValue = null;
+		if (key != null) {
+			Arrays.fill(buffer, (byte) 0);
+			System.arraycopy(key, 0, buffer, 0, key.length);
+			bufferPos = BLOCK_LENGTH_BYTES; // zero padding
+		}
+		init();
+	}
+
+	private void compress(byte[] message, int messagePos) {
+		initializeInternalState();
+
+		int[] m = new int[16];
+		for (int j = 0; j < 16; j++) {
+			m[j] = bytes2int(message, messagePos + j * 4);
+		}
+
+		for (int round = 0; round < ROUNDS; round++) {
+
+			// G apply to columns of internalState:m[blake2s_sigma[round][2 *
+			// blockPos]] /+1
+			G(m[blake2s_sigma[round][0]], m[blake2s_sigma[round][1]], 0, 4, 8,
+					12);
+			G(m[blake2s_sigma[round][2]], m[blake2s_sigma[round][3]], 1, 5, 9,
+					13);
+			G(m[blake2s_sigma[round][4]], m[blake2s_sigma[round][5]], 2, 6, 10,
+					14);
+			G(m[blake2s_sigma[round][6]], m[blake2s_sigma[round][7]], 3, 7, 11,
+					15);
+			// G apply to diagonals of internalState:
+			G(m[blake2s_sigma[round][8]], m[blake2s_sigma[round][9]], 0, 5, 10,
+					15);
+			G(m[blake2s_sigma[round][10]], m[blake2s_sigma[round][11]], 1, 6,
+					11, 12);
+			G(m[blake2s_sigma[round][12]], m[blake2s_sigma[round][13]], 2, 7,
+					8, 13);
+			G(m[blake2s_sigma[round][14]], m[blake2s_sigma[round][15]], 3, 4,
+					9, 14);
+		}
+
+		// update chain values:
+		for (int offset = 0; offset < chainValue.length; offset++) {
+			chainValue[offset] = chainValue[offset] ^ internalState[offset]
+					^ internalState[offset + 8];
+		}
+	}
+
+	private void G(int m1, int m2, int posA, int posB, int posC, int posD) {
+		internalState[posA] = internalState[posA] + internalState[posB] + m1;
+		internalState[posD] = rotr32(internalState[posD] ^ internalState[posA],
+				16);
+		internalState[posC] = internalState[posC] + internalState[posD];
+		internalState[posB] = rotr32(internalState[posB] ^ internalState[posC],
+				12);
+		internalState[posA] = internalState[posA] + internalState[posB] + m2;
+		internalState[posD] = rotr32(internalState[posD] ^ internalState[posA],
+				8);
+		internalState[posC] = internalState[posC] + internalState[posD];
+		internalState[posB] = rotr32(internalState[posB] ^ internalState[posC],
+				7);
+	}
+
+	private int rotr32(int x, int rot) {
+		return x >>> rot | (x << (32 - rot));
+	}
+
+	// convert one int value in byte array
+	// little-endian byte order!
+	private byte[] int2bytes(int intValue) {
+		return new byte[] {
+				(byte) intValue, (byte) (intValue >> 8),
+				(byte) (intValue >> 16), (byte) (intValue >> 24)
+		};
+	}
+
+	// little-endian byte order!
+	private int bytes2int(byte[] byteArray, int offset) {
+		return (((int) byteArray[offset] & 0xFF)
+				| (((int) byteArray[offset + 1] & 0xFF) << 8)
+				| (((int) byteArray[offset + 2] & 0xFF) << 16)
+				| (((int) byteArray[offset + 3] & 0xFF) << 24));
+	}
+
+	/**
+	 * Return the algorithm name.
+	 *
+	 * @return the algorithm name
+	 */
+	public String getAlgorithmName() {
+		return "BLAKE2s";
+	}
+
+	/**
+	 * Return the size in bytes of the digest produced by this message digest.
+	 *
+	 * @return the size in bytes of the digest produced by this message digest.
+	 */
+	public int getDigestSize() {
+		return digestLength;
+	}
+
+	/**
+	 * Return the size in bytes of the internal buffer the digest applies its
+	 * compression function to.
+	 *
+	 * @return byte length of the digest's internal buffer.
+	 */
+	public int getByteLength() {
+		return BLOCK_LENGTH_BYTES;
+	}
+
+	/**
+	 * Overwrite the key if it is no longer used (zeroization).
+	 */
+	public void clearKey() {
+		if (key != null) {
+			Arrays.fill(key, (byte) 0);
+			Arrays.fill(buffer,  (byte) 0);
+		}
+	}
+
+	/**
+	 * Overwrite the salt (pepper) if it is secret and no longer used
+	 * (zeroization).
+	 */
+	public void clearSalt() {
+		if (salt != null) {
+			Arrays.fill(salt, (byte) 0);
+		}
+	}
+}
\ No newline at end of file
diff --git a/briar-tests/build.xml b/briar-tests/build.xml
index 6d8cc33ac30cf0a4391fa1ed2610c2da6b760b6c..33fcfa0e30596d11b9e2eca0e1cb162967e69157 100644
--- a/briar-tests/build.xml
+++ b/briar-tests/build.xml
@@ -93,6 +93,7 @@
 			<sysproperty key='java.library.path' value='../briar-desktop/libs'/>
 			<test name='org.briarproject.LockFairnessTest'/>
 			<test name='org.briarproject.ProtocolIntegrationTest'/>
+			<test name='org.briarproject.crypto.Blake2sDigestTest'/>
 			<test name='org.briarproject.crypto.EllipticCurveMultiplicationTest'/>
 			<test name='org.briarproject.crypto.FortunaGeneratorTest'/>
 			<test name='org.briarproject.crypto.FortunaSecureRandomTest'/>
diff --git a/briar-tests/src/org/briarproject/crypto/Blake2sDigestTest.java b/briar-tests/src/org/briarproject/crypto/Blake2sDigestTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..843e6857a903200267aa4b7bcc3f536d777612d8
--- /dev/null
+++ b/briar-tests/src/org/briarproject/crypto/Blake2sDigestTest.java
@@ -0,0 +1,172 @@
+package org.briarproject.crypto;
+
+import org.briarproject.BriarTestCase;
+import org.briarproject.util.StringUtils;
+import org.junit.Test;
+
+import java.util.Random;
+
+import static org.junit.Assert.assertArrayEquals;
+
+public class Blake2sDigestTest extends BriarTestCase {
+
+	// Vectors from BLAKE2 web site: https://blake2.net/blake2s-test.txt
+	private static final String[][] keyedTestVectors = {
+			// input/message, key, hash
+			{
+					"",
+					"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
+					"48a8997da407876b3d79c0d92325ad3b89cbb754d86ab71aee047ad345fd2c49",
+			},
+			{
+					"00",
+					"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
+					"40d15fee7c328830166ac3f918650f807e7e01e177258cdc0a39b11f598066f1",
+			},
+			{
+					"0001",
+					"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
+					"6bb71300644cd3991b26ccd4d274acd1adeab8b1d7914546c1198bbe9fc9d803",
+			},
+			{
+					"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d",
+					"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
+					"172ffc67153d12e0ca76a8b6cd5d4731885b39ce0cac93a8972a18006c8b8baf",
+			},
+			{
+					"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3",
+					"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
+					"4f8ce1e51d2fe7f24043a904d898ebfc91975418753413aa099b795ecb35cedb",
+			},
+			{
+					"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe",
+					"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
+					"3fb735061abc519dfe979e54c1ee5bfad0a9d858b3315bad34bde999efd724dd",
+			},
+	};
+
+	@Test
+	public void testDigestWithKeyedTestVectors() {
+		Blake2sDigest digest = new Blake2sDigest(StringUtils.fromHexString(
+				keyedTestVectors[0][1]));
+		for (String[] keyedTestVector : keyedTestVectors) {
+			byte[] input = StringUtils.fromHexString(keyedTestVector[0]);
+			digest.reset();
+
+			digest.update(input, 0, input.length);
+			byte[] hash = new byte[32];
+			digest.doFinal(hash, 0);
+
+			assertArrayEquals(StringUtils.fromHexString(keyedTestVector[2]),
+					hash);
+		}
+	}
+
+	@Test
+	public void testDigestWithKeyedTestVectorsAndRandomUpdate() {
+		Blake2sDigest digest = new Blake2sDigest(StringUtils.fromHexString(
+				keyedTestVectors[0][1]));
+		Random random = new Random();
+		for (int i = 0; i < 100; i++) {
+			for (String[] keyedTestVector : keyedTestVectors) {
+				byte[] input = StringUtils.fromHexString(keyedTestVector[0]);
+				if (input.length < 3) continue;
+				digest.reset();
+
+				int pos = random.nextInt(input.length);
+				if (pos > 0)
+					digest.update(input, 0, pos);
+				digest.update(input[pos]);
+				if (pos < (input.length - 1))
+					digest.update(input, pos + 1, input.length - (pos + 1));
+
+				byte[] hash = new byte[32];
+				digest.doFinal(hash, 0);
+
+				assertArrayEquals(StringUtils.fromHexString(keyedTestVector[2]),
+						hash);
+			}
+		}
+	}
+
+	@Test
+	public void testReset() {
+		// Generate a non-zero key
+		byte[] key = new byte[32];
+		for (byte i = 0; i < key.length; i++) key[i] = i;
+		// Generate some non-zero input longer than the key
+		byte[] input = new byte[key.length + 1];
+		for (byte i = 0; i < input.length; i++) input[i] = i;
+		// Hash the input
+		Blake2sDigest digest = new Blake2sDigest(key);
+		digest.update(input, 0, input.length);
+		byte[] hash = new byte[digest.getDigestSize()];
+		digest.doFinal(hash, 0);
+		// Create a second instance, hash the input without calling doFinal()
+		Blake2sDigest digest1 = new Blake2sDigest(key);
+		digest1.update(input, 0, input.length);
+		// Reset the second instance and hash the input again
+		digest1.reset();
+		digest1.update(input, 0, input.length);
+		byte[] hash1 = new byte[digest.getDigestSize()];
+		digest1.doFinal(hash1, 0);
+		// The hashes should be identical
+		assertArrayEquals(hash, hash1);
+	}
+
+	// Self-test routine from https://tools.ietf.org/html/rfc7693#appendix-E
+	private static final String SELF_TEST_RESULT =
+			"6A411F08CE25ADCDFB02ABA641451CEC53C598B24F4FC787FBDC88797F4C1DFE";
+	private static final int[] SELF_TEST_DIGEST_LEN = {16, 20, 28, 32};
+	private static final int[] SELF_TEST_INPUT_LEN = {0, 3, 64, 65, 255, 1024};
+
+	private static byte[] selfTestSequence(int len, int seed) {
+		int a = 0xDEAD4BAD * seed;
+		int b = 1;
+		int t;
+		byte[] out = new byte[len];
+
+		for (int i = 0; i < len; i++) {
+			t = a + b;
+			a = b;
+			b = t;
+			out[i] = (byte) ((t >> 24) & 0xFF);
+		}
+
+		return out;
+	}
+
+	@Test
+	public void runSelfTest() {
+		Blake2sDigest testDigest = new Blake2sDigest();
+		byte[] md = new byte[32];
+
+		for (int i = 0; i < 4; i++) {
+			int outlen = SELF_TEST_DIGEST_LEN[i];
+			for (int j = 0; j < 6; j++) {
+				int inlen = SELF_TEST_INPUT_LEN[j];
+
+				// unkeyed hash
+				byte[] in = selfTestSequence(inlen, inlen);
+				Blake2sDigest unkeyedDigest = new Blake2sDigest(outlen * 8);
+				unkeyedDigest.update(in, 0, inlen);
+				unkeyedDigest.doFinal(md, 0);
+				// hash the hash
+				testDigest.update(md, 0, outlen);
+
+				// keyed hash
+				byte[] key = selfTestSequence(outlen, outlen);
+				Blake2sDigest keyedDigest = new Blake2sDigest(key, outlen, null,
+						null);
+				keyedDigest.update(in, 0, inlen);
+				keyedDigest.doFinal(md, 0);
+				// hash the hash
+				testDigest.update(md, 0, outlen);
+			}
+		}
+
+		byte[] hash = new byte[32];
+		testDigest.doFinal(hash, 0);
+		assertArrayEquals(StringUtils.fromHexString(SELF_TEST_RESULT), hash);
+	}
+}