Skip to main content

aoc/year2020/
day25.rs

1//! # Combo Breaker
2//!
3//! The puzzle description for today describes the [Diffie-Hellman-Merkle] handshake, which your
4//! browser uses every day for encrypted traffic. It relies on the fact that if you use the
5//! product of two prime numbers as your encryption key, where each of the two parties in
6//! the transaction only knows one of the two prime numbers, then modular arithmetic allows
7//! both sides to efficiently compute the same results, while an eavesdropper would have
8//! exponentially more work to do to first factor the product into the original components. Here,
9//! we are playing the role of the eavesdropper, so we are doing exponential work. But since
10//! 2²⁵ is small, brute force is tolerable, when compared to more typical Diffie-Hellman parameters
11//! of 2⁵¹² or larger (these days, anything less than 2048 bits is insecure).
12//!
13//! The common encryption key is then calculated efficiently by [modular exponentiation] using
14//! [exponentiation by squaring].
15//!
16//! That said, we can still perform much faster than the potential of 20201227 steps needed for
17//! brute force, by using the [Baby-step giant-step algorithm]. This takes only √20201227 = 4495
18//! steps, similar to how listing all divisors of a number can stop after reaching the square
19//! root of that number, since all the larger divisors will pair with a smaller one.
20//!
21//! See also [this alternative patch](https://github.com/maneatingape/advent-of-code-rust/pull/88)
22//! that can further speed things up to less than one-tenth of the work by using the [Pohlig-Hellman]
23//! algorithm (yes, the same Hellman that described secure key exchange also described how to speed
24//! up the factoring of that key). However, the complexity required to exploit properties from
25//! [Fermat's Little Theorem] and the [Chinese Remainder Theorem] to reduce the work to √116099 = 341
26//! table entries (as one of the factors of `20201227-1`) is harder to maintain, when this
27//! solution is already fast enough.
28//!
29//! [Diffie-Hellman-Merkle](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange)
30//! [modular exponentiation](https://en.wikipedia.org/wiki/Modular_exponentiation)
31//! [exponentiation by squaring](https://en.wikipedia.org/wiki/Exponentiation_by_squaring)
32//! [Baby-step giant-step algorithm](https://en.wikipedia.org/wiki/Baby-step_giant-step)
33//! [Pohlig-Hellman](https://en.wikipedia.org/wiki/Pohlig-Hellman_algorithm)
34//! [Fermat's Little Theorem](https://en.wikipedia.org/wiki/Fermat%27s_little_theorem)
35//! [Chinese Remainder Theorem](https://en.wikipedia.org/wiki/Chinese_remainder_theorem)
36use crate::util::hash::*;
37use crate::util::iter::*;
38use crate::util::math::*;
39use crate::util::parse::*;
40
41const MOD: u64 = 20201227;
42
43pub fn parse(input: &str) -> [u64; 2] {
44    input.iter_unsigned().chunk::<2>().next().unwrap()
45}
46
47pub fn part1(input: &[u64; 2]) -> u64 {
48    let [card_public_key, door_public_key] = *input;
49    let card_loop_count = discrete_logarithm(card_public_key);
50    door_public_key.mod_pow(card_loop_count, MOD)
51}
52
53pub fn part2(_input: &[u64; 2]) -> &'static str {
54    "n/a"
55}
56
57/// Baby-step giant-step algorithm to compute discrete logarithm.
58/// Constants are hardcoded to this specific problem.
59/// * 4495 is the ceiling of √20201227
60/// * 680915 is 7⁻⁴⁴⁹⁵, or the
61///   [multiplicative modular inverse](https://en.wikipedia.org/wiki/Modular_multiplicative_inverse)
62///   of 7 to modular exponent 4495.
63fn discrete_logarithm(public_key: u64) -> u64 {
64    let m = 4495;
65    let mut map = FastMap::with_capacity(m as usize);
66
67    let mut a = 1;
68    for j in 0..m {
69        map.insert(a, j);
70        a = (a * 7) % MOD;
71    }
72
73    let mut b = public_key;
74    for i in 0..m {
75        if let Some(j) = map.get(&b) {
76            return i * m + j;
77        }
78        b = (b * 680915) % MOD;
79    }
80
81    unreachable!()
82}