package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"strings"
	"time"
)

// kunciDemo dibuat tetap agar output contoh deterministik; produksi memuat
// kunci acak dari environment variable dan tidak menuliskannya di source code.
const kunciDemo = "kunci-demo-tanda-tangan"

// waktuSekarang dibekukan agar contoh deterministik; produksi memakai time.Now().
var waktuSekarang = time.Date(2026, 1, 15, 8, 0, 0, 0, time.UTC)

// klaim berisi data yang dibawa token. Hanya klaim standar yang dipakai di sini.
type klaim struct {
	Sub string `json:"sub"`
	Iat int64  `json:"iat"`
	Exp int64  `json:"exp"`
}

// tandatangan menghitung HMAC-SHA256 lalu menyandikannya ke base64 URL.
func tandatangan(data string) string {
	mac := hmac.New(sha256.New, []byte(kunciDemo))
	mac.Write([]byte(data))
	return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
}

// buatToken merakit header.payload.signature seperti struktur JWT.
func buatToken(sub string, iat, exp int64) (string, error) {
	header := `{"alg":"HS256","typ":"JWT"}`
	payload, err := json.Marshal(klaim{Sub: sub, Iat: iat, Exp: exp})
	if err != nil {
		return "", err
	}
	h := base64.RawURLEncoding.EncodeToString([]byte(header))
	p := base64.RawURLEncoding.EncodeToString(payload)
	return h + "." + p + "." + tandatangan(h+"."+p), nil
}

// verifikasiToken memeriksa signature lalu kedaluwarsa token.
func verifikasiToken(token string) (klaim, error) {
	bagian := strings.Split(token, ".")
	if len(bagian) != 3 {
		return klaim{}, errors.New("token harus terdiri dari tiga bagian")
	}
	sigHitung, err := base64.RawURLEncoding.DecodeString(tandatangan(bagian[0] + "." + bagian[1]))
	if err != nil {
		return klaim{}, errors.New("signature tidak valid")
	}
	sigAsli, err := base64.RawURLEncoding.DecodeString(bagian[2])
	if err != nil {
		return klaim{}, errors.New("signature tidak valid")
	}
	// hmac.Equal membandingkan byte demi byte dengan waktu konstan.
	if !hmac.Equal(sigAsli, sigHitung) {
		return klaim{}, errors.New("signature tidak cocok")
	}
	isi, err := base64.RawURLEncoding.DecodeString(bagian[1])
	if err != nil {
		return klaim{}, errors.New("payload tidak valid")
	}
	var k klaim
	if err := json.Unmarshal(isi, &k); err != nil {
		return klaim{}, errors.New("payload bukan JSON valid")
	}
	if waktuSekarang.Unix() >= k.Exp {
		return klaim{}, errors.New("token sudah kedaluwarsa")
	}
	return k, nil
}

func main() {
	// Token sah untuk budi, berlaku satu jam sejak waktu beku.
	token, err := buatToken("budi", waktuSekarang.Unix(), waktuSekarang.Add(time.Hour).Unix())
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("token       :", token)
	bagian := strings.Split(token, ".")
	isiPayload, err := base64.RawURLEncoding.DecodeString(bagian[1])
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("payload     :", string(isiPayload))

	k, err := verifikasiToken(token)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("token sah   : valid (sub:", k.Sub+")")

	// Pemalsuan: payload diganti menjadi sub=siti, signature lama dipakai ulang.
	payloadPalsu, _ := json.Marshal(klaim{Sub: "siti", Iat: waktuSekarang.Unix(), Exp: waktuSekarang.Add(time.Hour).Unix()})
	bagian[1] = base64.RawURLEncoding.EncodeToString(payloadPalsu)
	_, err = verifikasiToken(strings.Join(bagian, "."))
	fmt.Println("token palsu : tidak valid -", err)

	// Kedaluwarsa: token diterbitkan dua jam lalu dan sudah lewat masa berlaku.
	tokenLama, err := buatToken("budi", waktuSekarang.Add(-2*time.Hour).Unix(), waktuSekarang.Add(-time.Hour).Unix())
	if err != nil {
		log.Fatal(err)
	}
	_, err = verifikasiToken(tokenLama)
	fmt.Println("token lama  : tidak valid -", err)
}
