Skip to main content

ic_kdf/
argon2.rs

1//! RFC 9106 Argon2, the memory-hard password hash.
2//!
3//! **Not FIPS-approved**, and that is the whole tension: PBKDF2 is the only
4//! approved password KDF, and it is not memory-hard, so a GPU or FPGA attacks
5//! it far faster than a CPU can defend it. Argon2 forces an attacker to spend
6//! *memory* as well as time, which is what closes that gap. If you have a FIPS
7//! obligation you are stuck with PBKDF2; if you do not, use this.
8//!
9//! # Which variant
10//!
11//! * [`Variant::Argon2id`] — the default, and what RFC 9106 recommends. Its
12//!   first half-pass indexes data-independently (resisting side-channel
13//!   attacks on the memory access pattern) and the rest indexes
14//!   data-dependently (resisting time-memory trade-offs). Use this unless you
15//!   have a specific reason not to.
16//! * [`Variant::Argon2i`] — data-independent throughout. Only for threat models
17//!   where an attacker can observe memory access patterns *and* the extra
18//!   trade-off resistance is unwanted.
19//! * [`Variant::Argon2d`] — data-dependent throughout. Maximum trade-off
20//!   resistance, no side-channel resistance. Intended for settings with no
21//!   untrusted co-tenant, such as cryptocurrency proof-of-work.
22//!
23//! # Parameters
24//!
25//! ```
26//! use ic_kdf::argon2::{Argon2Params, Variant, argon2};
27//!
28//! let params = Argon2Params::RECOMMENDED;   // 2 GiB, t=1, p=4
29//! let params = Argon2Params::INTERACTIVE;   // 64 MiB, t=3, p=4
30//!
31//! let mut key = [0u8; 32];
32//! argon2(Variant::Argon2id, &params, b"password", b"a 16-byte salt..", &mut key)?;
33//! # Ok::<(), ic_core::Error>(())
34//! ```
35//!
36//! Memory is the parameter that matters; raising `t` on a small `m` buys far
37//! less than raising `m`.
38
39use alloc::vec;
40use ic_core::{ensure, Result, Zeroize};
41use ic_hash::{blake2b_long, Blake2b};
42
43/// Bytes in one Argon2 memory block.
44const BLOCK_LEN: usize = 1024;
45
46/// 64-bit words in one memory block.
47const BLOCK_WORDS: usize = BLOCK_LEN / 8;
48
49/// Argon2 synchronization points per pass.
50const SLICES: usize = 4;
51
52/// The version this implements (0x13, i.e. 1.3).
53const VERSION: u32 = 0x13;
54
55/// Which Argon2 variant to run.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum Variant {
58    /// Data-dependent indexing throughout.
59    Argon2d,
60    /// Data-independent indexing throughout.
61    Argon2i,
62    /// Data-independent for the first half-pass, data-dependent thereafter.
63    Argon2id,
64}
65
66impl Variant {
67    /// The type constant that goes into `H0`.
68    const fn type_id(self) -> u32 {
69        match self {
70            Self::Argon2d => 0,
71            Self::Argon2i => 1,
72            Self::Argon2id => 2,
73        }
74    }
75
76    /// Stable identifier used by the ontology and the CLI.
77    pub const fn id(self) -> &'static str {
78        match self {
79            Self::Argon2d => "argon2d",
80            Self::Argon2i => "argon2i",
81            Self::Argon2id => "argon2id",
82        }
83    }
84
85    /// Whether this pass and slice index data-independently.
86    fn independent(self, pass: u32, slice: usize) -> bool {
87        match self {
88            Self::Argon2d => false,
89            Self::Argon2i => true,
90            // Argon2id: the first two slices of the first pass only.
91            Self::Argon2id => pass == 0 && slice < 2,
92        }
93    }
94}
95
96/// Cost parameters.
97#[derive(Debug, Clone, Copy)]
98pub struct Argon2Params {
99    /// Memory in kibibytes.
100    pub memory_kib: u32,
101    /// Number of passes over the memory.
102    pub passes: u32,
103    /// Degree of parallelism (lanes).
104    pub lanes: u32,
105}
106
107impl Argon2Params {
108    /// RFC 9106's first recommendation: 2 GiB, one pass, four lanes.
109    ///
110    /// Use this for offline key derivation where a second of latency and two
111    /// gibibytes of resident memory are acceptable.
112    pub const RECOMMENDED: Argon2Params = Argon2Params {
113        memory_kib: 2 * 1024 * 1024,
114        passes: 1,
115        lanes: 4,
116    };
117
118    /// RFC 9106's second recommendation: 64 MiB, three passes, four lanes.
119    ///
120    /// For interactive logins, where the 2 GiB option would be a denial of
121    /// service against your own server.
122    pub const INTERACTIVE: Argon2Params = Argon2Params {
123        memory_kib: 64 * 1024,
124        passes: 3,
125        lanes: 4,
126    };
127
128    /// Check the parameters against RFC 9106's limits.
129    pub fn validate(&self) -> Result<()> {
130        ensure!(
131            self.lanes >= 1 && self.lanes <= 0x00FF_FFFF,
132            InvalidParameter,
133            "argon2 lanes must be 1..=2^24-1"
134        );
135        ensure!(
136            self.passes >= 1,
137            InvalidParameter,
138            "argon2 passes must be at least 1"
139        );
140        ensure!(
141            self.memory_kib >= 8 * self.lanes,
142            InvalidParameter,
143            "argon2 memory must be at least 8 KiB per lane"
144        );
145        Ok(())
146    }
147}
148
149/// One 1024-byte memory block, as 128 little-endian words.
150#[derive(Clone, Copy)]
151struct Block([u64; BLOCK_WORDS]);
152
153impl Block {
154    const ZERO: Block = Block([0u64; BLOCK_WORDS]);
155
156    fn from_bytes(bytes: &[u8; BLOCK_LEN]) -> Block {
157        let mut b = [0u64; BLOCK_WORDS];
158        for (i, word) in b.iter_mut().enumerate() {
159            let mut w = [0u8; 8];
160            w.copy_from_slice(&bytes[i * 8..i * 8 + 8]);
161            *word = u64::from_le_bytes(w);
162        }
163        Block(b)
164    }
165
166    fn to_bytes(self) -> [u8; BLOCK_LEN] {
167        let mut out = [0u8; BLOCK_LEN];
168        for (chunk, word) in out.chunks_exact_mut(8).zip(self.0.iter()) {
169            chunk.copy_from_slice(&word.to_le_bytes());
170        }
171        out
172    }
173
174    fn xor(&self, other: &Block) -> Block {
175        let mut out = [0u64; BLOCK_WORDS];
176        for ((slot, a), b) in out.iter_mut().zip(self.0.iter()).zip(other.0.iter()) {
177            *slot = a ^ b;
178        }
179        Block(out)
180    }
181}
182
183/// Argon2's modified BLAKE2b mixing function.
184///
185/// The extra `2 * lo(a) * lo(b)` term is what distinguishes it from plain
186/// BLAKE2b: it makes the round function non-linear over 64-bit words, which is
187/// what forces an attacker to actually perform the multiplications rather than
188/// shortcutting them.
189#[inline(always)]
190fn gb(v: &mut [u64; 16], a: usize, b: usize, c: usize, d: usize) {
191    #[inline(always)]
192    fn mix(x: u64, y: u64) -> u64 {
193        x.wrapping_add(y).wrapping_add(
194            2u64.wrapping_mul(x & 0xFFFF_FFFF)
195                .wrapping_mul(y & 0xFFFF_FFFF),
196        )
197    }
198
199    v[a] = mix(v[a], v[b]);
200    v[d] = (v[d] ^ v[a]).rotate_right(32);
201    v[c] = mix(v[c], v[d]);
202    v[b] = (v[b] ^ v[c]).rotate_right(24);
203    v[a] = mix(v[a], v[b]);
204    v[d] = (v[d] ^ v[a]).rotate_right(16);
205    v[c] = mix(v[c], v[d]);
206    v[b] = (v[b] ^ v[c]).rotate_right(63);
207}
208
209/// The permutation `P` over sixteen words.
210#[inline]
211fn permute(v: &mut [u64; 16]) {
212    gb(v, 0, 4, 8, 12);
213    gb(v, 1, 5, 9, 13);
214    gb(v, 2, 6, 10, 14);
215    gb(v, 3, 7, 11, 15);
216    gb(v, 0, 5, 10, 15);
217    gb(v, 1, 6, 11, 12);
218    gb(v, 2, 7, 8, 13);
219    gb(v, 3, 4, 9, 14);
220}
221
222/// The compression function `G`.
223///
224/// `R = X ^ Y`, then `P` over each row, then `P` over each column, then XOR
225/// with `R` again. The row-then-column structure is what makes every output
226/// word depend on every input word.
227fn compress(x: &Block, y: &Block) -> Block {
228    let r = x.xor(y);
229    let mut q = r;
230
231    // Rows: eight groups of sixteen consecutive words.
232    for row in 0..8 {
233        let mut v = [0u64; 16];
234        v.copy_from_slice(&q.0[row * 16..row * 16 + 16]);
235        permute(&mut v);
236        q.0[row * 16..row * 16 + 16].copy_from_slice(&v);
237    }
238
239    // Columns: word pairs strided across the rows.
240    for col in 0..8 {
241        let mut v = [0u64; 16];
242        for i in 0..8 {
243            v[i * 2] = q.0[i * 16 + col * 2];
244            v[i * 2 + 1] = q.0[i * 16 + col * 2 + 1];
245        }
246        permute(&mut v);
247        for i in 0..8 {
248            q.0[i * 16 + col * 2] = v[i * 2];
249            q.0[i * 16 + col * 2 + 1] = v[i * 2 + 1];
250        }
251    }
252
253    q.xor(&r)
254}
255
256/// Map `(j1, j2)` onto a reference block index, per RFC 9106 §3.4.1.2.
257#[allow(clippy::too_many_arguments)]
258fn reference_index(
259    j1: u32,
260    j2: u32,
261    pass: u32,
262    lane: u32,
263    slice: usize,
264    index: usize,
265    lanes: u32,
266    lane_len: usize,
267    segment_len: usize,
268) -> usize {
269    // Which lane the reference comes from. On the very first segment there is
270    // nothing in the other lanes yet, so it must be our own.
271    let ref_lane = if pass == 0 && slice == 0 {
272        lane
273    } else {
274        j2 % lanes
275    };
276
277    // How many finished blocks are visible to this position.
278    let same_lane = ref_lane == lane;
279    let mut reference_area = if pass == 0 {
280        if slice == 0 {
281            index - 1
282        } else if same_lane {
283            slice * segment_len + index - 1
284        } else {
285            slice * segment_len - usize::from(index == 0)
286        }
287    } else if same_lane {
288        lane_len - segment_len + index - 1
289    } else {
290        lane_len - segment_len - usize::from(index == 0)
291    };
292    if reference_area == usize::MAX {
293        reference_area = 0;
294    }
295
296    // A non-uniform map that favours recent blocks, which is what makes a
297    // time-memory trade-off expensive.
298    let x = ((j1 as u64) * (j1 as u64)) >> 32;
299    let y = ((reference_area as u64) * x) >> 32;
300    let z = (reference_area as u64) - 1 - y;
301
302    let start = if pass == 0 || slice == SLICES - 1 {
303        0
304    } else {
305        (slice + 1) * segment_len
306    };
307    let position = (start as u64 + z) % (lane_len as u64);
308    (ref_lane as usize) * lane_len + position as usize
309}
310
311/// Run Argon2 and write `out.len()` bytes of tag.
312///
313/// `out` must be at least 4 bytes. `salt` must be at least 8 bytes; RFC 9106
314/// recommends 16, which is what [`Argon2Params`] documentation assumes.
315pub fn argon2(
316    variant: Variant,
317    params: &Argon2Params,
318    password: &[u8],
319    salt: &[u8],
320    out: &mut [u8],
321) -> Result<()> {
322    ic_core::module::operational()?;
323    argon2_full(variant, params, password, salt, &[], &[], out)
324}
325
326/// Argon2 with the optional secret and associated-data inputs.
327///
328/// The `secret` is a site-wide pepper: an attacker who steals the password
329/// database but not the secret cannot mount an offline attack at all. Very few
330/// deployments use it, and those that do usually should.
331pub fn argon2_full(
332    variant: Variant,
333    params: &Argon2Params,
334    password: &[u8],
335    salt: &[u8],
336    secret: &[u8],
337    associated_data: &[u8],
338    out: &mut [u8],
339) -> Result<()> {
340    ic_core::module::operational()?;
341    params.validate()?;
342    ensure!(
343        out.len() >= 4,
344        InvalidLength,
345        "argon2 tag must be >= 4 bytes"
346    );
347    ensure!(
348        salt.len() >= 8,
349        InvalidParameter,
350        "argon2 salt must be >= 8 bytes"
351    );
352
353    let lanes = params.lanes;
354    let passes = params.passes;
355
356    // Round the memory down to a multiple of 4*p blocks.
357    let blocks = core::cmp::max(params.memory_kib, 8 * lanes);
358    let blocks = (blocks / (SLICES as u32 * lanes)) * (SLICES as u32 * lanes);
359    let lane_len = (blocks / lanes) as usize;
360    let segment_len = lane_len / SLICES;
361    let total = blocks as usize;
362
363    // H0: a 64-byte seed over every input and every parameter.
364    //
365    // This is plain BLAKE2b-512, *not* the variable-length `H'` used for the
366    // memory blocks and the final tag. `H'` prefixes its output length to the
367    // input, which would corrupt the seed.
368    let mut h0 = [0u8; 72];
369    {
370        let mut hasher = Blake2b::new(64)?;
371        let le = |v: u32| v.to_le_bytes();
372        hasher.update(&le(lanes));
373        hasher.update(&le(out.len() as u32));
374        hasher.update(&le(params.memory_kib));
375        hasher.update(&le(passes));
376        hasher.update(&le(VERSION));
377        hasher.update(&le(variant.type_id()));
378        hasher.update(&le(password.len() as u32));
379        hasher.update(password);
380        hasher.update(&le(salt.len() as u32));
381        hasher.update(salt);
382        hasher.update(&le(secret.len() as u32));
383        hasher.update(secret);
384        hasher.update(&le(associated_data.len() as u32));
385        hasher.update(associated_data);
386
387        let mut seed = [0u8; 64];
388        hasher.finalize_into(&mut seed)?;
389        h0[..64].copy_from_slice(&seed);
390        seed.zeroize();
391    }
392
393    let mut memory = vec![Block::ZERO; total];
394
395    // The first two blocks of each lane come straight from H0.
396    for lane in 0..lanes {
397        for index in 0..2u32 {
398            h0[64..68].copy_from_slice(&index.to_le_bytes());
399            h0[68..72].copy_from_slice(&lane.to_le_bytes());
400            let mut block = [0u8; BLOCK_LEN];
401            blake2b_long(&[&h0], &mut block)?;
402            memory[lane as usize * lane_len + index as usize] = Block::from_bytes(&block);
403            block.zeroize();
404        }
405    }
406
407    // Fill the rest, segment by segment.
408    for pass in 0..passes {
409        for slice in 0..SLICES {
410            for lane in 0..lanes {
411                let mut addresses = [0u64; BLOCK_WORDS];
412                let independent = variant.independent(pass, slice);
413                let mut address_counter = 0u64;
414
415                let start = if pass == 0 && slice == 0 { 2 } else { 0 };
416                for index in start..segment_len {
417                    let position = slice * segment_len + index;
418                    let current = lane as usize * lane_len + position;
419                    let previous = if position == 0 {
420                        lane as usize * lane_len + lane_len - 1
421                    } else {
422                        current - 1
423                    };
424
425                    let (j1, j2) = if independent {
426                        // Refresh the address block every 128 positions.
427                        if index % BLOCK_WORDS == 0 || address_counter == 0 {
428                            address_counter += 1;
429                            let mut input = Block::ZERO;
430                            input.0[0] = pass as u64;
431                            input.0[1] = lane as u64;
432                            input.0[2] = slice as u64;
433                            input.0[3] = total as u64;
434                            input.0[4] = passes as u64;
435                            input.0[5] = variant.type_id() as u64;
436                            input.0[6] = address_counter;
437                            let zero = Block::ZERO;
438                            let tmp = compress(&zero, &input);
439                            let block = compress(&zero, &tmp);
440                            addresses.copy_from_slice(&block.0);
441                        }
442                        let word = addresses[index % BLOCK_WORDS];
443                        (word as u32, (word >> 32) as u32)
444                    } else {
445                        let word = memory[previous].0[0];
446                        (word as u32, (word >> 32) as u32)
447                    };
448
449                    let ref_index = reference_index(
450                        j1,
451                        j2,
452                        pass,
453                        lane,
454                        slice,
455                        index,
456                        lanes,
457                        lane_len,
458                        segment_len,
459                    );
460
461                    let mixed = compress(&memory[previous], &memory[ref_index]);
462                    memory[current] = if pass == 0 {
463                        mixed
464                    } else {
465                        // Later passes XOR into the existing block rather than
466                        // replacing it.
467                        mixed.xor(&memory[current])
468                    };
469                }
470            }
471        }
472    }
473
474    // The final block is the XOR of the last block of every lane.
475    let mut final_block = memory[lane_len - 1];
476    for lane in 1..lanes as usize {
477        final_block = final_block.xor(&memory[lane * lane_len + lane_len - 1]);
478    }
479
480    let bytes = final_block.to_bytes();
481    blake2b_long(&[&bytes], out)?;
482
483    // Wipe the whole arena: it is full of password-derived material.
484    for block in memory.iter_mut() {
485        block.0.zeroize();
486    }
487    h0.zeroize();
488    Ok(())
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494    use ic_core::codec::hex;
495
496    /// RFC 9106 §5 uses one input set for all three variants.
497    fn rfc_inputs() -> ([u8; 32], [u8; 16], [u8; 8], [u8; 12], Argon2Params) {
498        (
499            [0x01u8; 32],
500            [0x02u8; 16],
501            [0x03u8; 8],
502            [0x04u8; 12],
503            Argon2Params {
504                memory_kib: 32,
505                passes: 3,
506                lanes: 4,
507            },
508        )
509    }
510
511    fn rfc_tag(variant: Variant) -> String {
512        let (password, salt, secret, ad, params) = rfc_inputs();
513        let mut out = [0u8; 32];
514        argon2_full(variant, &params, &password, &salt, &secret, &ad, &mut out).unwrap();
515        hex(&out)
516    }
517
518    /// RFC 9106 §5.3, the recommended variant.
519    #[test]
520    fn rfc9106_argon2id_vector() {
521        assert_eq!(
522            rfc_tag(Variant::Argon2id),
523            "0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659"
524        );
525    }
526
527    /// RFC 9106 §5.1.
528    #[test]
529    fn rfc9106_argon2d_vector() {
530        assert_eq!(
531            rfc_tag(Variant::Argon2d),
532            "512b391b6f1162975371d30919734294f868e3be3984f3c1a13a4db9fabe4acb"
533        );
534    }
535
536    /// RFC 9106 §5.2.
537    #[test]
538    fn rfc9106_argon2i_vector() {
539        assert_eq!(
540            rfc_tag(Variant::Argon2i),
541            "c814d9d1dc7f37aa13f0d77f2494bda1c8de6b016dd388d29952a4c4672b6ce8"
542        );
543    }
544
545    #[test]
546    fn is_deterministic() {
547        let params = Argon2Params {
548            memory_kib: 32,
549            passes: 2,
550            lanes: 1,
551        };
552        let mut a = [0u8; 32];
553        let mut b = [0u8; 32];
554        argon2(Variant::Argon2id, &params, b"pw", b"salt-8-b", &mut a).unwrap();
555        argon2(Variant::Argon2id, &params, b"pw", b"salt-8-b", &mut b).unwrap();
556        assert_eq!(a, b);
557    }
558
559    #[test]
560    fn every_input_changes_the_tag() {
561        let params = Argon2Params {
562            memory_kib: 32,
563            passes: 1,
564            lanes: 1,
565        };
566        let base = {
567            let mut o = [0u8; 32];
568            argon2(Variant::Argon2id, &params, b"pw", b"salt-8-b", &mut o).unwrap();
569            o
570        };
571
572        let mut changed = [0u8; 32];
573        argon2(Variant::Argon2id, &params, b"px", b"salt-8-b", &mut changed).unwrap();
574        assert_ne!(base, changed, "password");
575
576        argon2(Variant::Argon2id, &params, b"pw", b"salt-8-c", &mut changed).unwrap();
577        assert_ne!(base, changed, "salt");
578
579        let more = Argon2Params {
580            passes: 2,
581            ..params
582        };
583        argon2(Variant::Argon2id, &more, b"pw", b"salt-8-b", &mut changed).unwrap();
584        assert_ne!(base, changed, "passes");
585
586        let bigger = Argon2Params {
587            memory_kib: 64,
588            ..params
589        };
590        argon2(Variant::Argon2id, &bigger, b"pw", b"salt-8-b", &mut changed).unwrap();
591        assert_ne!(base, changed, "memory");
592
593        argon2(Variant::Argon2d, &params, b"pw", b"salt-8-b", &mut changed).unwrap();
594        assert_ne!(base, changed, "variant");
595    }
596
597    /// The tag length is bound into H0, so a short tag is not a prefix of a
598    /// long one.
599    #[test]
600    fn tag_length_is_bound_in() {
601        let params = Argon2Params {
602            memory_kib: 32,
603            passes: 1,
604            lanes: 1,
605        };
606        let mut short = [0u8; 16];
607        let mut long = [0u8; 64];
608        argon2(Variant::Argon2id, &params, b"pw", b"salt-8-b", &mut short).unwrap();
609        argon2(Variant::Argon2id, &params, b"pw", b"salt-8-b", &mut long).unwrap();
610        assert_ne!(&long[..16], &short[..]);
611    }
612
613    #[test]
614    fn parallelism_is_honoured() {
615        let one = Argon2Params {
616            memory_kib: 64,
617            passes: 1,
618            lanes: 1,
619        };
620        let four = Argon2Params { lanes: 4, ..one };
621        let mut a = [0u8; 32];
622        let mut b = [0u8; 32];
623        argon2(Variant::Argon2id, &one, b"pw", b"salt-8-b", &mut a).unwrap();
624        argon2(Variant::Argon2id, &four, b"pw", b"salt-8-b", &mut b).unwrap();
625        assert_ne!(a, b);
626    }
627
628    #[test]
629    fn rejects_invalid_parameters() {
630        let ok = Argon2Params {
631            memory_kib: 32,
632            passes: 1,
633            lanes: 1,
634        };
635        let mut out = [0u8; 32];
636
637        assert!(
638            argon2(Variant::Argon2id, &ok, b"pw", b"short", &mut out).is_err(),
639            "salt"
640        );
641        assert!(
642            argon2(Variant::Argon2id, &ok, b"pw", b"salt-8-b", &mut [0u8; 3]).is_err(),
643            "tag too short"
644        );
645
646        let no_passes = Argon2Params { passes: 0, ..ok };
647        assert!(no_passes.validate().is_err());
648
649        let no_lanes = Argon2Params { lanes: 0, ..ok };
650        assert!(no_lanes.validate().is_err());
651
652        let too_little = Argon2Params {
653            memory_kib: 4,
654            lanes: 4,
655            passes: 1,
656        };
657        assert!(too_little.validate().is_err());
658    }
659
660    #[test]
661    fn recommended_parameters_validate() {
662        Argon2Params::RECOMMENDED.validate().unwrap();
663        Argon2Params::INTERACTIVE.validate().unwrap();
664    }
665
666    #[test]
667    fn variant_identifiers() {
668        assert_eq!(Variant::Argon2id.id(), "argon2id");
669        assert_eq!(Variant::Argon2id.type_id(), 2);
670        assert_eq!(Variant::Argon2i.type_id(), 1);
671        assert_eq!(Variant::Argon2d.type_id(), 0);
672    }
673
674    /// Argon2id must index independently only for the first two slices of the
675    /// first pass; that split is the entire difference from the other two.
676    #[test]
677    fn argon2id_switches_indexing_halfway() {
678        assert!(Variant::Argon2id.independent(0, 0));
679        assert!(Variant::Argon2id.independent(0, 1));
680        assert!(!Variant::Argon2id.independent(0, 2));
681        assert!(!Variant::Argon2id.independent(1, 0));
682
683        assert!(Variant::Argon2i.independent(5, 3));
684        assert!(!Variant::Argon2d.independent(0, 0));
685    }
686}