Files
satusehat-service/pkg/crypto/rsa.go
T

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)
}