161 lines
5.0 KiB
Go
161 lines
5.0 KiB
Go
package crypto
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
)
|
|
|
|
// GenerateRSAKeyPair menghasilkan private dan public key RSA dalam format string PEM.
|
|
// Umumnya Satu Sehat mewajibkan RSA dengan panjang minimal 2048 bit.
|
|
func GenerateRSAKeyPair(bits int) (privateKeyPEM string, publicKeyPEM string, err error) {
|
|
// 1. Generate RSA Private Key
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, bits)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("gagal mengenerate private key: %w", err)
|
|
}
|
|
|
|
// 2. Encode Private Key ke format PEM (PKCS#1)
|
|
privBytes := x509.MarshalPKCS1PrivateKey(privateKey)
|
|
privBlock := &pem.Block{
|
|
Type: "RSA PRIVATE KEY",
|
|
Bytes: privBytes,
|
|
}
|
|
privateKeyPEM = string(pem.EncodeToMemory(privBlock))
|
|
|
|
// 3. Extract dan Encode Public Key ke format PEM (SPKI)
|
|
pubBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("gagal marshal public key: %w", err)
|
|
}
|
|
|
|
pubBlock := &pem.Block{
|
|
Type: "PUBLIC KEY",
|
|
Bytes: pubBytes,
|
|
}
|
|
// Output standard PEM dari Go (pem.EncodeToMemory) sudah identik dengan openssl_pkey_get_details di PHP.
|
|
publicKeyPEM = string(pem.EncodeToMemory(pubBlock))
|
|
|
|
return privateKeyPEM, publicKeyPEM, nil
|
|
}
|
|
|
|
// Public Key resmi Kemenkes (Production / Staging) untuk mengenkripsi kunci AES.
|
|
const KemkesPublicKeyPEM = `-----BEGIN PUBLIC KEY-----
|
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxLwvebfOrPLIODIxAwFp
|
|
4Qhksdtn7bEby5OhkQNLTdClGAbTe2tOO5Tiib9pcdruKxTodo481iGXTHR5033I
|
|
A5X55PegFeoY95NH5Noj6UUhyTFfRuwnhtGJgv9buTeBa4pLgHakfebqzKXr0Lce
|
|
/Ff1MnmQAdJTlvpOdVWJggsb26fD3cXyxQsbgtQYntmek2qvex/gPM9Nqa5qYrXx
|
|
8KuGuqHIFQa5t7UUH8WcxlLVRHWOtEQ3+Y6TQr8sIpSVszfhpjh9+Cag1EgaMzk+
|
|
HhAxMtXZgpyHffGHmPJ9eXbBO008tUzrE88fcuJ5pMF0LATO6ayXTKgZVU0WO/4e
|
|
iQIDAQAB
|
|
-----END PUBLIC KEY-----`
|
|
|
|
const (
|
|
beginEncryptedMsg = "-----BEGIN ENCRYPTED MESSAGE-----\r\n"
|
|
endEncryptedMsg = "-----END ENCRYPTED MESSAGE-----"
|
|
)
|
|
|
|
// EncryptSatuSehatPayload mengenkripsi JSON payload sesuai standar SatuSehat (AES-256-GCM & RSA-OAEP).
|
|
func EncryptSatuSehatPayload(message []byte) (string, error) {
|
|
block, _ := pem.Decode([]byte(KemkesPublicKeyPEM))
|
|
if block == nil {
|
|
return "", errors.New("gagal mem-parsing Kemenkes Public Key")
|
|
}
|
|
pubKeyAny, err := x509.ParsePKIXPublicKey(block.Bytes)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
kemkesPubKey := pubKeyAny.(*rsa.PublicKey)
|
|
|
|
// 1. Generate 32 bytes AES Symmetric Key
|
|
aesKey := make([]byte, 32)
|
|
if _, err := io.ReadFull(rand.Reader, aesKey); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// 2. Encrypt AES Key menggunakan Kemenkes Public Key (RSA-OAEP)
|
|
wrappedAesKey, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, kemkesPubKey, aesKey, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// 3. Encrypt payload menggunakan AES-256-GCM
|
|
blockCipher, err := aes.NewCipher(aesKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
aesgcm, err := cipher.NewGCM(blockCipher)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
nonce := make([]byte, 12) // 12 bytes IV / Nonce
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Seal menggabungkan Ciphertext + Authentication Tag (16 bytes)
|
|
ciphertextWithTag := aesgcm.Seal(nil, nonce, message, nil)
|
|
|
|
// 4. Concat: WrappedKey(256) + IV(12) + CipherText + Tag(16)
|
|
encryptedData := append(nonce, ciphertextWithTag...)
|
|
payload := append(wrappedAesKey, encryptedData...)
|
|
|
|
// 5. Base64 Encode & Chunk Split (76 characters)
|
|
b64 := base64.StdEncoding.EncodeToString(payload)
|
|
var chunked strings.Builder
|
|
for i := 0; i < len(b64); i += 76 {
|
|
end := i + 76
|
|
if end > len(b64) {
|
|
end = len(b64)
|
|
}
|
|
chunked.WriteString(b64[i:end])
|
|
chunked.WriteString("\r\n")
|
|
}
|
|
|
|
return beginEncryptedMsg + chunked.String() + endEncryptedMsg, nil
|
|
}
|
|
|
|
// DecryptSatuSehatPayload mendekripsi response Kemenkes yang telah dienkripsi.
|
|
func DecryptSatuSehatPayload(encryptedStr string, privPEM string) ([]byte, error) {
|
|
startIdx := strings.Index(encryptedStr, "-----BEGIN ENCRYPTED MESSAGE-----")
|
|
endIdx := strings.Index(encryptedStr, "-----END ENCRYPTED MESSAGE-----")
|
|
if startIdx == -1 || endIdx == -1 {
|
|
return nil, errors.New("respons tidak memiliki tag ENCRYPTED MESSAGE")
|
|
}
|
|
|
|
b64Data := encryptedStr[startIdx+len("-----BEGIN ENCRYPTED MESSAGE-----") : endIdx]
|
|
b64Data = strings.ReplaceAll(strings.ReplaceAll(b64Data, "\r", ""), "\n", "")
|
|
|
|
binaryData, err := base64.StdEncoding.DecodeString(strings.TrimSpace(b64Data))
|
|
if err != nil || len(binaryData) < 256+12 {
|
|
return nil, errors.New("gagal me-decode base64 payload / panjang payload tidak valid")
|
|
}
|
|
|
|
wrappedKey := binaryData[:256]
|
|
iv := binaryData[256 : 256+12]
|
|
ciphertextWithTag := binaryData[256+12:]
|
|
|
|
block, _ := pem.Decode([]byte(privPEM))
|
|
privKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes)
|
|
|
|
aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privKey, wrappedKey, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gagal me-unwrap kunci AES: %w", err)
|
|
}
|
|
|
|
blockCipher, _ := aes.NewCipher(aesKey)
|
|
aesgcm, _ := cipher.NewGCM(blockCipher)
|
|
|
|
return aesgcm.Open(nil, iv, ciphertextWithTag, nil)
|
|
}
|