diff --git a/Cargo.lock b/Cargo.lock
index 7e439e61c..46b548ff8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -135,6 +135,7 @@ version = "0.2.1"
 dependencies = [
  "digest",
  "hex-literal",
+ "sha3 0.10.8",
 ]
 
 [[package]]
@@ -148,9 +149,9 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.142"
+version = "0.2.143"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
+checksum = "edc207893e85c5d6be840e969b496b53d94cec8be2d501b214f50daa97fa8024"
 
 [[package]]
 name = "md-5"
@@ -249,6 +250,16 @@ dependencies = [
  "keccak",
 ]
 
+[[package]]
+name = "sha3"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
+dependencies = [
+ "digest",
+ "keccak",
+]
+
 [[package]]
 name = "shabal"
 version = "0.4.1"
diff --git a/Cargo.toml b/Cargo.toml
index 7f544e284..00c103c38 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,6 +8,7 @@ members = [
     "gost94",
     "groestl",
     "k12",
+    "m14",
     "md2",
     "md4",
     "md5",
diff --git a/k12/Cargo.toml b/k12/Cargo.toml
index ada20f487..35b3ba4fb 100644
--- a/k12/Cargo.toml
+++ b/k12/Cargo.toml
@@ -12,7 +12,8 @@ keywords = ["crypto", "hash", "digest"]
 categories = ["cryptography", "no-std"]
 
 [dependencies]
-digest = { version = "0.10.3", features = ["alloc"] }
+digest = { version = "0.10.3", default-features = false, features = ["core-api"] }
+sha3 = { path = "../sha3" }
 
 [dev-dependencies]
 digest = { version = "0.10.3", features = ["alloc", "dev"] }
diff --git a/k12/src/lanes.rs b/k12/src/lanes.rs
deleted file mode 100644
index 8214fb4e0..000000000
--- a/k12/src/lanes.rs
+++ /dev/null
@@ -1,133 +0,0 @@
-#![allow(clippy::unreadable_literal)]
-
-macro_rules! REPEAT4 {
-    ($e: expr) => {
-        $e;
-        $e;
-        $e;
-        $e;
-    };
-}
-
-macro_rules! REPEAT5 {
-    ($e: expr) => {
-        $e;
-        $e;
-        $e;
-        $e;
-        $e;
-    };
-}
-
-macro_rules! REPEAT6 {
-    ($e: expr) => {
-        $e;
-        $e;
-        $e;
-        $e;
-        $e;
-        $e;
-    };
-}
-
-macro_rules! REPEAT24 {
-    ($e: expr, $s: expr) => {
-        REPEAT6!({
-            $e;
-            $s;
-        });
-        REPEAT6!({
-            $e;
-            $s;
-        });
-        REPEAT6!({
-            $e;
-            $s;
-        });
-        REPEAT5!({
-            $e;
-            $s;
-        });
-        $e;
-    };
-}
-
-macro_rules! FOR5 {
-    ($v: expr, $s: expr, $e: expr) => {
-        $v = 0;
-        REPEAT4!({
-            $e;
-            $v += $s;
-        });
-        $e;
-    };
-}
-
-pub const RC: [u64; 12] = [
-    0x000000008000808b,
-    0x800000000000008b,
-    0x8000000000008089,
-    0x8000000000008003,
-    0x8000000000008002,
-    0x8000000000000080,
-    0x000000000000800a,
-    0x800000008000000a,
-    0x8000000080008081,
-    0x8000000000008080,
-    0x0000000080000001,
-    0x8000000080008008,
-];
-
-// (0..24).map(|t| ((t+1)*(t+2)/2) % 64)
-pub const RHO: [u32; 24] = [
-    1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44,
-];
-pub const PI: [usize; 24] = [
-    10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1,
-];
-
-pub fn keccak(lanes: &mut [u64; 25]) {
-    let mut c = [0u64; 5];
-    let (mut x, mut y): (usize, usize);
-
-    #[allow(clippy::needless_range_loop)]
-    for round in 0..12 {
-        // θ
-        FOR5!(x, 1, {
-            c[x] = lanes[x] ^ lanes[x + 5] ^ lanes[x + 10] ^ lanes[x + 15] ^ lanes[x + 20];
-        });
-
-        FOR5!(x, 1, {
-            FOR5!(y, 5, {
-                lanes[x + y] ^= c[(x + 4) % 5] ^ c[(x + 1) % 5].rotate_left(1);
-            });
-        });
-
-        // ρ and π
-        let mut a = lanes[1];
-        x = 0;
-        REPEAT24!(
-            {
-                c[0] = lanes[PI[x]];
-                lanes[PI[x]] = a.rotate_left(RHO[x]);
-            },
-            {
-                a = c[0];
-                x += 1;
-            }
-        );
-
-        // χ
-        FOR5!(y, 5, {
-            FOR5!(x, 1, {
-                c[x] = lanes[x + y];
-            });
-            FOR5!(x, 1, {
-                lanes[x + y] = c[x] ^ ((!c[(x + 1) % 5]) & c[(x + 2) % 5]);
-            });
-        });
-
-        // ι
-        lanes[0] ^= RC[round];
-    }
-}
diff --git a/k12/src/lib.rs b/k12/src/lib.rs
index a85ee25a2..5fcd250db 100644
--- a/k12/src/lib.rs
+++ b/k12/src/lib.rs
@@ -1,12 +1,7 @@
 //! Experimental pure Rust implementation of the KangarooTwelve
 //! cryptographic hash algorithm, based on the reference implementation:
 //!
-//! <https://github.com/gvanas/KeccakCodePackage/blob/master/Standalone/kangaroo_twelve-reference/K12.py>
-//!
-//! Some optimisations copied from: <https://github.com/RustCrypto/hashes/tree/master/sha3/src>
-
-// Based off this translation originally by Diggory Hardy:
-// <https://github.com/dhardy/hash-bench/blob/master/src/k12.rs>
+//! <https://datatracker.ietf.org/doc/draft-irtf-cfrg-kangarootwelve/>
 
 #![no_std]
 #![doc(
@@ -16,242 +11,250 @@
 #![forbid(unsafe_code)]
 #![warn(missing_docs, rust_2018_idioms)]
 
-// TODO(tarcieri): eliminate alloc requirement
-#[macro_use]
-extern crate alloc;
-
 pub use digest;
 
-#[macro_use]
-mod lanes;
-
-// TODO(tarcieri): eliminate usage of `Vec`
-use alloc::vec::Vec;
-use core::{cmp::min, convert::TryInto, mem};
-use digest::{ExtendableOutput, ExtendableOutputReset, HashMarker, Reset, Update, XofReader};
+use core::fmt;
+use digest::block_buffer::Eager;
+use digest::consts::{U128, U168};
+use digest::core_api::{
+    AlgorithmName, Block, BlockSizeUser, Buffer, BufferKindUser, CoreWrapper, ExtendableOutputCore,
+    UpdateCore, XofReaderCore, XofReaderCoreWrapper,
+};
+use digest::{ExtendableOutputReset, HashMarker, Reset, Update, XofReader};
+
+use sha3::{TurboShake128, TurboShake128Core, TurboShake128ReaderCore};
+
+/// Implement tree hash functions such as KangarooTwelve or MarsupilamiFourteen.
+#[macro_export]
+macro_rules! impl_tree_hash {
+    (
+        $name:ident, $full_name:ident, $reader:ident, $reader_full:ident,
+        $tshk:ident, $tshk_full:ident, $tshk_reader_full:ident, $rate:ident, $round_count: ident, $chaining_value_size: ident, $alg_name:expr $(,)?
+    ) => {
+        const CHUNK_SIZE: usize = 8192;
+        const LENGTH_ENCODE_SIZE: usize = 255;
+
+        #[doc = "Core "]
+        #[doc = $alg_name]
+        #[doc = " hasher state."]
+        #[derive(Clone)]
+        #[allow(non_camel_case_types)]
+        pub struct $name<'cs> {
+            customization: &'cs [u8],
+            buffer: [u8; CHUNK_SIZE],
+            bufpos: usize,
+            final_tshk: $tshk_full,
+            chain_tshk: $tshk_full,
+            chain_length: usize,
+        }
 
-/// The KangarooTwelve extendable-output function (XOF).
-#[derive(Debug, Default)]
-pub struct KangarooTwelve {
-    /// Input to be processed
-    // TODO(tarcieri): don't store input in a `Vec`
-    buffer: Vec<u8>,
+        impl<'cs> $name<'cs> {
+            /// Creates a new KangarooTwelve instance with the given customization.
+            pub fn new(customization: &'cs [u8]) -> Self {
+                Self {
+                    customization,
+                    buffer: [0u8; CHUNK_SIZE],
+                    bufpos: 0usize,
+                    final_tshk: $tshk_full::from_core(<$tshk>::new_with_round_count(
+                        0x06,
+                        $round_count,
+                    )),
+                    chain_tshk: $tshk_full::from_core(<$tshk>::new_with_round_count(
+                        0x0B,
+                        $round_count,
+                    )),
+                    chain_length: 0usize,
+                }
+            }
+        }
 
-    /// Customization string to apply
-    // TODO(tarcieri): don't store customization in a `Vec`
-    customization: Vec<u8>,
-}
+        impl HashMarker for $name<'_> {}
 
-impl KangarooTwelve {
-    /// Create a new [`KangarooTwelve`] instance.
-    pub fn new() -> Self {
-        Self::default()
-    }
-
-    /// Create a new [`KangarooTwelve`] instance with the given customization.
-    pub fn new_with_customization(customization: impl AsRef<[u8]>) -> Self {
-        Self {
-            buffer: Vec::new(),
-            customization: customization.as_ref().into(),
+        impl BlockSizeUser for $name<'_> {
+            type BlockSize = U128;
         }
-    }
-}
 
-impl HashMarker for KangarooTwelve {}
-
-impl Update for KangarooTwelve {
-    fn update(&mut self, bytes: &[u8]) {
-        self.buffer.extend_from_slice(bytes);
-    }
-}
+        impl BufferKindUser for $name<'_> {
+            type BufferKind = Eager;
+        }
 
-impl ExtendableOutput for KangarooTwelve {
-    type Reader = Reader;
+        impl UpdateCore for $name<'_> {
+            #[inline]
+            fn update_blocks(&mut self, blocks: &[Block<Self>]) {
+                for block in blocks {
+                    self.buffer[self.bufpos..self.bufpos + 128].clone_from_slice(block);
+                    self.bufpos += 128;
+
+                    if self.bufpos != CHUNK_SIZE {
+                        continue;
+                    }
+
+                    if self.chain_length == 0 {
+                        self.final_tshk.update(&self.buffer);
+                        self.final_tshk
+                            .update(&[0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
+                    } else {
+                        let mut result = [0u8; $chaining_value_size];
+                        self.chain_tshk.update(&self.buffer);
+                        self.chain_tshk.finalize_xof_reset_into(&mut result);
+                        self.final_tshk.update(&result);
+                    }
+
+                    self.chain_length += 1;
+                    self.buffer = [0u8; CHUNK_SIZE];
+                    self.bufpos = 0;
+                }
+            }
+        }
 
-    fn finalize_xof(self) -> Self::Reader {
-        Reader {
-            buffer: self.buffer,
-            customization: self.customization,
-            finished: false,
+        impl ExtendableOutputCore for $name<'_> {
+            type ReaderCore = $reader;
+
+            #[inline]
+            fn finalize_xof_core(&mut self, buffer: &mut Buffer<Self>) -> Self::ReaderCore {
+                let mut lenbuf = [0u8; LENGTH_ENCODE_SIZE];
+
+                // Digest customization
+                buffer.digest_blocks(self.customization, |block| self.update_blocks(block));
+                buffer.digest_blocks(
+                    length_encode(self.customization.len(), &mut lenbuf),
+                    |block| self.update_blocks(block),
+                );
+
+                // Read leftover data from buffer
+                self.buffer[self.bufpos..(self.bufpos + buffer.get_pos())]
+                    .copy_from_slice(buffer.get_data());
+                self.bufpos += buffer.get_pos();
+
+                // Calculate final node
+                let tshk = if self.chain_length == 0 {
+                    // Input didnot exceed a single chaining value
+                    $tshk_full::from_core(<$tshk>::new_with_round_count(0x07, $round_count))
+                        .chain(&self.buffer[..self.bufpos])
+                        .finalize_xof_reset()
+                } else {
+                    // Calculate last chaining value
+                    let mut result = [0u8; $chaining_value_size];
+                    self.chain_tshk.update(&self.buffer[..self.bufpos]);
+                    self.chain_tshk.finalize_xof_reset_into(&mut result);
+                    self.final_tshk.update(&result);
+                    // Pad final node calculation
+                    self.final_tshk
+                        .update(length_encode(self.chain_length, &mut lenbuf));
+                    self.final_tshk.update(&[0xff, 0xff]);
+                    self.final_tshk.finalize_xof_reset()
+                };
+                $reader { tshk }
+            }
         }
-    }
-}
 
-impl ExtendableOutputReset for KangarooTwelve {
-    fn finalize_xof_reset(&mut self) -> Self::Reader {
-        let mut buffer = vec![];
-        let mut customization = vec![];
+        impl Default for $name<'_> {
+            #[inline]
+            fn default() -> Self {
+                Self {
+                    customization: &[],
+                    buffer: [0u8; CHUNK_SIZE],
+                    bufpos: 0usize,
+                    final_tshk: $tshk_full::from_core(<$tshk>::new_with_round_count(
+                        0x06,
+                        $round_count,
+                    )),
+                    chain_tshk: $tshk_full::from_core(<$tshk>::new_with_round_count(
+                        0x0B,
+                        $round_count,
+                    )),
+                    chain_length: 0usize,
+                }
+            }
+        }
 
-        mem::swap(&mut self.buffer, &mut buffer);
-        mem::swap(&mut self.customization, &mut customization);
+        impl Reset for $name<'_> {
+            #[inline]
+            fn reset(&mut self) {
+                *self = Self::new(self.customization);
+            }
+        }
 
-        Reader {
-            buffer,
-            customization,
-            finished: false,
+        impl AlgorithmName for $name<'_> {
+            fn write_alg_name(f: &mut fmt::Formatter<'_>) -> fmt::Result {
+                f.write_str(stringify!($full_name))
+            }
         }
-    }
-}
 
