108 lines
3.1 KiB
Go
108 lines
3.1 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package auth
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"sync"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/keychain"
|
|
)
|
|
|
|
var (
|
|
migrationOnce sync.Once
|
|
migrationDone bool
|
|
)
|
|
|
|
// SaveTokenDataKeychain saves TokenData to the platform keychain.
|
|
// This is the new secure storage method using random master key.
|
|
func SaveTokenDataKeychain(data *TokenData) error {
|
|
jsonData, err := json.MarshalIndent(data, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("marshal token data: %w", err)
|
|
}
|
|
// Zero sensitive data after use
|
|
defer func() {
|
|
for i := range jsonData {
|
|
jsonData[i] = 0
|
|
}
|
|
}()
|
|
|
|
if err := keychain.Set(keychain.Service, keychain.AccountToken, string(jsonData)); err != nil {
|
|
return fmt.Errorf("save to keychain: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LoadTokenDataKeychain loads TokenData from the platform keychain.
|
|
func LoadTokenDataKeychain() (*TokenData, error) {
|
|
jsonStr, err := keychain.Get(keychain.Service, keychain.AccountToken)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load from keychain: %w", err)
|
|
}
|
|
if jsonStr == "" {
|
|
return nil, fmt.Errorf("no token data in keychain")
|
|
}
|
|
|
|
var data TokenData
|
|
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
|
|
return nil, fmt.Errorf("parse token data: %w", err)
|
|
}
|
|
return &data, nil
|
|
}
|
|
|
|
// DeleteTokenDataKeychain removes TokenData from the platform keychain.
|
|
func DeleteTokenDataKeychain() error {
|
|
return keychain.Remove(keychain.Service, keychain.AccountToken)
|
|
}
|
|
|
|
// TokenDataExistsKeychain checks if token data exists in keychain.
|
|
func TokenDataExistsKeychain() bool {
|
|
return keychain.Exists(keychain.Service, keychain.AccountToken)
|
|
}
|
|
|
|
// EnsureMigration performs one-time migration from legacy .data to keychain.
|
|
// This should be called early in the auth flow (e.g., during GetAccessToken).
|
|
// The migration is idempotent and thread-safe.
|
|
func EnsureMigration(configDir string, logger *slog.Logger) {
|
|
migrationOnce.Do(func() {
|
|
result := keychain.MigrateFromLegacy(configDir)
|
|
migrationDone = true
|
|
|
|
if result.Migrated {
|
|
if logger != nil {
|
|
logger.Info("migrated token data to secure keychain storage",
|
|
"from", result.FromPath,
|
|
"backup", result.BackupPath)
|
|
}
|
|
} else if result.NeedRelogin {
|
|
if logger != nil {
|
|
logger.Warn("cannot migrate legacy token data, please re-login",
|
|
"error", result.Error)
|
|
}
|
|
} else if result.Error != nil {
|
|
if logger != nil {
|
|
logger.Error("migration failed", "error", result.Error)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// IsMigrationDone returns true if migration has been attempted.
|
|
func IsMigrationDone() bool {
|
|
return migrationDone
|
|
}
|