import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.Map;

public class HashKataSandi {

    static final int ITERASI = 120_000;
    static final int PANJANG_KUNCI_BIT = 256;

    public static void main(String[] args) throws Exception {
        System.out.println("== 1. Penyimpanan polos (dilarang di produksi) ==");
        Map<String, String> tabelPolos = new LinkedHashMap<>();
        tabelPolos.put("budi", "sandiSama123");
        tabelPolos.put("siti", "sandiSama123");
        for (Map.Entry<String, String> e : tabelPolos.entrySet()) {
            System.out.println("tabel: " + e.getKey() + " -> " + e.getValue());
        }
        System.out.println("sandi terbaca langsung; satu kebocoran membongkar semua akun");
        System.out.println();

        System.out.println("== 2. SHA-256 tanpa salt (masih tidak memadai) ==");
        String d1 = hex(sha256("sandiSama123"));
        String d2 = hex(sha256("sandiSama123"));
        String d3 = hex(sha256("sandiBaru456"));
        System.out.println("sandiSama123 -> " + d1);
        System.out.println("sandiSama123 -> " + d2);
        System.out.println("sandiBaru456 -> " + d3);
        System.out.println("dua sandi sama menghasilkan hash sama: " + d1.equals(d2));
        System.out.println("hash siap dipecahkan tabel prahitung (rainbow table)");
        System.out.println();

        System.out.println("== 3. PBKDF2 dengan salt per pengguna ==");
        String simpanBudi = simpan("budi", "sandiSama123");
        String simpanSiti = simpan("siti", "sandiSama123");
        System.out.println("budi: " + simpanBudi);
        System.out.println("siti: " + simpanSiti);
        String hashBudi = potong(simpanBudi);
        String hashSiti = potong(simpanSiti);
        System.out.println("sandi sama, hash tetap beda : " + !hashBudi.equals(hashSiti));
        System.out.println("biaya per tebakan           : " + ITERASI
                + " putaran HMAC-SHA256");
        System.out.println();

        System.out.println("== 4. Verifikasi tanpa pernah membaca sandi polos ==");
        cek("budi", "sandiSama123", simpanBudi);
        cek("budi", "tebakanSalah", simpanBudi);
        cek("siti", "sandiSama123", simpanSiti);
    }

    // ---- Fungsi kunci ----

    // Hash cepat: cocok untuk checksum berkas, TIDAK untuk kata sandi.
    static byte[] sha256(String teks) {
        try {
            return MessageDigest.getInstance("SHA-256")
                    .digest(teks.getBytes(StandardCharsets.UTF_8));
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 wajib tersedia di JVM", e);
        }
    }

    // Salt deterministik per pengguna agar contoh ini dapat diuji ulang.
    // Di produksi: SecureRandom.getInstanceStrong() saat pendaftaran.
    static byte[] saltUntuk(String pengguna) {
        return sha256("salt$" + pengguna);
    }

    // PBKDF2: sandi + salt digiling ITERASI kali menjadi kunci PANJANG_KUNCI_BIT.
    static byte[] pbkdf2(String sandi, byte[] salt) throws Exception {
        PBEKeySpec spec = new PBEKeySpec(sandi.toCharArray(), salt,
                ITERASI, PANJANG_KUNCI_BIT);
        try {
            return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
                    .generateSecret(spec).getEncoded();
        } finally {
            spec.clearPassword(); // bersihkan salinan sandi di memori
        }
    }

    // Format penyimpanan mandiri: algoritma:iterasi:salt:hash.
    // Mirip awalan {bcrypt} milik DelegatingPasswordEncoder.
    static String simpan(String pengguna, String sandi) throws Exception {
        byte[] salt = saltUntuk(pengguna);
        byte[] hash = pbkdf2(sandi, salt);
        return "pbkdf2:" + ITERASI + ":" + hex(salt) + ":" + hex(hash);
    }

    static void cek(String pengguna, String sandi, String tersimpan) throws Exception {
        String[] bagian = tersimpan.split(":", 4);
        int iterasi = Integer.parseInt(bagian[1]);
        byte[] salt = unhex(bagian[2]);
        byte[] tersimpanHash = unhex(bagian[3]);

        PBEKeySpec spec = new PBEKeySpec(sandi.toCharArray(), salt,
                iterasi, tersimpanHash.length * 8);
        byte[] hitungUlang;
        try {
            hitungUlang = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
                    .generateSecret(spec).getEncoded();
        } finally {
            spec.clearPassword();
        }
        // MessageDigest.isEqual membandingkan konstanta-waktu, bukan equals().
        boolean cocok = MessageDigest.isEqual(hitungUlang, tersimpanHash);
        System.out.printf("%-6s sandi=%-14s -> %s%n", pengguna, sandi,
                cocok ? "COCOK" : "TIDAK COCOK");
    }

    static String potong(String tersimpan) {
        return tersimpan.split(":", 4)[3];
    }

    static String hex(byte[] b) {
        return HexFormat.of().formatHex(b);
    }

    static byte[] unhex(String s) {
        return HexFormat.of().parseHex(s);
    }
}
