Challenge

import random
from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes, inverse
import hashlib

bit_length = 128
p = getPrime(bit_length)
q = getPrime(bit_length)
r = getPrime(bit_length)

N = p * q * r

e1 = 3
e2 = 5

def nested_encrypt(message, e1, e2, N):
    msg_num = bytes_to_long(message)
    step1 = pow(msg_num, e1, N)
    salt = (p + q)  
    step2 = (step1 + salt) % N
    step3 = pow(step2, e2, N)
    mask = int(hashlib.sha256(long_to_bytes(step3)).hexdigest()[:8], 16) 
    ciphertext = step3 ^ mask
    return ciphertext

flag = b"csd{am_i_late_or_am_i_late}"
ciphertext = nested_encrypt(flag, e1, e2, N)
leak_p_bits = bin(p)[-64:]  
leak_q_bits = bin(q)[-64:]
partial_salt = (p + q) % 100000  

with open("public.txt", "w") as f:
    f.write(f"N = {N}\nLeaked bits of p = {leak_p_bits}\nLeaked bits of q = {leak_q_bits}\n")
    f.write(f"Partial salt (p + q mod 100000) = {partial_salt}\n")

with open("ciphertext.txt", "w") as f:
    f.write(str(ciphertext))

print("Try breaking me :)")


N = 20829189282001863372322428196733308195464709019397028562940874561583326274287129648306568901830962480022928679678123
a lil p = 1110100110011011101000100001101100001010111001100001011001101111
a lil q = 1011110111011111011101010111111010011010110011110011010100111111
salty= 18766
14148786803331853127777889559896138396417219981773502601578745985604370779076393473723769040986523787622227351205298


Solve

First brute the mask, it’s less than 32 bits.

from tqdm import trange
from Crypto.Util.number import *
from hashlib import sha256

ciphertext = 14148786803331853127777889559896138396417219981773502601578745985604370779076393473723769040986523787622227351205298

for mask in trange(2**32):
    step3 = mask ^ ciphertext
    if mask == int(sha256(long_to_bytes(step3)).hexdigest()[:8], 16):
        print(mask)

You can get a small speedup with rust:

use rug::ops::BitXorFrom;
use rug::Integer;
use sha256::digest;
use indicatif::ProgressBar;


fn int_to_bytes(mut n: Integer) -> Vec<u8> {
    let mut ret = vec![];
    while n > 0 {
        let x = n.mod_u(256) as u8;
        ret.push(x);
        n = n / 256;
    }
    ret.reverse();
    return ret;
}

fn main() {
    let ciphertext = Integer::from_str_radix("14148786803331853127777889559896138396417219981773502601578745985604370779076393473723769040986523787622227351205298", 10).unwrap();

    let max: u64 = 4294967296; // 2**32
    let bar = ProgressBar::new(max);
    for mask in 0..max {
        bar.inc(1);
        let mut step3 = Integer::from(mask);
        step3.bitxor_from(ciphertext.clone());
        let hash = &digest(int_to_bytes(step3))[..8];
        let mask_hex = format!("{:x}", mask);
        if hash == mask_hex {
            println!("mask = {}", hash);
        }
    }
}


There are multiple possibilities, I found these:

possible_masks = [3306955427, 2186477323, 2523079120]
for mask in possible_masks:
    step3 = mask ^ ciphertext
    print(mask == int(sha256(long_to_bytes(step3)).hexdigest()[:8], 16))


Factor n with bruteforce, the author confirmed the hints were actually just red herrings…

Final solver:

from tqdm import trange
from Crypto.Util.number import *
from hashlib import sha256

N = 20829189282001863372322428196733308195464709019397028562940874561583326274287129648306568901830962480022928679678123
p = 196193902291230366369929504455328667247
q = 330729152607130810754863800538364851519 
r = 321006911433478242108053772673636286011 
pari.addprimes([p, q, r])
assert N == p*q*r
salt = p + q
assert salt % 100_000 == 18766
ciphertext = 14148786803331853127777889559896138396417219981773502601578745985604370779076393473723769040986523787622227351205298

possible_masks = [3306955427, 2186477323, 2523079120]
for mask in possible_masks:
    step3 = mask ^^ ciphertext
    assert mask == int(sha256(long_to_bytes(step3)).hexdigest()[:8], 16)
    for step2 in Zmod(N)(step3).nth_root(5, all=True):
        step1 = (step2 - salt) % N
        for m in Zmod(N)(step1).nth_root(3, all=True):
            flag = long_to_bytes(int(m))
            if b'csd' in flag:
                print(flag)
# csd{dH4R_dh4r_4Nt1_P1R4cY_5CR33n}