276 lines
8.1 KiB
Go
276 lines
8.1 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
|
|
}
|
|
|
|
// 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 = "BAD5A05F330D5B8CDDFCA9E7ECDC840B"
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
cryptoKey := cfg.DecryptedCryptoKey
|
|
if cryptoKey == "" {
|
|
cryptoKey = "BAD5A05F330D5B8CDDFCA9E7ECDC840B"
|
|
}
|
|
|
|
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, cryptoKey)
|
|
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()
|
|
cryptoKey := cfg.DecryptedCryptoKey
|
|
if cryptoKey == "" {
|
|
cryptoKey = "BAD5A05F330D5B8CDDFCA9E7ECDC840B"
|
|
}
|
|
|
|
info, err := ValidateAndParseLicense(encryptedKey, cryptoKey)
|
|
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, ""
|
|
}
|