package service import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" "strconv" "strings" "time" "infinite-canvas/backend/internal/model" "gorm.io/gorm" ) const ossSettingKey = "oss" const encryptedSettingPrefix = "enc:v1:" const defaultOSSPathPrefix = "open-ai-canvas" const ( aliyunOSSProvider = "aliyun" tencentCOSProvider = "tencent" qiniuKodoProvider = "qiniu" s3Provider = "s3" ) type OSSSettingRequest struct { Enabled bool `json:"enabled"` Provider string `json:"provider"` Region string `json:"region"` Endpoint string `json:"endpoint"` CDNBaseURL string `json:"cdnBaseUrl"` Bucket string `json:"bucket"` AccessKeyID string `json:"accessKeyId"` AccessKeySecret string `json:"accessKeySecret"` PublicBaseURL string `json:"publicBaseUrl"` PathPrefix string `json:"pathPrefix"` S3Preset string `json:"s3Preset"` PathStyle bool `json:"pathStyle"` SessionToken string `json:"sessionToken"` AllowUserS3 bool `json:"allowUserS3"` } type PublicOSSSetting struct { Enabled bool `json:"enabled"` Provider string `json:"provider"` Region string `json:"region"` Endpoint string `json:"endpoint"` CDNBaseURL string `json:"cdnBaseUrl"` Bucket string `json:"bucket"` AccessKeyID string `json:"accessKeyId"` HasAccessKeySecret bool `json:"hasAccessKeySecret"` PublicBaseURL string `json:"publicBaseUrl"` PathPrefix string `json:"pathPrefix"` S3Preset string `json:"s3Preset"` PathStyle bool `json:"pathStyle"` HasSessionToken bool `json:"hasSessionToken"` StorageLocationID string `json:"storageLocationId"` TestedAt *time.Time `json:"testedAt,omitempty"` TestedDigest string `json:"testedDigest,omitempty"` HistoryCount int64 `json:"historyCount"` ReferencedResourceCount int64 `json:"referencedResourceCount"` AllowUserS3 bool `json:"allowUserS3"` UpdatedBy string `json:"updatedBy"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type ossSettingValue struct { Enabled bool `json:"enabled"` Provider string `json:"provider"` Region string `json:"region"` Endpoint string `json:"endpoint"` CDNBaseURL string `json:"cdnBaseUrl"` Bucket string `json:"bucket"` AccessKeyID string `json:"accessKeyId"` AccessKeySecret string `json:"accessKeySecret"` PublicBaseURL string `json:"publicBaseUrl"` PathPrefix string `json:"pathPrefix"` S3Preset string `json:"s3Preset"` PathStyle bool `json:"pathStyle"` SessionToken string `json:"sessionToken"` StorageLocationID string `json:"storageLocationId"` AllowUserS3 bool `json:"allowUserS3"` // 平台切换云厂商后仍需读取历史资源,因此仅归档非当前厂商的访问密钥。 ArchivedCredentials map[string]ossProviderCredentials `json:"archivedCredentials,omitempty"` } type ossProviderCredentials struct { AccessKeyID string `json:"accessKeyId"` AccessKeySecret string `json:"accessKeySecret"` } func (s *Service) AdminOSSSetting(actor *model.User) (*PublicOSSSetting, error) { if err := s.RequireAdmin(actor); err != nil { return nil, err } setting, value, err := s.readOSSSetting() if err != nil { return nil, err } public, err := s.publicOSSSetting(setting, value, "platform", "") if err != nil { return nil, err } return &public, nil } func (s *Service) UpdateOSSSetting(actor *model.User, req OSSSettingRequest) (*PublicOSSSetting, error) { if err := s.RequireAdmin(actor); err != nil { return nil, err } currentSetting, currentValue, err := s.readOSSSetting() if err != nil { return nil, err } next, err := ossSettingFromRequest(req, currentValue) if err != nil { return nil, err } if !next.Enabled { if next.PublicBaseURL == "" { return nil, BadAuthRequest("服务器本地存储需要填写服务器访问地址") } if _, err := validatePublicResourceBaseURL(next.PublicBaseURL); err != nil { return nil, fmt.Errorf("服务器访问地址无效:%w", err) } } next = archiveOSSProviderCredentials(next, currentValue) if next.Provider == s3Provider && next.Enabled { location, err := s.requireTestedS3Location("platform", "", next) if err != nil { return nil, err } next.StorageLocationID = location.ID } else if next.Provider == s3Provider && storageLocationDigest(next) == storageLocationDigest(currentValue) { next.StorageLocationID = currentValue.StorageLocationID } stored, err := s.encryptOSSSettingSecrets(next) if err != nil { return nil, err } valueJSON, err := json.Marshal(stored) if err != nil { return nil, err } setting := model.SystemSetting{ Key: ossSettingKey, ValueJSON: string(valueJSON), UpdatedBy: actor.ID, } if currentSetting != nil { setting.CreatedAt = currentSetting.CreatedAt } if err := s.repo.SaveSystemSetting(&setting); err != nil { return nil, err } if next.Provider == s3Provider && next.StorageLocationID != "" { if err := s.repo.ActivateStorageLocation("platform", "", next.StorageLocationID, next.Enabled); err != nil { return nil, err } } else if currentValue.Provider == s3Provider && currentValue.StorageLocationID != "" { if err := s.repo.ActivateStorageLocation("platform", "", "", false); err != nil { return nil, err } } public, err := s.publicOSSSetting(&setting, next, "platform", "") if err != nil { return nil, err } return &public, nil } func (s *Service) UserOSSSetting(actor *model.User) (*PublicOSSSetting, error) { if actor == nil { return nil, Unauthorized("请先登录") } setting, value, err := s.readUserOSSSetting(actor.ID) if err != nil { return nil, err } _, platform, err := s.readOSSSetting() if err != nil { return nil, err } public, err := s.publicUserOSSSetting(setting, value, actor.ID, platform.AllowUserS3) if err != nil { return nil, err } return &public, nil } func (s *Service) UpdateUserOSSSetting(actor *model.User, req OSSSettingRequest) (*PublicOSSSetting, error) { if actor == nil { return nil, Unauthorized("请先登录") } _, currentValue, err := s.readUserOSSSetting(actor.ID) if err != nil { return nil, err } next, err := ossSettingFromRequest(req, currentValue) if err != nil { return nil, err } _, platform, err := s.readOSSSetting() if err != nil { return nil, err } if next.Enabled && next.Provider == s3Provider && !platform.AllowUserS3 { return nil, Forbidden("平台管理员尚未允许个人 S3 兼容存储") } if next.Provider == s3Provider && next.Enabled { location, err := s.requireTestedS3Location("user", actor.ID, next) if err != nil { return nil, err } next.StorageLocationID = location.ID } else if next.Provider == s3Provider && storageLocationDigest(next) == storageLocationDigest(currentValue) { next.StorageLocationID = currentValue.StorageLocationID } stored, err := s.encryptOSSSettingSecrets(next) if err != nil { return nil, err } valueJSON, err := json.Marshal(stored) if err != nil { return nil, err } // 配置按版本追加而不是覆盖,资源会固定引用创建时的版本。 setting := model.UserOSSSetting{ID: newID(), UserID: actor.ID, Enabled: next.Enabled, ValueJSON: string(valueJSON)} if err := s.repo.CreateUserOSSSetting(&setting); err != nil { return nil, err } if next.Provider == s3Provider && next.StorageLocationID != "" { if err := s.repo.ActivateStorageLocation("user", actor.ID, next.StorageLocationID, next.Enabled); err != nil { return nil, err } } else if currentValue.Provider == s3Provider && currentValue.StorageLocationID != "" { if err := s.repo.ActivateStorageLocation("user", actor.ID, "", false); err != nil { return nil, err } } public, err := s.publicUserOSSSetting(&setting, next, actor.ID, platform.AllowUserS3) if err != nil { return nil, err } return &public, nil } func (s *Service) readOSSSetting() (*model.SystemSetting, ossSettingValue, error) { setting, err := s.repo.SystemSetting(ossSettingKey) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, normalizeOSSSetting(defaultOSSSetting()), nil } if err != nil { return nil, ossSettingValue{}, err } value := defaultOSSSetting() if strings.TrimSpace(setting.ValueJSON) != "" { if err := json.Unmarshal([]byte(setting.ValueJSON), &value); err != nil { return nil, ossSettingValue{}, errors.New("平台 OSS 配置格式无效") } } needsMigration, err := s.decryptOSSSettingSecrets(&value) if err != nil { return nil, ossSettingValue{}, err } if needsMigration { migrated, err := s.encryptOSSSettingSecrets(value) if err != nil { return nil, ossSettingValue{}, err } encoded, err := json.Marshal(migrated) if err != nil { return nil, ossSettingValue{}, err } setting.ValueJSON = string(encoded) if err := s.repo.SaveSystemSetting(setting); err != nil { return nil, ossSettingValue{}, err } } return setting, normalizeOSSSetting(value), nil } func (s *Service) readUserOSSSetting(userID string) (*model.UserOSSSetting, ossSettingValue, error) { setting, err := s.repo.LatestUserOSSSetting(userID) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, normalizeOSSSetting(defaultOSSSetting()), nil } if err != nil { return nil, ossSettingValue{}, err } value, err := s.userOSSSettingValue(setting) return setting, value, err } func (s *Service) readUserOSSSettingByID(userID string, id string) (*model.UserOSSSetting, ossSettingValue, error) { setting, err := s.repo.UserOSSSettingForUser(userID, id) if err != nil { return nil, ossSettingValue{}, err } value, err := s.userOSSSettingValue(setting) return setting, value, err } func (s *Service) userOSSSettingValue(setting *model.UserOSSSetting) (ossSettingValue, error) { value := defaultOSSSetting() if strings.TrimSpace(setting.ValueJSON) != "" { if err := json.Unmarshal([]byte(setting.ValueJSON), &value); err != nil { return ossSettingValue{}, errors.New("用户 OSS 配置格式无效") } } if _, err := s.decryptOSSSettingSecrets(&value); err != nil { return ossSettingValue{}, err } value.Enabled = setting.Enabled return normalizeOSSSetting(value), nil } func (s *Service) encryptOSSSettingSecrets(value ossSettingValue) (ossSettingValue, error) { var err error value.AccessKeySecret, err = s.encryptSettingSecret(value.AccessKeySecret) if err != nil { return ossSettingValue{}, err } value.SessionToken, err = s.encryptSettingSecret(value.SessionToken) if err != nil { return ossSettingValue{}, err } value.ArchivedCredentials = cloneOSSProviderCredentials(value.ArchivedCredentials) for provider, credentials := range value.ArchivedCredentials { credentials.AccessKeySecret, err = s.encryptSettingSecret(credentials.AccessKeySecret) if err != nil { return ossSettingValue{}, err } value.ArchivedCredentials[provider] = credentials } return value, nil } func (s *Service) decryptOSSSettingSecrets(value *ossSettingValue) (bool, error) { needsMigration := value.AccessKeySecret != "" && !strings.HasPrefix(value.AccessKeySecret, encryptedSettingPrefix) secret, err := s.decryptSettingSecret(value.AccessKeySecret) if err != nil { return false, err } value.AccessKeySecret = secret if value.SessionToken != "" && !strings.HasPrefix(value.SessionToken, encryptedSettingPrefix) { needsMigration = true } value.SessionToken, err = s.decryptSettingSecret(value.SessionToken) if err != nil { return false, err } value.ArchivedCredentials = cloneOSSProviderCredentials(value.ArchivedCredentials) for provider, credentials := range value.ArchivedCredentials { if credentials.AccessKeySecret != "" && !strings.HasPrefix(credentials.AccessKeySecret, encryptedSettingPrefix) { needsMigration = true } credentials.AccessKeySecret, err = s.decryptSettingSecret(credentials.AccessKeySecret) if err != nil { return false, err } value.ArchivedCredentials[provider] = credentials } return needsMigration, nil } func (s *Service) encryptSettingSecret(value string) (string, error) { if value == "" { return "", nil } key, err := s.settingsEncryptionKey() if err != nil { return "", err } block, err := aes.NewCipher(key) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonce := make([]byte, gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return "", err } ciphertext := gcm.Seal(nil, nonce, []byte(value), nil) return encryptedSettingPrefix + base64.RawStdEncoding.EncodeToString(append(nonce, ciphertext...)), nil } func (s *Service) decryptSettingSecret(value string) (string, error) { if !strings.HasPrefix(value, encryptedSettingPrefix) { return value, nil } payload, err := base64.RawStdEncoding.DecodeString(strings.TrimPrefix(value, encryptedSettingPrefix)) if err != nil { return "", errors.New("OSS 密钥密文格式无效") } key, err := s.settingsEncryptionKey() if err != nil { return "", err } block, err := aes.NewCipher(key) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } if len(payload) < gcm.NonceSize() { return "", errors.New("OSS 密钥密文长度无效") } plaintext, err := gcm.Open(nil, payload[:gcm.NonceSize()], payload[gcm.NonceSize():], nil) if err != nil { return "", errors.New("OSS 密钥解密失败,请检查存储加密密钥") } return string(plaintext), nil } func (s *Service) settingsEncryptionKey() ([]byte, error) { path := filepath.Join(s.dataDir, ".settings-key") if data, err := os.ReadFile(path); err == nil && len(data) == 32 { return data, nil } if err := os.MkdirAll(s.dataDir, 0o750); err != nil { return nil, err } key := make([]byte, 32) if _, err := io.ReadFull(rand.Reader, key); err != nil { return nil, err } file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) if errors.Is(err, os.ErrExist) { data, readErr := os.ReadFile(path) if readErr != nil { return nil, fmt.Errorf("读取存储加密密钥失败:%w", readErr) } if len(data) != 32 { return nil, errors.New("存储加密密钥长度无效") } return data, nil } if err != nil { return nil, err } if _, err := file.Write(key); err != nil { _ = file.Close() return nil, err } if err := file.Close(); err != nil { return nil, err } return key, nil } func (s *Service) protectTaskSecrets(value interface{}) error { switch item := value.(type) { case map[string]interface{}: for key, child := range item { if isTaskSecretField(key) { secret, _ := child.(string) if secret != "" && secret != "system" && !strings.HasPrefix(secret, encryptedSettingPrefix) { encrypted, err := s.encryptSettingSecret(secret) if err != nil { return err } item[key] = encrypted } continue } if err := s.protectTaskSecrets(child); err != nil { return err } } case []interface{}: for _, child := range item { if err := s.protectTaskSecrets(child); err != nil { return err } } } return nil } func (s *Service) decryptTaskInputJSON(raw string) (string, error) { if strings.TrimSpace(raw) == "" || !strings.Contains(raw, encryptedSettingPrefix) { return raw, nil } var input interface{} if err := json.Unmarshal([]byte(raw), &input); err != nil { return "", err } if err := s.decryptTaskSecrets(input); err != nil { return "", err } encoded, err := json.Marshal(input) return string(encoded), err } func (s *Service) decryptTaskSecrets(value interface{}) error { switch item := value.(type) { case map[string]interface{}: for key, child := range item { if isTaskSecretField(key) { secret, _ := child.(string) if strings.HasPrefix(secret, encryptedSettingPrefix) { plain, err := s.decryptSettingSecret(secret) if err != nil { return err } item[key] = plain } continue } if err := s.decryptTaskSecrets(child); err != nil { return err } } case []interface{}: for _, child := range item { if err := s.decryptTaskSecrets(child); err != nil { return err } } } return nil } func isTaskSecretField(key string) bool { switch key { case "apiKey", "secretKey", "runningHubWalletApiKey", "runningHubUploadApiKey": return true default: return false } } func ossSettingFromRequest(req OSSSettingRequest, current ossSettingValue) (ossSettingValue, error) { next := normalizeOSSSetting(ossSettingValue{ Enabled: req.Enabled, Provider: strings.TrimSpace(req.Provider), Region: strings.TrimSpace(req.Region), Endpoint: strings.TrimRight(strings.TrimSpace(req.Endpoint), "/"), CDNBaseURL: strings.TrimRight(strings.TrimSpace(req.CDNBaseURL), "/"), Bucket: strings.TrimSpace(req.Bucket), AccessKeyID: strings.TrimSpace(req.AccessKeyID), AccessKeySecret: strings.TrimSpace(req.AccessKeySecret), PublicBaseURL: strings.TrimRight(strings.TrimSpace(req.PublicBaseURL), "/"), PathPrefix: strings.Trim(strings.TrimSpace(req.PathPrefix), "/"), S3Preset: strings.TrimSpace(req.S3Preset), PathStyle: req.PathStyle, SessionToken: strings.TrimSpace(req.SessionToken), AllowUserS3: req.AllowUserS3, }) if next.Provider != aliyunOSSProvider && next.Provider != tencentCOSProvider && next.Provider != qiniuKodoProvider && next.Provider != s3Provider { return next, BadAuthRequest("仅支持阿里云 OSS、腾讯云 COS、七牛云 Kodo 和通用 S3") } if next.Provider == s3Provider { switch next.S3Preset { case "aws", "r2", "b2", "rustfs", "custom": default: return next, BadAuthRequest("S3 预设无效") } } current = normalizeOSSSetting(current) // 同一云厂商修改存储位置时,留空仍表示保留现有密钥;切换厂商时不能复用。 if next.AccessKeySecret == "" && next.Provider == current.Provider { next.AccessKeySecret = current.AccessKeySecret } if next.SessionToken == "" && next.Provider == current.Provider { next.SessionToken = current.SessionToken } if next.Enabled { if next.Bucket == "" { return next, BadAuthRequest("请填写对象存储 Bucket") } if next.Endpoint == "" { if next.Provider == tencentCOSProvider { return next, BadAuthRequest("请填写腾讯云 COS Region 或 Endpoint") } if next.Provider == qiniuKodoProvider { return next, BadAuthRequest("请填写七牛云 Kodo 上传 Endpoint") } if next.Provider == s3Provider { return next, BadAuthRequest("请填写 S3 Endpoint 服务根 URL") } return next, BadAuthRequest("请填写阿里云 OSS Endpoint") } if next.Provider == s3Provider && next.Region == "" { return next, BadAuthRequest("请填写 S3 Region") } var endpointErr error if next.Provider == s3Provider { _, endpointErr = validateStorageEndpoint(next.Endpoint) } else { _, endpointErr = ValidateOutboundURL(next.Endpoint) } if endpointErr != nil { return next, endpointErr } if next.CDNBaseURL != "" { if _, err := ossCDNBaseURL(next.CDNBaseURL); err != nil { return next, BadAuthRequest(err.Error()) } if _, err := ValidateOutboundURL(next.CDNBaseURL); err != nil { return next, err } } if next.AccessKeyID == "" { return next, BadAuthRequest("请填写访问密钥 AccessKey") } if next.AccessKeySecret == "" { return next, BadAuthRequest("请填写访问密钥 SecretKey") } } return next, nil } func archiveOSSProviderCredentials(next ossSettingValue, current ossSettingValue) ossSettingValue { next.ArchivedCredentials = cloneOSSProviderCredentials(current.ArchivedCredentials) if current.Provider != next.Provider && (current.AccessKeyID != "" || current.AccessKeySecret != "") { if next.ArchivedCredentials == nil { next.ArchivedCredentials = make(map[string]ossProviderCredentials) } next.ArchivedCredentials[current.Provider] = ossProviderCredentials{AccessKeyID: current.AccessKeyID, AccessKeySecret: current.AccessKeySecret} } delete(next.ArchivedCredentials, next.Provider) return next } func cloneOSSProviderCredentials(source map[string]ossProviderCredentials) map[string]ossProviderCredentials { if len(source) == 0 { return nil } cloned := make(map[string]ossProviderCredentials, len(source)) for provider, credentials := range source { cloned[strings.ToLower(strings.TrimSpace(provider))] = ossProviderCredentials{ AccessKeyID: strings.TrimSpace(credentials.AccessKeyID), AccessKeySecret: strings.TrimSpace(credentials.AccessKeySecret), } } return cloned } func normalizeOSSSetting(value ossSettingValue) ossSettingValue { value.Provider = strings.ToLower(strings.TrimSpace(value.Provider)) if value.Provider == "" { value.Provider = aliyunOSSProvider } value.Region = strings.TrimSpace(value.Region) value.Endpoint = strings.TrimRight(strings.TrimSpace(value.Endpoint), "/") if value.Provider == tencentCOSProvider && value.Endpoint == "" && value.Region != "" { value.Endpoint = "https://cos." + value.Region + ".myqcloud.com" } value.CDNBaseURL = strings.TrimRight(strings.TrimSpace(value.CDNBaseURL), "/") value.Bucket = strings.TrimSpace(value.Bucket) value.AccessKeyID = strings.TrimSpace(value.AccessKeyID) value.AccessKeySecret = strings.TrimSpace(value.AccessKeySecret) value.PublicBaseURL = strings.TrimRight(strings.TrimSpace(value.PublicBaseURL), "/") value.PathPrefix = strings.Trim(strings.TrimSpace(value.PathPrefix), "/") if value.PathPrefix == "" { value.PathPrefix = defaultOSSPathPrefix } value.S3Preset = strings.ToLower(strings.TrimSpace(value.S3Preset)) if value.S3Preset == "" { value.S3Preset = "custom" } value.SessionToken = strings.TrimSpace(value.SessionToken) value.StorageLocationID = strings.TrimSpace(value.StorageLocationID) value.ArchivedCredentials = cloneOSSProviderCredentials(value.ArchivedCredentials) return value } func defaultOSSSetting() ossSettingValue { return ossSettingValue{Provider: aliyunOSSProvider, PathPrefix: defaultOSSPathPrefix} } func (s *Service) publicOSSSetting(setting *model.SystemSetting, value ossSettingValue, scope string, ownerID string) (PublicOSSSetting, error) { result := PublicOSSSetting{ Enabled: value.Enabled, Provider: value.Provider, Region: value.Region, Endpoint: value.Endpoint, CDNBaseURL: value.CDNBaseURL, Bucket: value.Bucket, AccessKeyID: value.AccessKeyID, HasAccessKeySecret: strings.TrimSpace(value.AccessKeySecret) != "", PublicBaseURL: value.PublicBaseURL, PathPrefix: value.PathPrefix, S3Preset: value.S3Preset, PathStyle: value.PathStyle, HasSessionToken: value.SessionToken != "", StorageLocationID: value.StorageLocationID, AllowUserS3: value.AllowUserS3, } if setting != nil { result.UpdatedBy = setting.UpdatedBy result.CreatedAt = setting.CreatedAt result.UpdatedAt = setting.UpdatedAt } if err := s.populateStorageLocationPublic(&result, scope, ownerID, value.StorageLocationID); err != nil { return PublicOSSSetting{}, err } return result, nil } func (s *Service) publicUserOSSSetting(setting *model.UserOSSSetting, value ossSettingValue, ownerID string, allowUserS3 bool) (PublicOSSSetting, error) { result := PublicOSSSetting{ Enabled: value.Enabled, Provider: value.Provider, Region: value.Region, Endpoint: value.Endpoint, CDNBaseURL: value.CDNBaseURL, Bucket: value.Bucket, AccessKeyID: value.AccessKeyID, HasAccessKeySecret: strings.TrimSpace(value.AccessKeySecret) != "", PublicBaseURL: value.PublicBaseURL, PathPrefix: value.PathPrefix, S3Preset: value.S3Preset, PathStyle: value.PathStyle, HasSessionToken: value.SessionToken != "", StorageLocationID: value.StorageLocationID, AllowUserS3: allowUserS3, } if value.Provider == s3Provider && !allowUserS3 { result.Enabled = false } if setting != nil { result.UpdatedBy = setting.UserID result.CreatedAt = setting.CreatedAt result.UpdatedAt = setting.UpdatedAt } if err := s.populateStorageLocationPublic(&result, "user", ownerID, value.StorageLocationID); err != nil { return PublicOSSSetting{}, err } return result, nil } func storageLocationDigest(value ossSettingValue) string { value = normalizeOSSSetting(value) payload := strings.Join([]string{value.Provider, value.Endpoint, value.Bucket, value.Region, strconv.FormatBool(value.PathStyle), value.PathPrefix}, "\x00") sum := sha256.Sum256([]byte(payload)) return hex.EncodeToString(sum[:]) } func storageTestDigest(value ossSettingValue) string { payload := strings.Join([]string{storageLocationDigest(value), value.AccessKeyID, value.AccessKeySecret, value.SessionToken}, "\x00") sum := sha256.Sum256([]byte(payload)) return hex.EncodeToString(sum[:]) }