Files
dingtalk-workspace-cli/internal/keychain/keychain_windows_validation_test.go

195 lines
6.9 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.
//go:build windows
package keychain
import (
"encoding/base64"
"errors"
"strings"
"testing"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
func writeRawRegistryString(t *testing.T, service, account, value string) {
t.Helper()
writeRawRegistryNamedString(t, service, valueNameForAccount(account), value)
}
func writeRawRegistryNamedString(t *testing.T, service, name, value string) {
t.Helper()
k, _, err := registry.CreateKey(
registry.CURRENT_USER,
registryPathForService(service),
registry.SET_VALUE,
)
if err != nil {
t.Fatalf("registry.CreateKey() error = %v", err)
}
defer k.Close()
if err := k.SetStringValue(name, value); err != nil {
t.Fatalf("SetStringValue(%q) error = %v", name, err)
}
}
func writeRawRegistryDWORD(t *testing.T, service, account string, value uint32) {
t.Helper()
k, _, err := registry.CreateKey(
registry.CURRENT_USER,
registryPathForService(service),
registry.SET_VALUE,
)
if err != nil {
t.Fatalf("registry.CreateKey() error = %v", err)
}
defer k.Close()
if err := k.SetDWordValue(valueNameForAccount(account), value); err != nil {
t.Fatalf("SetDWordValue(%q) error = %v", account, err)
}
}
func TestCrossPlatformCoverageWindowsRegistryDistinguishesMissingFromUnreadable(t *testing.T) {
service := "test-service-" + t.Name()
account := AccountToken + ":legacy-corp"
t.Cleanup(func() { _ = Remove(service, account) })
got, err := Get(service, account)
if err != nil || got != "" {
t.Fatalf("missing registry account = %q, %v; want empty and nil", got, err)
}
writeRawRegistryString(t, service, account, "")
if _, err := Get(service, account); err == nil || !strings.Contains(err.Error(), "empty DPAPI ciphertext") {
t.Fatalf("empty registry value error = %v", err)
}
missingAccount := AccountToken + ":missing-value"
if got, err := Get(service, missingAccount); err != nil || got != "" {
t.Fatalf("missing value in existing registry key = %q, %v; want empty and nil", got, err)
}
writeRawRegistryString(t, service, account, "%%%not-base64%%")
if _, err := Get(service, account); err == nil || !strings.Contains(err.Error(), "decode registry account") {
t.Fatalf("invalid base64 error = %v", err)
}
writeRawRegistryString(t, service, account, base64.StdEncoding.EncodeToString([]byte("not a DPAPI blob")))
if _, err := Get(service, account); err == nil || !strings.Contains(err.Error(), "dpapi unprotect") {
t.Fatalf("invalid DPAPI blob error = %v", err)
}
writeRawRegistryDWORD(t, service, account, 1)
if _, err := Get(service, account); err == nil || !strings.Contains(err.Error(), "registry read account") {
t.Fatalf("unexpected registry value type error = %v", err)
}
}
func TestCrossPlatformCoverageWindowsAuthTokenInventoryValidatesOrphanSlots(t *testing.T) {
service := "test-service-" + t.Name()
const unrelated = "app-secret:client"
orphan := AccountToken + ":orphan-corp"
t.Cleanup(func() {
_ = Remove(service, unrelated)
_ = Remove(service, orphan)
})
if err := ValidateAuthTokenEntries(service); err != nil {
t.Fatalf("missing registry key validation error = %v", err)
}
// Corruption in unrelated keychain accounts is outside the auth inventory.
writeRawRegistryString(t, service, unrelated, "%%%not-base64%%")
writeRawRegistryNamedString(t, service, "%%%not-an-account-name%%", "%%%not-base64%%")
if err := ValidateAuthTokenEntries(service); err != nil {
t.Fatalf("unrelated registry value validation error = %v", err)
}
// The orphan is deliberately not represented in profiles.json. Inventory
// discovery must still find it by its historical auth-token account prefix.
writeRawRegistryString(t, service, orphan, "%%%not-base64%%")
if err := ValidateAuthTokenEntries(service); err == nil || !strings.Contains(err.Error(), orphan) {
t.Fatalf("orphan auth token validation error = %v", err)
}
if err := Set(service, orphan, "legacy token"); err != nil {
t.Fatalf("Set(orphan) error = %v", err)
}
if err := ValidateAuthTokenEntries(service); err != nil {
t.Fatalf("valid orphan auth token validation error = %v", err)
}
got, err := Get(service, orphan)
if err != nil || got != "legacy token" {
t.Fatalf("round trip = %q, %v; want legacy token", got, err)
}
}
func TestCrossPlatformCoverageWindowsRegistryReadFailuresFailClosed(t *testing.T) {
t.Run("open failure", func(t *testing.T) {
originalOpen := registryOpenReadKey
failure := windows.ERROR_ACCESS_DENIED
registryOpenReadKey = func(registry.Key, string, uint32) (registry.Key, error) {
return 0, failure
}
t.Cleanup(func() { registryOpenReadKey = originalOpen })
if _, err := Get("unreadable-service", AccountToken); !errors.Is(err, failure) {
t.Fatalf("Get() error = %v, want %v", err, failure)
}
if err := ValidateAuthTokenEntries("unreadable-service"); !errors.Is(err, failure) {
t.Fatalf("ValidateAuthTokenEntries() error = %v, want %v", err, failure)
}
})
t.Run("inventory enumeration failure", func(t *testing.T) {
service := "test-service-" + t.Name()
account := AccountToken + ":enumeration-failure"
writeRawRegistryString(t, service, account, "fixture")
t.Cleanup(func() { _ = Remove(service, account) })
originalReadNames := registryReadValueNames
failure := windows.ERROR_ACCESS_DENIED
registryReadValueNames = func(registry.Key, int) ([]string, error) {
return nil, failure
}
t.Cleanup(func() { registryReadValueNames = originalReadNames })
if err := ValidateAuthTokenEntries(service); !errors.Is(err, failure) {
t.Fatalf("ValidateAuthTokenEntries() error = %v, want %v", err, failure)
}
})
t.Run("inventory value disappears", func(t *testing.T) {
service := "test-service-" + t.Name()
account := AccountToken + ":disappearing"
unrelated := "app-secret:keeps-key-present"
writeRawRegistryString(t, service, unrelated, "fixture")
t.Cleanup(func() { _ = Remove(service, unrelated) })
originalReadNames := registryReadValueNames
registryReadValueNames = func(registry.Key, int) ([]string, error) {
// Model the real race after enumeration: the account name was
// observed, but GetStringValue no longer finds its value.
return []string{valueNameForAccount(account)}, nil
}
t.Cleanup(func() { registryReadValueNames = originalReadNames })
err := ValidateAuthTokenEntries(service)
if err == nil || !strings.Contains(err.Error(), "value disappeared during validation") {
t.Fatalf("ValidateAuthTokenEntries() error = %v, want disappearance error", err)
}
})
}