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, "" }