-impl Reset for KangarooTwelve {
-    fn reset(&mut self) {
-        self.buffer.clear();
-    }
-}
+        impl fmt::Debug for $name<'_> {
+            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+                f.write_str(concat!(stringify!($name), " { ... }"))
+            }
+        }
 
-/// Extensible output reader.
-///
-/// NOTE: this presently only supports one invocation and will *panic* if
-/// [`XofReader::read`] is invoked on it multiple times.
-#[derive(Debug, Default)]
-pub struct Reader {
-    /// Input to be processed
-    // TODO(tarcieri): don't store input in a `Vec`
-    buffer: Vec<u8>,
-
-    /// Customization string to apply
-    // TODO(tarcieri): don't store customization in a `Vec`
-    customization: Vec<u8>,
-
-    /// Has the XOF output already been consumed?
-    // TODO(tarcieri): allow `XofReader::result` to be called multiple times
-    finished: bool,
-}
+        #[doc = "Core "]
+        #[doc = $alg_name]
+        #[doc = " reader state."]
+        #[derive(Clone)]
+        #[allow(non_camel_case_types)]
+        pub struct $reader {
+            tshk: XofReaderCoreWrapper<$tshk_reader_full>,
+        }
 
-// TODO(tarcieri): factor more of this logic into the `KangarooTwelve` type
-impl XofReader for Reader {
-    /// Get the resulting output of the function.
-    ///
-    /// Panics if called multiple times on the same instance (TODO: don't panic!)
-    fn read(&mut self, output: &mut [u8]) {
-        assert!(
-            !self.finished,
-            "not yet implemented: multiple XofReader::read invocations unsupported"
-        );
-
-        let b = 8192;
-        let c = 256;
-
-        let mut slice = Vec::new(); // S
-        slice.extend_from_slice(&self.buffer);
-        slice.extend_from_slice(&self.customization);
-        slice.extend_from_slice(&right_encode(self.customization.len())[..]);
-
-        // === Cut the input string into chunks of b bytes ===
-        let n = (slice.len() + b - 1) / b;
-        let mut slices = Vec::with_capacity(n); // Si
-        for i in 0..n {
-            let ub = min((i + 1) * b, slice.len());
-            slices.push(&slice[i * b..ub]);
+        impl BlockSizeUser for $reader {
+            type BlockSize = $rate; // TurboSHAKE128 block size
         }
 
-        // TODO(tarcieri): get rid of intermediate output buffer
-        let tmp_buffer = if n == 1 {
-            // === Process the tree with only a final node ===
-            f(slices[0], 0x07, output.len())
-        } else {
-            // === Process the tree with kangaroo hopping ===
-            // TODO: in parallel
-            let mut intermediate = Vec::with_capacity(n - 1); // CVi
-            for i in 0..n - 1 {
-                intermediate.push(f(slices[i + 1], 0x0B, c / 8));
+        impl XofReaderCore for $reader {
+            #[inline]
+            fn read_block(&mut self) -> Block<Self> {
+                let mut block = Block::<Self>::default();
+                self.tshk.read(&mut block);
+                block
             }
+        }
 
-            let mut node_star = Vec::new();
-            node_star.extend_from_slice(slices[0]);
-            node_star.extend_from_slice(&[3, 0, 0, 0, 0, 0, 0, 0]);
-
-            #[allow(clippy::needless_range_loop)]
-            for i in 0..n - 1 {
-                node_star.extend_from_slice(&intermediate[i][..]);
-            }
+        #[doc = $alg_name]
+        #[doc = " hasher state."]
+        pub type $full_name<'cs> = CoreWrapper<$name<'cs>>;
 
-            node_star.extend_from_slice(&right_encode(n - 1));
-            node_star.extend_from_slice(b"\xFF\xFF");
+        #[doc = $alg_name]
+        #[doc = " reader state."]
+        pub type $reader_full = XofReaderCoreWrapper<$reader>;
 
-            f(&node_star[..], 0x06, output.len())
-        };
+        fn length_encode(mut length: usize, buffer: &mut [u8; LENGTH_ENCODE_SIZE]) -> &mut [u8] {
+            let mut bufpos = 0usize;
+            while length > 0 {
+                buffer[bufpos] = (length % 256) as u8;
+                length /= 256;
+                bufpos += 1;
+            }
+            buffer[..bufpos].reverse();
 
-        output.copy_from_slice(&tmp_buffer);
-        self.finished = true;
-    }
-}
+            buffer[bufpos] = bufpos as u8;
+            bufpos += 1;
 
-fn f(input: &[u8], suffix: u8, mut output_len: usize) -> Vec<u8> {
-    let mut state = [0u8; 200];
-    let max_block_size = 1344 / 8; // r, also known as rate in bytes
-
-    // === Absorb all the input blocks ===
-    // We unroll first loop, which allows simple copy
-    let mut block_size = min(input.len(), max_block_size);
-    state[0..block_size].copy_from_slice(&input[0..block_size]);
-
-    let mut offset = block_size;
-    while offset < input.len() {
-        keccak(&mut state);
-        block_size = min(input.len() - offset, max_block_size);
-        for i in 0..block_size {
-            // TODO: is this sufficiently optimisable or better to convert to u64 first?
-            state[i] ^= input[i + offset];
-        }
-        offset += block_size;
-    }
-    if block_size == max_block_size {
-        // TODO: condition is nearly always false; tests pass without this.
-        // Why is it here?
-        keccak(&mut state);
-        block_size = 0;
-    }
-
-    // === Do the padding and switch to the squeezing phase ===
-    state[block_size] ^= suffix;
-    if ((suffix & 0x80) != 0) && (block_size == (max_block_size - 1)) {
-        // TODO: condition is almost always false — in fact tests pass without
-        // this block! So why is it here?
-        keccak(&mut state);
-    }
-    state[max_block_size - 1] ^= 0x80;
-    keccak(&mut state);
-
-    // === Squeeze out all the output blocks ===
-    let mut output = Vec::with_capacity(output_len);
-    while output_len > 0 {
-        block_size = min(output_len, max_block_size);
-        output.extend_from_slice(&state[0..block_size]);
-        output_len -= block_size;
-        if output_len > 0 {
-            keccak(&mut state);
+            &mut buffer[..bufpos]
         }
-    }
-    output
-}
-
-fn keccak(state: &mut [u8; 200]) {
-    let mut lanes = [0u64; 25];
-    let mut y;
-    for x in 0..5 {
-        FOR5!(y, 5, {
-            let pos = 8 * (x + y);
-            lanes[x + y] = u64::from_le_bytes(state[pos..(pos + 8)].try_into().unwrap());
-        });
-    }
-    lanes::keccak(&mut lanes);
-    for x in 0..5 {
-        FOR5!(y, 5, {
-            let i = 8 * (x + y);
-            state[i..i + 8].copy_from_slice(&lanes[x + y].to_le_bytes());
-        });
-    }
+    };
 }
 
-fn right_encode(mut x: usize) -> Vec<u8> {
-    let mut slice = Vec::new();
-    while x > 0 {
-        slice.push((x % 256) as u8);
-        x /= 256;
-    }
-    slice.reverse();
-    let len = slice.len();
-    slice.push(len as u8);
-    slice
+const K12_ROUND_COUNT: usize = 12;
+const K12_CHAINING_VALUE_SIZE: usize = 32;
+
+impl_tree_hash!(
+    KangarooTwelveCore,
+    KangarooTwelve,
+    KangarooTwelveReaderCore,
+    KangarooTwelveReader,
+    TurboShake128Core,
+    TurboShake128,
+    TurboShake128ReaderCore,
+    U168,
+    K12_ROUND_COUNT,
+    K12_CHAINING_VALUE_SIZE,
+    "KangarooTwelve",
+);
+
+#[test]
+fn test_length_encode() {
+    let mut buffer = [0u8; LENGTH_ENCODE_SIZE];
+    assert_eq!(length_encode(0, &mut buffer), &[0x00]);
+    assert_eq!(length_encode(12, &mut buffer), &[0x0C, 0x01]);
+    assert_eq!(length_encode(65538, &mut buffer), &[0x01, 0x00, 0x02, 0x03]);
 }
diff --git a/k12/tests/mod.rs b/k12/tests/mod.rs
index bd27c4464..92456e046 100644
--- a/k12/tests/mod.rs
+++ b/k12/tests/mod.rs
@@ -2,11 +2,11 @@ use core::iter;
 use hex_literal::hex;
 use k12::{
     digest::{ExtendableOutput, Update},
-    KangarooTwelve,
+    KangarooTwelve, KangarooTwelveCore,
 };
 
 fn digest_and_box(data: &[u8], n: usize) -> Box<[u8]> {
-    let mut h = KangarooTwelve::new();
+    let mut h = KangarooTwelve::default();
     h.update(data);
     h.finalize_boxed(n)
 }
@@ -67,7 +67,7 @@ fn pat_c() {
         let m: Vec<u8> = iter::repeat(0xFF).take(2usize.pow(i) - 1).collect();
         let len = 41usize.pow(i);
         let c: Vec<u8> = (0..len).map(|j| (j % 251) as u8).collect();
-        let mut h = KangarooTwelve::new_with_customization(c);
+        let mut h = KangarooTwelve::from_core(KangarooTwelveCore::new(&c));
         h.update(&m);
         let result = h.finalize_boxed(32);
         assert_eq!(result[..], expected[i as usize][..]);
diff --git a/m14/CHANGELOG.md b/m14/CHANGELOG.md
new file mode 100644
index 000000000..12fdfb609
--- /dev/null
+++ b/m14/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## 0.0.1 (2023-05-08)
+- Initial release
diff --git a/m14/Cargo.toml b/m14/Cargo.toml
new file mode 100644
index 000000000..acb2d6cff
--- /dev/null
+++ b/m14/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "m14"
+version = "0.0.1"
+description = "Rust implementation of the MarsupilamiFourteen hash function"
+license = "Apache-2.0 OR MIT"
+readme = "README.md"
+edition = "2018"
+documentation = "https://docs.rs/m14"
+repository = "https://github.com/RustCrypto/hashes"
+keywords = ["crypto", "hash", "digest"]
+categories = ["cryptography", "no-std"]
+
+[dependencies]
+digest = { version = "0.10.3", default-features = false, features = ["core-api"] }
+k12 = { path = "../k12" }
+sha3 = { path = "../sha3" }
+
+[dev-dependencies]
+digest = { version = "0.10.3", features = ["alloc", "dev"] }
+hex-literal = "0.2.2"
+
+[features]
+default = ["std"]
+std = ["digest/std"]
diff --git a/m14/LICENSE-APACHE b/m14/LICENSE-APACHE
new file mode 100644
index 000000000..78173fa2e
--- /dev/null
+++ b/m14/LICENSE-APACHE
@@ -0,0 +1,201 @@
+                              Apache License
+                        Version 2.0, January 2004
+                     http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+   "License" shall mean the terms and conditions for use, reproduction,
+   and distribution as defined by Sections 1 through 9 of this document.
+
+   "Licensor" shall mean the copyright owner or entity authorized by
+   the copyright owner that is granting the License.
+
+   "Legal Entity" shall mean the union of the acting entity and all
+   other entities that control, are controlled by, or are under common
+   control with that entity. For the purposes of this definition,
+   "control" means (i) the power, direct or indirect, to cause the
+   direction or management of such entity, whether by contract or
+   otherwise, or (ii) ownership of fifty percent (50%) or more of the
+   outstanding shares, or (iii) beneficial ownership of such entity.
+
+   "You" (or "Your") shall mean an individual or Legal Entity
+   exercising permissions granted by this License.
+
+   "Source" form shall mean the preferred form for making modifications,
+   including but not limited to software source code, documentation
+   source, and configuration files.
+
+   "Object" form shall mean any form resulting from mechanical
+   transformation or translation of a Source form, including but
+   not limited to compiled object code, generated documentation,
+   and conversions to other media types.
+
+   "Work" shall mean the work of authorship, whether in Source or
+   Object form, made available under the License, as indicated by a
+   copyright notice that is included in or attached to the work
+   (an example is provided in the Appendix below).
+
+   "Derivative Works" shall mean any work, whether in Source or Object
+   form, that is based on (or derived from) the Work and for which the
+   editorial revisions, annotations, elaborations, or other modifications
+   represent, as a whole, an original work of authorship. For the purposes
+   of this License, Derivative Works shall not include works that remain
+   separable from, or merely link (or bind by name) to the interfaces of,
+   the Work and Derivative Works thereof.
+
+   "Contribution" shall mean any work of authorship, including
+   the original version of the Work and any modifications or additions
+   to that Work or Derivative Works thereof, that is intentionally
+   submitted to Licensor for inclusion in the Work by the copyright owner
+   or by an individual or Legal Entity authorized to submit on behalf of
+   the copyright owner. For the purposes of this definition, "submitted"
+   means any form of electronic, verbal, or written communication sent
+   to the Licensor or its representatives, including but not limited to
+   communication on electronic mailing lists, source code control systems,
+   and issue tracking systems that are managed by, or on behalf of, the
+   Licensor for the purpose of discussing and improving the Work, but
+   excluding communication that is conspicuously marked or otherwise
+   designated in writing by the copyright owner as "Not a Contribution."
+
+   "Contributor" shall mean Licensor and any individual or Legal Entity
+   on behalf of whom a Contribution has been received by Licensor and
+   subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   copyright license to reproduce, prepare Derivative Works of,
+   publicly display, publicly perform, sublicense, and distribute the
+   Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   (except as stated in this section) patent license to make, have made,
+   use, offer to sell, sell, import, and otherwise transfer the Work,
+   where such license applies only to those patent claims licensable
+   by such Contributor that are necessarily infringed by their
+   Contribution(s) alone or by combination of their Contribution(s)
+   with the Work to which such Contribution(s) was submitted. If You
+   institute patent litigation against any entity (including a
+   cross-claim or counterclaim in a lawsuit) alleging that the Work
+   or a Contribution incorporated within the Work constitutes direct
+   or contributory patent infringement, then any patent licenses
+   granted to You under this License for that Work shall terminate
+   as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+   Work or Derivative Works thereof in any medium, with or without
+   modifications, and in Source or Object form, provided that You
+   meet the following conditions:
+
+   (a) You must give any other recipients of the Work or
+       Derivative Works a copy of this License; and
+
+   (b) You must cause any modified files to carry prominent notices
+       stating that You changed the files; and
+
+   (c) You must retain, in the Source form of any Derivative Works
+       that You distribute, all copyright, patent, trademark, and
+       attribution notices from the Source form of the Work,
+       excluding those notices that do not pertain to any part of
+       the Derivative Works; and
+
+   (d) If the Work includes a "NOTICE" text file as part of its
+       distribution, then any Derivative Works that You distribute must
+       include a readable copy of the attribution notices contained
+       within such NOTICE file, excluding those notices that do not
+       pertain to any part of the Derivative Works, in at least one
+       of the following places: within a NOTICE text file distributed
+       as part of the Derivative Works; within the Source form or
+       documentation, if provided along with the Derivative Works; or,
+       within a display generated by the Derivative Works, if and
+       wherever such third-party notices normally appear. The contents
+       of the NOTICE file are for informational purposes only and
+       do not modify the License. You may add Your own attribution
+       notices within Derivative Works that You distribute, alongside
+       or as an addendum to the NOTICE text from the Work, provided
+       that such additional attribution notices cannot be construed
+       as modifying the License.
+
+   You may add Your own copyright statement to Your modifications and
+   may provide additional or different license terms and conditions
+   for use, reproduction, or distribution of Your modifications, or
+   for any such Derivative Works as a whole, provided Your use,
+   reproduction, and distribution of the Work otherwise complies with
+   the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+   any Contribution intentionally submitted for inclusion in the Work
+   by You to the Licensor shall be under the terms and conditions of
+   this License, without any additional terms or conditions.
+   Notwithstanding the above, nothing herein shall supersede or modify
+   the terms of any separate license agreement you may have executed
+   with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+   names, trademarks, service marks, or product names of the Licensor,
+   except as required for reasonable and customary use in describing the
+   origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+   agreed to in writing, Licensor provides the Work (and each
+   Contributor provides its Contributions) on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied, including, without limitation, any warranties or conditions
+   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+   PARTICULAR PURPOSE. You are solely responsible for determining the
+   appropriateness of using or redistributing the Work and assume any
+   risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+   whether in tort (including negligence), contract, or otherwise,
+   unless required by applicable law (such as deliberate and grossly
+   negligent acts) or agreed to in writing, shall any Contributor be
+   liable to You for damages, including any direct, indirect, special,
+   incidental, or consequential damages of any character arising as a
+   result of this License or out of the use or inability to use the
+   Work (including but not limited to damages for loss of goodwill,
+   work stoppage, computer failure or malfunction, or any and all
+   other commercial damages or losses), even if such Contributor
+   has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+   the Work or Derivative Works thereof, You may choose to offer,
+   and charge a fee for, acceptance of support, warranty, indemnity,
+   or other liability obligations and/or rights consistent with this
+   License. However, in accepting such obligations, You may act only
+   on Your own behalf and on Your sole responsibility, not on behalf
+   of any other Contributor, and only if You agree to indemnify,
+   defend, and hold each Contributor harmless for any liability
+   incurred by, or claims asserted against, such Contributor by reason
+   of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+   To apply the Apache License to your work, attach the following
+   boilerplate notice, with the fields enclosed by brackets "[]"
+   replaced with your own identifying information. (Don't include
+   the brackets!)  The text should be enclosed in the appropriate
+   comment syntax for the file format. We also recommend that a
+   file or class name and description of purpose be included on the
+   same "printed page" as the copyright notice for easier
+   identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+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.
diff --git a/m14/LICENSE-MIT b/m14/LICENSE-MIT
new file mode 100644
index 000000000..f39f9ff82
--- /dev/null
+++ b/m14/LICENSE-MIT
@@ -0,0 +1,25 @@
+Copyright (c) 2020 RustCrypto Developers
+
+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.
diff --git a/m14/README.md b/m14/README.md
new file mode 100644
index 000000000..4185e4326
--- /dev/null
+++ b/m14/README.md
@@ -0,0 +1,56 @@
+# RustCrypto: MarsupilamiFourteen
+
+[![crate][crate-image]][crate-link]
+[![Docs][docs-image]][docs-link]
+![Apache2/MIT licensed][license-image]
+![Rust Version][rustc-image]
+[![Build Status][build-image]][build-link]
+
+Pure Rust implementation of the [MarsupilamiFourteen][1] extensible-output
+function (XOF).
+
+[Documentation][docs-link]
+
+## Minimum Supported Rust Version
+
+Rust **1.41** or higher.
+
+Minimum supported Rust version can be changed in the future, but it will be
+done with a minor version bump.
+
+## SemVer Policy
+
+- All on-by-default features of this library are covered by SemVer
+- MSRV is considered exempt from SemVer as noted above
+
+## License
+
+Licensed under either of:
+
+ * [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
+ * [MIT license](http://opensource.org/licenses/MIT)
+
+at your option.
+
+### Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally submitted
+for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
+dual licensed as above, without any additional terms or conditions.
+
+[//]: # (badges)
+
+[crate-image]: https://img.shields.io/crates/v/m14.svg
+[crate-link]: https://crates.io/crates/m14
+[docs-image]: https://docs.rs/m14/badge.svg
+[docs-link]: https://docs.rs/m14/
+[license-image]: https://img.shields.io/badge/license-Apache2.0/MIT-blue.svg
+[rustc-image]: https://img.shields.io/badge/rustc-1.41+-blue.svg
+[chat-image]: https://img.shields.io/badge/zulip-join_chat-blue.svg
+[chat-link]: https://rustcrypto.zulipchat.com/#narrow/stream/260041-hashes
+[build-image]: https://github.com/RustCrypto/hashes/workflows/m14/badge.svg?branch=master
+[build-link]: https://github.com/RustCrypto/hashes/actions?query=workflow%3Am14
+
+[//]: # (general links)
+
+[1]: https://eprint.iacr.org/2016/770.pdf
diff --git a/m14/src/lib.rs b/m14/src/lib.rs
new file mode 100644
index 000000000..51622a827
--- /dev/null
+++ b/m14/src/lib.rs
@@ -0,0 +1,46 @@
+//! Rust implementation of the MarsupilamiFourteen cryptographic hash algorithm.
+//! MarsupilamiFourteen is a variant of KangarooTwelve aiming for 256-bit security
+//! strength (compared to 128-bit security strength of KangarooTwelve).
+//! The implementation is based on the reference implementation:
+//!
+//! <https://datatracker.ietf.org/doc/draft-irtf-cfrg-kangarootwelve/>
+
+#![no_std]
+#![doc(
+    html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg",
+    html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg"
+)]
+#![forbid(unsafe_code)]
+#![warn(missing_docs, rust_2018_idioms)]
+
+pub use digest;
+
+use core::fmt;
+use digest::block_buffer::Eager;
+use digest::consts::{U128, U136};
+use digest::core_api::{
+    AlgorithmName, Block, BlockSizeUser, Buffer, BufferKindUser, CoreWrapper, ExtendableOutputCore,
+    UpdateCore, XofReaderCore, XofReaderCoreWrapper,
+};
+use digest::{ExtendableOutputReset, HashMarker, Reset, Update, XofReader};
+
+use k12::impl_tree_hash;
+
+use sha3::{TurboShake256, TurboShake256Core, TurboShake256ReaderCore};
+
+const M14_ROUND_COUNT: usize = 14;
+const M14_CHAINING_VALUE_SIZE: usize = 64;
+
+impl_tree_hash!(
+    MarsupilamiFourteenCore,
+    MarsupilamiFourteen,
+    MarsupilamiFourteenReaderCore,
+    MarsupilamiFourteenReader,
+    TurboShake256Core,
+    TurboShake256,
+    TurboShake256ReaderCore,
+    U136,
+    M14_ROUND_COUNT,
+    M14_CHAINING_VALUE_SIZE,
+    "MarsupilamiFourteen",
+);
diff --git a/m14/tests/mod.rs b/m14/tests/mod.rs
new file mode 100644
index 000000000..ced533965
--- /dev/null
+++ b/m14/tests/mod.rs
@@ -0,0 +1,74 @@
+use core::iter;
+use hex_literal::hex;
+use m14::{
+    digest::{ExtendableOutput, Update},
+    MarsupilamiFourteen, MarsupilamiFourteenCore,
+};
+
+fn digest_and_box(data: &[u8], n: usize) -> Box<[u8]> {
+    let mut h = MarsupilamiFourteen::default();
+    h.update(data);
+    h.finalize_boxed(n)
+}
+
+#[test]
+#[rustfmt::skip]
+fn empty() {
+    // Source: reference paper
+    assert_eq!(
+        digest_and_box(b"", 32)[..],
+        hex!("6f66ef1474eb53807aa329257c768bb88893d9f086e51da2f5c80d17ca0fc57d")[..]
+    );
+
+    assert_eq!(
+        digest_and_box(b"", 64)[..],
+        hex!("
+            6f66ef1474eb53807aa329257c768bb88893d9f086e51da2f5c80d17ca0fc57d
+            5a24fac879014f8b30a3fdf5ac56ebafa219eb891d4bbbab7e1df3b27205b459
+        ")[..]
+    );
+
+    assert_eq!(
+        digest_and_box(b"", 10032)[10000..],
+        hex!("c09322de1513d0cd604728f36d11adff58b93f776381095a071921eafb30e1e3")[..]
+    );
+}
+
+#[test]
+fn pat_m() {
+    let expected = [
+        hex!("cc05ebc928156c7a03540085355c47c6aea1d07dc811cdded0e4c367f8d99368"),
+        hex!("aa764fd8b38f19976a305cb007f19384b210a5c7b0fc4499d6f83c6227bff850"),
+        hex!("f18a6e250b1cc83dea89ffbb4de56a8e70041c71fc5b17a2aaab05c606aa6bf2"),
+        hex!("0ac89b11a06f46b2f6feeff046c97e90dc02910ae509b8739cfea5df1df90b82"),
+        hex!("35af0a5fc6c4d111fbc68f879d05506aafd300b5ab136986d7aed8a9f1be331e"),
+        hex!("0c982c5d5334e27cc6591cda308dfa6b4fdd736aadbe64536bdef83c1d496ba0"),
+    ];
+    for i in 0..5
+    /*NOTE: can be up to 6 but is slow*/
+    {
+        let len = 17usize.pow(i);
+        let m: Vec<u8> = (0..len).map(|j| (j % 251) as u8).collect();
+        let result = digest_and_box(&m, 32);
+        assert_eq!(result[..], expected[i as usize][..]);
+    }
+}
+
+#[test]
+fn pat_c() {
+    let expected = [
+        hex!("e6c23ceeab2089d14dc3b088fdfe6d4418bf8a6f330fb3edcc300cd81e1bef2f"),
+        hex!("2bab75b31b8c3049abeb7674774771b64f59225be20e930ebdbf8e37c24fad69"),
+        hex!("732a60c308bebf5f7b3d3e8f0d26e324c04bab4197ca0a608b0befaa25ea5976"),
+        hex!("61583cdfaa64ab60e77b8c8bdd0ad088f9d760b2944f7d64c5dd81ce7e92d96b"),
+    ];
+    for i in 0..4 {
+        let m: Vec<u8> = iter::repeat(0xFF).take(2usize.pow(i) - 1).collect();
+        let len = 41usize.pow(i);
+        let c: Vec<u8> = (0..len).map(|j| (j % 251) as u8).collect();
+        let mut h = MarsupilamiFourteen::from_core(MarsupilamiFourteenCore::new(&c));
+        h.update(&m);
+        let result = h.finalize_boxed(32);
+        assert_eq!(result[..], expected[i as usize][..]);
+    }
+}
diff --git a/sha3/src/macros.rs b/sha3/src/macros.rs
index f26291305..ed78e7256 100644
--- a/sha3/src/macros.rs
+++ b/sha3/src/macros.rs
@@ -240,20 +240,36 @@ macro_rules! impl_turbo_shake {
         #[allow(non_camel_case_types)]
         pub struct $name {
             domain_separation: u8,
+            round_count: usize,
             state: Sha3State,
         }
 
         impl $name {
             /// Creates a new TurboSHAKE instance with the given domain separation.
             /// Note that the domain separation needs to be a byte with a value in
-            /// the range [0x01, . . . , 0x7F]
+            /// the range [0x01, . . . , 0x7F].
             pub fn new(domain_separation: u8) -> Self {
                 assert!((0x01..=0x7F).contains(&domain_separation));
                 Self {
                     domain_separation,
+                    round_count: TURBO_SHAKE_ROUND_COUNT,
                     state: Sha3State::new(TURBO_SHAKE_ROUND_COUNT),
                 }
             }
+
+            /// Creates a new TurboSHAKE instance with the given domain separation
+            /// and round_count.
+            /// This is a low-level "hazmat" API.
+            /// Note that the domain separation needs to be a byte with a value in
+            /// the range [0x01, . . . , 0x7F].
+            pub fn new_with_round_count(domain_separation: u8, round_count: usize) -> Self {
+                assert!((0x01..=0x7F).contains(&domain_separation));
+                Self {
+                    domain_separation,
+                    round_count,
+                    state: Sha3State::new(round_count),
+                }
+            }
         }
 
         impl HashMarker for $name {}
@@ -296,7 +312,7 @@ macro_rules! impl_turbo_shake {
         impl Reset for $name {
             #[inline]
             fn reset(&mut self) {
-                *self = Self::new(self.domain_separation);
+                *self = Self::new_with_round_count(self.domain_separation, self.round_count);
             }
         }