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    argon2_full(variant, params, password, salt, &[], &[], out)
323}
324
325/// Argon2 with the optional secret and associated-data inputs.
326///
327/// The `secret` is a site-wide pepper: an attacker who steals the password
328/// database but not the secret cannot mount an offline attack at all. Very few
329/// deployments use it, and those that do usually should.
330pub fn argon2_full(
331    variant: Variant,
332    params: &Argon2Params,
333    password: &[u8],
334    salt: &[u8],
335    secret: &[u8],
336    associated_data: &[u8],
337    out: &mut [u8],
338) -> Result<()> {
339    params.validate()?;
340    ensure!(
341        out.len() >= 4,
342        InvalidLength,
343        "argon2 tag must be >= 4 bytes"
344    );
345    ensure!(
346        salt.len() >= 8,
347        InvalidParameter,
348        "argon2 salt must be >= 8 bytes"
349    );
350
351    let lanes = params.lanes;
352    let passes = params.passes;
353
354    // Round the memory down to a multiple of 4*p blocks.
355    let blocks = core::cmp::max(params.memory_kib, 8 * lanes);
356    let blocks = (blocks / (SLICES as u32 * lanes)) * (SLICES as u32 * lanes);
357    let lane_len = (blocks / lanes) as usize;
358    let segment_len = lane_len / SLICES;
359    let total = blocks as usize;
360
361    // H0: a 64-byte seed over every input and every parameter.
362    //
363    // This is plain BLAKE2b-512, *not* the variable-length `H'` used for the
364    // memory blocks and the final tag. `H'` prefixes its output length to the
365    // input, which would corrupt the seed.
366    let mut h0 = [0u8; 72];
367    {
368        let mut hasher = Blake2b::new(64)?;
369        let le = |v: u32| v.to_le_bytes();
370        hasher.update(&le(lanes));
371        hasher.update(&le(out.len() as u32));
372        hasher.update(&le(params.memory_kib));
373        hasher.update(&le(passes));
374        hasher.update(&le(VERSION));
375        hasher.update(&le(variant.type_id()));
376        hasher.update(&le(password.len() as u32));
377        hasher.update(password);
378        hasher.update(&le(salt.len() as u32));
379        hasher.update(salt);
380        hasher.update(&le(secret.len() as u32));
381        hasher.update(secret);
382        hasher.update(&le(associated_data.len() as u32));
383        hasher.update(associated_data);
384
385        let mut seed = [0u8; 64];
386        hasher.finalize_into(&mut seed)?;
387        h0[..64].copy_from_slice(&seed);
388        seed.zeroize();
389    }
390
391    let mut memory = vec![Block::ZERO; total];
392
393    // The first two blocks of each lane come straight from H0.
394    for lane in 0..lanes {
395        for index in 0..2u32 {
396            h0[64..68].copy_from_slice(&index.to_le_bytes());
397            h0[68..72].copy_from_slice(&lane.to_le_bytes());
398            let mut block = [0u8; BLOCK_LEN];
399            blake2b_long(&[&h0], &mut block)?;
400            memory[lane as usize * lane_len + index as usize] = Block::from_bytes(&block);
401            block.zeroize();
402        }
403    }
404
405    // Fill the rest, segment by segment.
406    for pass in 0..passes {
407        for slice in 0..SLICES {
408            for lane in 0..lanes {
409                let mut addresses = [0u64; BLOCK_WORDS];
410                let independent = variant.independent(pass, slice);
411                let mut address_counter = 0u64;
412
413                let start = if pass == 0 && slice == 0 { 2 } else { 0 };
414                for index in start..segment_len {
415                    let position = slice * segment_len + index;
416                    let current = lane as usize * lane_len + position;
417                    let previous = if position == 0 {
418                        lane as usize * lane_len + lane_len - 1
419                    } else {
420                        current - 1
421                    };
422
423                    let (j1, j2) = if independent {
424                        // Refresh the address block every 128 positions.
425                        if index % BLOCK_WORDS == 0 || address_counter == 0 {
426                            address_counter += 1;
427                            let mut input = Block::ZERO;
428                            input.0[0] = pass as u64;
429                            input.0[1] = lane as u64;
430                            input.0[2] = slice as u64;
431                            input.0[3] = total as u64;
432                            input.0[4] = passes as u64;
433                            input.0[5] = variant.type_id() as u64;
434                            input.0[6] = address_counter;
435                            let zero = Block::ZERO;
436                            let tmp = compress(&zero, &input);
437                            let block = compress(&zero, &tmp);
438                            addresses.copy_from_slice(&block.0);
439                        }
440                        let word = addresses[index % BLOCK_WORDS];
441                        (word as u32, (word >> 32) as u32)
442                    } else {
443                        let word = memory[previous].0[0];
444                        (word as u32, (word >> 32) as u32)
445                    };
446
447                    let ref_index = reference_index(
448                        j1,
449                        j2,
450                        pass,
451                        lane,
452                        slice,
453                        index,
454                        lanes,
455                        lane_len,
456                        segment_len,
457                    );
458
459                    let mixed = compress(&memory[previous], &memory[ref_index]);
460                    memory[current] = if pass == 0 {
461                        mixed
462                    } else {
463                        // Later passes XOR into the existing block rather than
464                        // replacing it.
465                        mixed.xor(&memory[current])
466                    };
467                }
468            }
469        }
470    }
471
472    // The final block is the XOR of the last block of every lane.
473    let mut final_block = memory[lane_len - 1];
474    for lane in 1..lanes as usize {
475        final_block = final_block.xor(&memory[lane * lane_len + lane_len - 1]);
476    }
477
478    let bytes = final_block.to_bytes();
479    blake2b_long(&[&bytes], out)?;
480
481    // Wipe the whole arena: it is full of password-derived material.
482    for block in memory.iter_mut() {
483        block.0.zeroize();
484    }
485    h0.zeroize();
486    Ok(())
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492    use ic_core::codec::hex;
493
494    /// RFC 9106 §5 uses one input set for all three variants.
495    fn rfc_inputs() -> ([u8; 32], [u8; 16], [u8; 8], [u8; 12], Argon2Params) {
496        (
497            [0x01u8; 32],
498            [0x02u8; 16],
499            [0x03u8; 8],
500            [0x04u8; 12],
501            Argon2Params {
502                memory_kib: 32,
503                passes: 3,
504                lanes: 4,
505            },
506        )
507    }
508
509    fn rfc_tag(variant: Variant) -> String {
510        let (password, salt, secret, ad, params) = rfc_inputs();
511        let mut out = [0u8; 32];
512        argon2_full(variant, &params, &password, &salt, &secret, &ad, &mut out).unwrap();
513        hex(&out)
514    }
515
516    /// RFC 9106 §5.3, the recommended variant.
517    #[test]
518    fn rfc9106_argon2id_vector() {
519        assert_eq!(
520            rfc_tag(Variant::Argon2id),
521            "0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659"
522        );
523    }
524
525    /// RFC 9106 §5.1.
526    #[test]
527    fn rfc9106_argon2d_vector() {
528        assert_eq!(
529            rfc_tag(Variant::Argon2d),
530            "512b391b6f1162975371d30919734294f868e3be3984f3c1a13a4db9fabe4acb"
531        );
532    }
533
534    /// RFC 9106 §5.2.
535    #[test]
536    fn rfc9106_argon2i_vector() {
537        assert_eq!(
538            rfc_tag(Variant::Argon2i),
539            "c814d9d1dc7f37aa13f0d77f2494bda1c8de6b016dd388d29952a4c4672b6ce8"
540        );
541    }
542
543    #[test]
544    fn is_deterministic() {
545        let params = Argon2Params {
546            memory_kib: 32,
547            passes: 2,
548            lanes: 1,
549        };
550        let mut a = [0u8; 32];
551        let mut b = [0u8; 32];
552        argon2(Variant::Argon2id, &params, b"pw", b"salt-8-b", &mut a).unwrap();
553        argon2(Variant::Argon2id, &params, b"pw", b"salt-8-b", &mut b).unwrap();
554        assert_eq!(a, b);
555    }
556
557    #[test]
558    fn every_input_changes_the_tag() {
559        let params = Argon2Params {
560            memory_kib: 32,
561            passes: 1,
562            lanes: 1,
563        };
564        let base = {
565            let mut o = [0u8; 32];
566            argon2(Variant::Argon2id, &params, b"pw", b"salt-8-b", &mut o).unwrap();
567            o
568        };
569
570        let mut changed = [0u8; 32];
571        argon2(Variant::Argon2id, &params, b"px", b"salt-8-b", &mut changed).unwrap();
572        assert_ne!(base, changed, "password");
573
574        argon2(Variant::Argon2id, &params, b"pw", b"salt-8-c", &mut changed).unwrap();
575        assert_ne!(base, changed, "salt");
576
577        let more = Argon2Params {
578            passes: 2,
579            ..params
580        };
581        argon2(Variant::Argon2id, &more, b"pw", b"salt-8-b", &mut changed).unwrap();
582        assert_ne!(base, changed, "passes");
583
584        let bigger = Argon2Params {
585            memory_kib: 64,
586            ..params
587        };
588        argon2(Variant::Argon2id, &bigger, b"pw", b"salt-8-b", &mut changed).unwrap();
589        assert_ne!(base, changed, "memory");
590
591        argon2(Variant::Argon2d, &params, b"pw", b"salt-8-b", &mut changed).unwrap();
592        assert_ne!(base, changed, "variant");
593    }
594
595    /// The tag length is bound into H0, so a short tag is not a prefix of a
596    /// long one.
597    #[test]
598    fn tag_length_is_bound_in() {
599        let params = Argon2Params {
600            memory_kib: 32,
601            passes: 1,
602            lanes: 1,
603        };
604        let mut short = [0u8; 16];
605        let mut long = [0u8; 64];
606        argon2(Variant::Argon2id, &params, b"pw", b"salt-8-b", &mut short).unwrap();
607        argon2(Variant::Argon2id, &params, b"pw", b"salt-8-b", &mut long).unwrap();
608        assert_ne!(&long[..16], &short[..]);
609    }
610
611    #[test]
612    fn parallelism_is_honoured() {
613        let one = Argon2Params {
614            memory_kib: 64,
615            passes: 1,
616            lanes: 1,
617        };
618        let four = Argon2Params { lanes: 4, ..one };
619        let mut a = [0u8; 32];
620        let mut b = [0u8; 32];
621        argon2(Variant::Argon2id, &one, b"pw", b"salt-8-b", &mut a).unwrap();
622        argon2(Variant::Argon2id, &four, b"pw", b"salt-8-b", &mut b).unwrap();
623        assert_ne!(a, b);
624    }
625
626    #[test]
627    fn rejects_invalid_parameters() {
628        let ok = Argon2Params {
629            memory_kib: 32,
630            passes: 1,
631            lanes: 1,
632        };
633        let mut out = [0u8; 32];
634
635        assert!(
636            argon2(Variant::Argon2id, &ok, b"pw", b"short", &mut out).is_err(),
637            "salt"
638        );
639        assert!(
640            argon2(Variant::Argon2id, &ok, b"pw", b"salt-8-b", &mut [0u8; 3]).is_err(),
641            "tag too short"
642        );
643
644        let no_passes = Argon2Params { passes: 0, ..ok };
645        assert!(no_passes.validate().is_err());
646
647        let no_lanes = Argon2Params { lanes: 0, ..ok };
648        assert!(no_lanes.validate().is_err());
649
650        let too_little = Argon2Params {
651            memory_kib: 4,
652            lanes: 4,
653            passes: 1,
654        };
655        assert!(too_little.validate().is_err());
656    }
657
658    #[test]
659    fn recommended_parameters_validate() {
660        Argon2Params::RECOMMENDED.validate().unwrap();
661        Argon2Params::INTERACTIVE.validate().unwrap();
662    }
663
664    #[test]
665    fn variant_identifiers() {
666        assert_eq!(Variant::Argon2id.id(), "argon2id");
667        assert_eq!(Variant::Argon2id.type_id(), 2);
668        assert_eq!(Variant::Argon2i.type_id(), 1);
669        assert_eq!(Variant::Argon2d.type_id(), 0);
670    }
671
672    /// Argon2id must index independently only for the first two slices of the
673    /// first pass; that split is the entire difference from the other two.
674    #[test]
675    fn argon2id_switches_indexing_halfway() {
676        assert!(Variant::Argon2id.independent(0, 0));
677        assert!(Variant::Argon2id.independent(0, 1));
678        assert!(!Variant::Argon2id.independent(0, 2));
679        assert!(!Variant::Argon2id.independent(1, 0));
680
681        assert!(Variant::Argon2i.independent(5, 3));
682        assert!(!Variant::Argon2d.independent(0, 0));
683    }
684}