Files
web-giao-ban-go/license/license.go
T

279 lines
8.4 KiB
Go

package license
import (
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"time"
"web-giao-ban-go/config"
"web-giao-ban-go/logger"
)
type LicenseInfo struct {
AtRow int `json:"AtRow,omitempty"`
Mabvbh string `json:"mabvbh"`
DateExpires string `json:"DateExpires"` // YYYY-MM-DD
Key string `json:"key"` // e.g. "web_giao_ban"
Title string `json:"title"`
TenBV string `json:"TenBV"`
DoiTac string `json:"Đối tác,omitempty"`
GhiChu string `json:"Ghi chú,omitempty"`
UseHddtBy string `json:"UseHddtBy,omitempty"`
Trial string `json:"Trial,omitempty"`
NgayHetHan string `json:"Ngày hết hạn,omitempty"`
// Evaluation fields
IsValid bool `json:"is_valid"`
RemainingDays int `json:"remaining_days"`
StatusMessage string `json:"status_message"`
RawEncrypted string `json:"raw_encrypted,omitempty"`
}
var (
licenseMu sync.RWMutex
currentLicense *LicenseInfo
)
// DeriveKeyAndIV implements OpenSSL's EVP_BytesToKey with MD5 for AES-256-CBC
func DeriveKeyAndIV(passphrase []byte, salt []byte, keyLen int, ivLen int) ([]byte, []byte) {
totalLen := keyLen + ivLen
var concatenatedHashes []byte
var currentHash []byte
for len(concatenatedHashes) < totalLen {
hasher := md5.New()
if len(currentHash) > 0 {
hasher.Write(currentHash)
}
hasher.Write(passphrase)
hasher.Write(salt)
currentHash = hasher.Sum(nil)
concatenatedHashes = append(concatenatedHashes, currentHash...)
}
key := concatenatedHashes[:keyLen]
iv := concatenatedHashes[keyLen : keyLen+ivLen]
return key, iv
}
// OpenSSLDecrypt decrypts a Base64 string created by CryptoJS / OpenSSL
func OpenSSLDecrypt(encryptedBase64 string, passphrase string) (string, error) {
clean := strings.TrimSpace(encryptedBase64)
clean = strings.ReplaceAll(clean, "\r", "")
clean = strings.ReplaceAll(clean, "\n", "")
clean = strings.ReplaceAll(clean, " ", "")
data, err := base64.StdEncoding.DecodeString(clean)
if err != nil {
return "", fmt.Errorf("lỗi giải mã base64: %v", err)
}
if len(data) < 16 || string(data[:8]) != "Salted__" {
return "", errors.New("định dạng key không đúng (thiếu header OpenSSL Salted__)")
}
salt := data[8:16]
ciphertext := data[16:]
key, iv := DeriveKeyAndIV([]byte(passphrase), salt, 32, 16)
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
if len(ciphertext)%aes.BlockSize != 0 {
return "", errors.New("kích thước dữ liệu mã hóa không hợp lệ (không phải bội số 16)")
}
mode := cipher.NewCBCDecrypter(block, iv)
plaintext := make([]byte, len(ciphertext))
mode.CryptBlocks(plaintext, ciphertext)
// PKCS7 unpadding
if len(plaintext) == 0 {
return "", errors.New("dữ liệu giải mã rỗng")
}
padLen := int(plaintext[len(plaintext)-1])
if padLen < 1 || padLen > aes.BlockSize || padLen > len(plaintext) {
return "", errors.New("lỗi padding - mật khẩu giải mã không đúng hoặc chuỗi key bị sai")
}
for i := len(plaintext) - padLen; i < len(plaintext); i++ {
if plaintext[i] != byte(padLen) {
return "", errors.New("lỗi padding bytes - mật khẩu giải mã không đúng hoặc chuỗi key bị sai")
}
}
return string(plaintext[:len(plaintext)-padLen]), nil
}
// internalSecretKey returns the internal private passphrase (DHCryptoJSKEY)
func internalSecretKey() string {
part1 := []byte{0x42, 0x41, 0x44, 0x35, 0x41, 0x30, 0x35, 0x46}
part2 := []byte{0x33, 0x33, 0x30, 0x44, 0x35, 0x42, 0x38, 0x43}
part3 := []byte{0x44, 0x44, 0x46, 0x43, 0x41, 0x39, 0x45, 0x37}
part4 := []byte{0x45, 0x43, 0x44, 0x43, 0x38, 0x34, 0x30, 0x42}
return string(append(append(append(part1, part2...), part3...), part4...))
}
// ValidateAndParseLicense parses and evaluates the license ciphertext
func ValidateAndParseLicense(encryptedKey string, cryptoKey string) (*LicenseInfo, error) {
if strings.TrimSpace(encryptedKey) == "" {
return nil, errors.New("chuỗi key bản quyền trống")
}
if strings.TrimSpace(cryptoKey) == "" {
cryptoKey = internalSecretKey()
}
decryptedJSON, err := OpenSSLDecrypt(encryptedKey, cryptoKey)
if err != nil {
return nil, fmt.Errorf("không thể giải mã key bản quyền: %v", err)
}
var info LicenseInfo
if err := json.Unmarshal([]byte(decryptedJSON), &info); err != nil {
return nil, fmt.Errorf("dữ liệu JSON bản quyền không hợp lệ: %v", err)
}
// Verify required fields
if info.Key != "web_giao_ban" {
return nil, fmt.Errorf("key bản quyền không dùng cho chức năng 'web_giao_ban' (nhận diện: %s)", info.Key)
}
if info.DateExpires == "" {
return nil, errors.New("key bản quyền không có thông tin hạn dùng (DateExpires)")
}
// Parse expiration date (YYYY-MM-DD)
expireDate, err := time.Parse("2006-01-02", strings.TrimSpace(info.DateExpires))
if err != nil {
// try DD/MM/YYYY
expireDate, err = time.Parse("02/01/2006", strings.TrimSpace(info.DateExpires))
if err != nil {
return nil, fmt.Errorf("định dạng hạn dùng không hợp lệ (%s)", info.DateExpires)
}
}
// Compare with today (start of current day in local time)
now := time.Now()
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
expireDay := time.Date(expireDate.Year(), expireDate.Month(), expireDate.Day(), 23, 59, 59, 0, now.Location())
daysRemaining := int(expireDay.Sub(today).Hours() / 24)
info.RemainingDays = daysRemaining
info.RawEncrypted = encryptedKey
if expireDay.Before(today) {
info.IsValid = false
info.StatusMessage = fmt.Sprintf("Key bản quyền đã hết hạn vào ngày %s (quá hạn %d ngày)", info.DateExpires, -daysRemaining)
} else {
info.IsValid = true
info.StatusMessage = fmt.Sprintf("Key bản quyền hợp lệ đến ngày %s (còn lại %d ngày)", info.DateExpires, daysRemaining)
}
return &info, nil
}
// InitLicense loads the license from config and evaluates it
func InitLicense() {
licenseMu.Lock()
defer licenseMu.Unlock()
cfg := config.Get()
if cfg == nil {
return
}
if cfg.License.Key == "" {
currentLicense = &LicenseInfo{
IsValid: false,
StatusMessage: "Chưa cập nhật key bản quyền tính năng",
}
logger.Warn("Chưa cập nhật key bản quyền web giao ban")
return
}
info, err := ValidateAndParseLicense(cfg.License.Key, "")
if err != nil {
currentLicense = &LicenseInfo{
IsValid: false,
StatusMessage: fmt.Sprintf("Key bản quyền không hợp lệ: %v", err),
RawEncrypted: cfg.License.Key,
}
logger.Error("Kiểm tra key bản quyền thất bại: %v", err)
return
}
currentLicense = info
if info.IsValid {
logger.Info("Bản quyền hợp lệ: Đơn vị '%s' (%s) - Hạn dùng: %s (%d ngày)", info.TenBV, info.Mabvbh, info.DateExpires, info.RemainingDays)
} else {
logger.Warn("Bản quyền không hợp lệ: %s", info.StatusMessage)
}
}
// GetStatus returns the current license status
func GetStatus() *LicenseInfo {
licenseMu.RLock()
defer licenseMu.RUnlock()
if currentLicense == nil {
return &LicenseInfo{
IsValid: false,
StatusMessage: "Chưa nạp thông tin bản quyền",
}
}
return currentLicense
}
// Activate saves and applies a new license key
func Activate(encryptedKey string) (*LicenseInfo, error) {
cfg := config.Get()
if cfg == nil {
return nil, errors.New("cấu hình chưa được khởi tạo")
}
info, err := ValidateAndParseLicense(encryptedKey, "")
if err != nil {
return nil, err
}
if !info.IsValid {
return nil, errors.New(info.StatusMessage)
}
// Update config
licenseMu.Lock()
currentLicense = info
licenseMu.Unlock()
cfg.License.Key = strings.TrimSpace(encryptedKey)
if err := config.SaveConfig(cfg); err != nil {
return nil, fmt.Errorf("lưu cấu hình thất bại: %v", err)
}
logger.Info("Kích hoạt bản quyền thành công cho đơn vị: %s (%s), hạn dùng: %s", info.TenBV, info.Mabvbh, info.DateExpires)
return info, nil
}
// CheckValid verifies if license is currently valid
func CheckValid() (bool, string) {
licenseMu.RLock()
defer licenseMu.RUnlock()
if currentLicense == nil || !currentLicense.IsValid {
msg := "Chưa cập nhật key bản quyền hoặc key đã hết hạn sử dụng. Vui lòng liên hệ bộ phận hỗ trợ kỹ thuật."
if currentLicense != nil && currentLicense.StatusMessage != "" {
msg = currentLicense.StatusMessage
}
return false, msg
}
return true, ""
}