def _measure_scaled_params(baseline: Measurement) -> typ.List[Measurement]: measurements = [baseline] while len(measurements) < 4: baseline = measurements[0] if len(measurements) == 1: m = baseline.m * PARAM_SCALING t = baseline.t elif len(measurements) == 2: m = baseline.m t = baseline.t * PARAM_SCALING elif len(measurements) == 3: m = baseline.m * PARAM_SCALING t = baseline.t * PARAM_SCALING else: # To increase accuracy, repeat measurement with previous # parameters and use the lower measurement. measurement = measurements[len(measurements) % 4] m = measurement.m t = measurement.t kdf_params = kdf.init_kdf_params(m, t) measurement = measure(kdf_params) measurements.append(measurement) return measurements
def test_digest_len(): kdf_params = kdf.init_kdf_params(p=1, m=10, t=1) secret_data = b"\x01\x23\x45\x67\x89\xab\xcd\xef" for hash_len in range(4, 50): res = kdf.digest(secret_data, kdf_params, hash_len) assert len(res) == hash_len
def test_argon2_fuzz(): # Compare two implementations. Ostensibly they both use # the same implementation underneath, so there should # be absolutely no difference try: importlib.import_module('pyargon2') except ImportError: return r = random.Random(0) for _ in range(10): p = r.randrange(1, 4) m = r.randrange(1, 100) t = r.randrange(1, 4) kdf_params = kdf.init_kdf_params(p=p, m=m, t=t) test_data = str(r.random()) * 2 hash_data_1 = kdf._hash_pyargon2( test_data, t=kdf_params.t, p=kdf_params.p, m=kdf_params.m, ) hash_data_2 = kdf._hash_argon2_cffi( test_data.encode("utf-8"), t=kdf_params.t, p=kdf_params.p, m=kdf_params.m, ) assert hash_data_1 == hash_data_2, kdf_params
def estimate_param_cost(tgt_kdf_params: kdf.KDFParams, sys_info: typ.Optional[SystemInfo] = None) -> Seconds: """Estimate the runtime for parameters in seconds. This extrapolates based on a few short measurements and is not very precise (but good enough for a progress bar). """ tgt_p, tgt_m, tgt_t, _ = tgt_kdf_params if tgt_m < 10 and tgt_t < 10: return 1.0 if sys_info is None: _sys_info = load_sys_info() if len(_sys_info.measurements) < 4: _sys_info = update_measurements(_sys_info) else: _sys_info = sys_info assert len(_sys_info.measurements) >= 4 measurements = _sys_info.measurements min_measurements: typ.Dict[kdf.KDFParams, float] = {} for measurement in measurements: key = kdf.init_kdf_params(measurement.m, measurement.t) if key in min_measurements: val = min_measurements[key] min_measurements[key] = min(measurement.duration, val) else: min_measurements[key] = measurement.duration measurements = [ Measurement(p, m, t, h, d) for (p, m, t, h), d in min_measurements.items() ] assert len(measurements) == 4 # Bilinear Interpolation # https://stackoverflow.com/a/8662355/62997 # https://en.wikipedia.org/wiki/Bilinear_interpolation#Algorithm m0, _, _, m1 = [m for p, m, t, h, d in measurements] t0, _, _, t1 = [t for p, m, t, h, d in measurements] d00, d01, d10, d11 = [d for p, m, t, h, d in measurements] s = [ d00 * (m1 - tgt_m) * (t1 - tgt_t), d10 * (tgt_m - m0) * (t1 - tgt_t), d01 * (m1 - tgt_m) * (tgt_t - t0), d11 * (tgt_m - m0) * (tgt_t - t0), ] return max(0.0, sum(s) / ((m1 - m0) * (t1 - t0) + 0.0))
def test_digest_progress(): increments = [] def _progress_cb(incr) -> None: increments.append(incr) kdf_params = kdf.init_kdf_params(p=1, m=10, t=100) secret_data = b"\x01\x23\x45\x67\x89\xab\xcd\xef" kdf.digest(secret_data, kdf_params, 8, _progress_cb) assert len(increments) > 0 assert increments[-1] == 100
def get_default_params() -> kdf.KDFParams: sys_info = load_sys_info() m = sys_info.initial_m t = 1 while True: test_kdf_params = kdf.init_kdf_params(m=m, t=t) est_cost = estimate_param_cost(test_kdf_params) if est_cost > DEFAULT_KDF_TIME_SEC: return test_kdf_params else: t = math.ceil(t * 1.5)
def test_digest_iters(): all_kdf_params = set() for p in range(1, 4): for t in range(1, 8): kdf_params = kdf.init_kdf_params(p=p, m=10, t=t) all_kdf_params.add(kdf_params) kdf_input = b"\x01\x23\x45\x67" * 4 digests = set() for kdf_params in all_kdf_params: res = kdf.digest(kdf_input, kdf_params, hash_len=32) digests.add(res) # digests should be unique for unique kdf_params assert len(digests) == len(all_kdf_params)
def test_digest_inputs(): kdf_params = kdf.init_kdf_params(p=1, m=random.randint(1, 4) * 8, t=random.randint(1, 20)) kdf_inputs = [ b"\x00\x00\x00\x00\x00\x00\x00\x00", b"\x11\x11\x11\x11\x11\x11\x11\x11", b"\x22\x22\x22\x22\x22\x22\x22\x22", b"\x33\x33\x33\x33\x33\x33\x33\x33", ] digests = set() for kdf_input in kdf_inputs: res = kdf.digest(kdf_input, kdf_params, hash_len=32) digests.add(res) # digests should be unique for unique inputs assert len(digests) == len(kdf_inputs)
def test_kdf_params_fuzz(): r = random.Random(0) for _ in range(100): p = r.randrange(1, int(kdf.P_BASE ** (2 ** 4 - 1))) * kdf.MIN_P m = r.randrange(1, int(kdf.M_BASE ** (2 ** 6 - 1))) * kdf.MIN_M t = r.randrange(1, int(kdf.T_BASE ** (2 ** 6 - 1))) * kdf.MIN_T kdf_params = kdf.init_kdf_params(p=p, m=m, t=t) decoded = kdf.KDFParams.decode(kdf_params.encode()) assert decoded == kdf_params dp = abs(kdf_params.p - p) / kdf_params.p dm = abs(kdf_params.m - m) / kdf_params.m dt = abs(kdf_params.t - t) / kdf_params.t assert dp <= 0.5 assert dm <= 0.5 assert dt <= 0.5
def _update_measurements(sys_info: SystemInfo) -> SystemInfo: # NOTE: choice of the baseline memory probably has the # largest influence on the accuracy of cost estimation # for parameters. Presumably you'd want to do something # more clever than a cutoff. We might for example look # to see if curve of the durations is past some inflection # point that is presumably related to a bottleneck. m = 1 while True: kdf_params = kdf.init_kdf_params(m=m, t=2) p = kdf_params.p m = kdf_params.m sample = measure(kdf_params) if sample.duration > KDF_MEASUREMENT_SIGNIFIGANCE_THRESHOLD: break else: m = math.ceil(m * 1.5) _dump_sys_info(sys_info) return sys_info
def test_kdf_params(p, m, t): kdf_params = kdf.init_kdf_params(p=p, m=m, t=t) assert kdf_params.p == p assert kdf_params.m == m assert kdf_params.t == t
while remaining_iters > 0: step_iters = max(1, round(remaining_iters / remaining_steps)) print(f"remaining: {remaining_iters:>3} of {t} - next: {step_iters}") result = argon2.low_level.hash_secret_raw( secret=result, salt=result, time_cost=step_iters, **constant_kwargs ) print("<<", len(result)) remaining_steps -= 1 remaining_iters -= step_iters assert remaining_steps == 0, remaining_steps assert remaining_iters == 0, remaining_iters return result[:digest_len] kdf_params = kdf.init_kdf_params(m=1, t=1) digest_data = digest(b"test1234", p=kdf_params.p, m=kdf_params.m, t=kdf_params.t) if VERIFY: assert digest_data == kdf.digest(b"test1234", kdf_params, hash_len=1024) else: print(binascii.hexlify(digest_data)) # remaining: 1 of 1 - next: 1 # b'f874b69ca85a76f373a203e7d55a2974c3dc50d94886383b8502aaeebaaf362d' # This can be verified against https://antelle.net/argon2-browser/ # # Params: pass=test1234, salt=test1234, time=1, mem=1024, hashLen=32, parallelism=1, type=0 # Encoded: $argon2d$v=19$m=1024,t=1,p=1$dGVzdDEyMzQ$O2GpxMquN/amTCVwe5GHPJr89BvBVnM0ylSHfzez4l8 kdf_params = kdf.init_kdf_params(m=1, t=2)