Skip to main content

encoded_len

Function encoded_len 

Source
pub fn encoded_len(
    algorithm: Algorithm,
    t_cost: u32,
    m_cost: u32,
    lanes: u32,
    salt_len: u32,
    hash_len: u32,
) -> usize
Expand description

argon2_encodedlen(...) from src/argon2.c.

strlen("$$v=$m=,t=,p=$$") + strlen(type) + numlen(t_cost) + numlen(m_cost)
  + numlen(parallelism) + b64len(saltlen) + b64len(hashlen)
  + numlen(ARGON2_VERSION_NUMBER) + 1

The trailing + 1 is the C string’s NUL terminator. It is kept so the value matches the C byte for byte; a Rust String is one byte shorter. It is also exactly the buffer size encode_string wants, which reserves the same byte (see there).

Note the C uses numlen(ARGON2_VERSION_NUMBER) and not the version actually being encoded. Both 0x10 (16) and 0x13 (19) are two digits, so it makes no difference; it is kept verbatim. (In the C it can differ, because ctx->version is a raw uint32_t: with version = 0 the string is one byte shorter than advertised, and a buffer of argon2_encodedlen() - 1 suffices. Measured on 1051 of 30000 fuzzed cases, all of them version = 0. Version is a closed enum of two two-digit values, so the size is always exact here — see encode_needs_encoded_len_bytes_exactly.)

§Argument order

t_cost comes before m_cost here, which is the opposite of the order a PHC string carries them in:

$argon2id$v=19$m=65536,t=3,p=1$<salt>$<tag>
               ^^^^^^^^^^^^^^^ the string reads m, then t, then p
encoded_len(algorithm, t_cost, m_cost, lanes, salt_len, hash_len)
                       ^^^^^^^^^^^^^^ this call reads t, then m

So a call transcribed field by field off a string is a call with the two costs swapped. Here that is harmless, and provably so rather than by luck: the result is a plain sum of num_len(t_cost) and num_len(m_cost), so it does not depend on which digit count came from which cost — pinned at every digit-count boundary by encoded_len_is_symmetric_in_m_and_t.

The transposition is not harmless anywhere that hashes, and the decoder behind Argon2::verify_encoded is where it would bite: m= must become the memory cost and t= the pass count, never the other way round, or a string verifies against a tag its writer never produced. Params itself is built through named setters — ParamsBuilder::memory and ParamsBuilder::passes — so there is no order to get wrong on that side. This function is the positional one.

The order is the C’s, kept so a call can be transcribed position for position: argon2_encodedlen(t_cost, m_cost, parallelism, saltlen, hashlen, type), declared at argon2.h:429 and defined at argon2.c:447. One argument did move. type went from last to first and became algorithm. The other five kept their order among themselves; parallelism is spelled lanes here, the name Params uses for it.

algorithm comes first here and on the rest of the encode-and-construct side; the verify family keeps the C’s trailing type and takes it last.

use argon2_rust::{Algorithm, Params, encoded_len, params::{Memory, TagLen}};

// The builder names each cost, so nothing here has an order to reverse.
let params = Params::builder()
    .memory(Memory::kib(65536))
    .passes(3)
    .lanes(1)
    .tag_len(TagLen::bytes(32))
    .build()?;
assert_eq!((params.memory_kib(), params.passes()), (65536, 3));

// `encoded_len` is positional, and it takes t_cost first: the same two
// costs, the other way round from the `m=65536,t=3` the string will show.
let n = encoded_len(
    Algorithm::Argon2id,
    params.passes(),
    params.memory_kib(),
    params.lanes(),
    16, // salt_len
    32, // hash_len
);
assert_eq!(n, 98);

§Against a string the crate really produced

The value is a C buffer size, so it counts the NUL terminator described above and a Rust String is one byte shorter. That is the whole of the relationship, and it is exact rather than an upper bound - see encode_needs_encoded_len_bytes_exactly:

use argon2_rust::{
    Algorithm, Argon2, Params, Version, encoded_len,
    params::{Memory, TagLen},
};

let params = Params::builder()
    .memory(Memory::kib(64))
    .passes(1)
    .lanes(1)
    .tag_len(TagLen::bytes(32))
    .build()?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let encoded = argon2.hash_encoded(b"password", b"somesalt")?;

// `salt_len` is the salt's own 8 bytes, not the 11 its base64 occupies.
// Note the `1, 64` against the string's `m=64,t=1`: t_cost comes first.
let n = encoded_len(Algorithm::Argon2id, 1, 64, 1, 8, 32);
assert_eq!(n, 84);
assert_eq!(encoded.len(), n - 1);
assert_eq!(
    encoded,
    "$argon2id$v=19$m=64,t=1,p=1$c29tZXNhbHQ$cpx6VEQbwTVZvcpxNIxOVUWZ5xnAipUmAe1cg2GMG70",
);