Files
dingtalk-workspace-cli/internal/keychain/migrate.go
T
2026-03-28 17:51:22 +08:00

145 lines
4.3 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 keychain
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/security"
)
const (
legacyDataFile = ".data"
legacyBackupFile = ".data.migrated.bak"
)
// MigrationResult contains the result of a migration attempt.
type MigrationResult struct {
Migrated bool // true if migration was performed
FromPath string // source file path
BackupPath string // backup file path (if migrated)
Error error // error if migration failed
NeedRelogin bool // true if user needs to re-login
}
// MigrateFromLegacy attempts to migrate from the legacy MAC-based encryption
// to the new keychain-based storage. It:
// 1. Checks if legacy .data file exists
// 2. Tries to decrypt with MAC address
// 3. Re-encrypts and stores in keychain
// 4. Backs up the old file
//
// If the keychain already has data, migration is skipped.
// If the legacy file doesn't exist, migration is skipped.
// If decryption fails (wrong MAC/corrupted), returns NeedRelogin=true.
func MigrateFromLegacy(configDir string) *MigrationResult {
result := &MigrationResult{}
// Check if keychain already has data - skip migration
if Exists(Service, AccountToken) {
return result // Already migrated or fresh install
}
// Check if legacy .data file exists
legacyPath := filepath.Join(configDir, legacyDataFile)
if _, err := os.Stat(legacyPath); os.IsNotExist(err) {
return result // No legacy data to migrate
}
result.FromPath = legacyPath
// Try to decrypt legacy data using MAC address
legacyData, err := loadLegacyData(configDir)
if err != nil {
// Decryption failed - likely different machine or corrupted
result.Error = fmt.Errorf("cannot decrypt legacy data: %w", err)
result.NeedRelogin = true
return result
}
// Store in new keychain
jsonData, err := json.Marshal(legacyData)
if err != nil {
result.Error = fmt.Errorf("marshal token data: %w", err)
return result
}
if err := Set(Service, AccountToken, string(jsonData)); err != nil {
result.Error = fmt.Errorf("store in keychain: %w", err)
return result
}
// Backup old file instead of deleting
backupPath := filepath.Join(configDir, legacyBackupFile)
if err := os.Rename(legacyPath, backupPath); err != nil {
// Non-fatal - data is already migrated
_ = os.Remove(legacyPath)
} else {
result.BackupPath = backupPath
}
result.Migrated = true
return result
}
// loadLegacyData loads and decrypts the legacy .data file using MAC address.
func loadLegacyData(configDir string) (map[string]interface{}, error) {
// Get MAC address for decryption
mac, err := security.GetMACAddress()
if err != nil {
return nil, fmt.Errorf("get MAC address: %w", err)
}
// Read encrypted file
path := filepath.Join(configDir, legacyDataFile)
ciphertext, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read legacy file: %w", err)
}
// Decrypt using legacy method
plaintext, err := security.Decrypt(ciphertext, []byte(mac))
if err != nil {
return nil, fmt.Errorf("decrypt legacy data: %w", err)
}
// Parse JSON
var data map[string]interface{}
if err := json.Unmarshal(plaintext, &data); err != nil {
return nil, fmt.Errorf("parse legacy JSON: %w", err)
}
return data, nil
}
// CleanupLegacyBackup removes the backup file created during migration.
// Call this after confirming the new keychain storage works correctly.
func CleanupLegacyBackup(configDir string) error {
backupPath := filepath.Join(configDir, legacyBackupFile)
if err := os.Remove(backupPath); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
// HasLegacyData checks if legacy .data file exists.
func HasLegacyData(configDir string) bool {
legacyPath := filepath.Join(configDir, legacyDataFile)
_, err := os.Stat(legacyPath)
return err == nil
}