Files
sub2api/backend/internal/pkg/kiro/fingerprint.go
T
2026-05-17 06:45:35 +08:00

259 lines
7.3 KiB
Go

package kiro
import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"math/rand"
"runtime"
"strings"
"sync"
"time"
"github.com/google/uuid"
)
type RuntimeFingerprint struct {
OIDCSDKVersion string
RuntimeSDKVersion string
StreamingSDKVersion string
OSType string
OSVersion string
NodeVersion string
KiroVersion string
KiroHash string
}
type runtimeFingerprintManager struct {
mu sync.RWMutex
fingerprints map[string]*RuntimeFingerprint
}
var (
globalRuntimeFingerprintManager *runtimeFingerprintManager
globalRuntimeFingerprintManagerOnce sync.Once
oidcSDKVersions = []string{"3.980.0", "3.975.0", "3.972.0", "3.808.0", "3.738.0", "3.737.0", "3.736.0", "3.735.0"}
runtimeSDKVersions = []string{"1.0.0"}
streamingSDKVersions = []string{"1.0.34"}
osTypes = []string{"darwin", "win32"}
osVersions = map[string][]string{
"darwin": {"24.6.0"},
"win32": {"10.0.22631"},
}
nodeVersions = []string{"22.22.0"}
kiroVersions = []string{
"0.11.132", "0.11.131", "0.11.130",
}
)
func globalRuntimeFingerprints() *runtimeFingerprintManager {
globalRuntimeFingerprintManagerOnce.Do(func() {
globalRuntimeFingerprintManager = &runtimeFingerprintManager{
fingerprints: make(map[string]*RuntimeFingerprint),
}
})
return globalRuntimeFingerprintManager
}
func (m *runtimeFingerprintManager) Get(accountKey, machineID string) *RuntimeFingerprint {
lookupKey := fingerprintLookupKey(accountKey, "runtime")
machineID = normalizeMachineIDOrFallback(machineID, lookupKey)
m.mu.RLock()
if fp, ok := m.fingerprints[lookupKey]; ok && fp.KiroHash == machineID {
m.mu.RUnlock()
return fp
}
m.mu.RUnlock()
m.mu.Lock()
defer m.mu.Unlock()
if fp, ok := m.fingerprints[lookupKey]; ok && fp.KiroHash == machineID {
return fp
}
fp := generateRuntimeFingerprint(lookupKey, machineID)
m.fingerprints[lookupKey] = fp
return fp
}
func generateRuntimeFingerprint(accountKey, machineID string) *RuntimeFingerprint {
hash := sha256.Sum256([]byte(accountKey))
seed := int64(binary.BigEndian.Uint64(hash[:8]))
rng := rand.New(rand.NewSource(seed))
osType := goOSToNodePlatform(runtime.GOOS)
if !containsString(osTypes, osType) {
osType = osTypes[rng.Intn(len(osTypes))]
}
osVersionPool := osVersions[osType]
if len(osVersionPool) == 0 {
osVersionPool = osVersions["darwin"]
}
return &RuntimeFingerprint{
OIDCSDKVersion: oidcSDKVersions[rng.Intn(len(oidcSDKVersions))],
RuntimeSDKVersion: runtimeSDKVersions[rng.Intn(len(runtimeSDKVersions))],
StreamingSDKVersion: streamingSDKVersions[rng.Intn(len(streamingSDKVersions))],
OSType: osType,
OSVersion: osVersionPool[rng.Intn(len(osVersionPool))],
NodeVersion: nodeVersions[rng.Intn(len(nodeVersions))],
KiroVersion: kiroVersions[rng.Intn(len(kiroVersions))],
KiroHash: machineID,
}
}
func goOSToNodePlatform(goos string) string {
switch strings.TrimSpace(goos) {
case "windows":
return "win32"
default:
return strings.TrimSpace(goos)
}
}
func containsString(items []string, target string) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
}
func BuildAccountKey(clientID, clientIDHash, refreshToken, profileArn string, accountID int64) string {
switch {
case strings.TrimSpace(clientIDHash) != "":
return clientIDHash
case strings.TrimSpace(clientID) != "":
return shortSHA(clientID)
case strings.TrimSpace(refreshToken) != "":
return shortSHA(refreshToken)
case strings.TrimSpace(profileArn) != "":
return shortSHA(profileArn)
case accountID > 0:
return shortSHA(fmt.Sprintf("account:%d", accountID))
default:
return shortSHA(uuid.NewString())
}
}
func NormalizeMachineID(machineID string) (string, bool) {
trimmed := strings.TrimSpace(machineID)
if len(trimmed) == 64 && isHexString(trimmed) {
return strings.ToLower(trimmed), true
}
withoutDashes := strings.ReplaceAll(trimmed, "-", "")
if len(withoutDashes) == 32 && isHexString(withoutDashes) {
normalized := strings.ToLower(withoutDashes)
return normalized + normalized, true
}
return "", false
}
func BuildMachineID(refreshToken, apiKey, fallbackKey string) string {
if refreshToken = strings.TrimSpace(refreshToken); refreshToken != "" {
return sha256Hex("KotlinNativeAPI/" + refreshToken)
}
if apiKey = strings.TrimSpace(apiKey); apiKey != "" {
return sha256Hex("KiroAPIKey/" + apiKey)
}
if fallbackKey = strings.TrimSpace(fallbackKey); fallbackKey != "" {
return sha256Hex("KiroFallback/" + fallbackKey)
}
return sha256Hex("KiroFallback/default")
}
func shortSHA(seed string) string {
sum := sha256.Sum256([]byte(seed))
return hex.EncodeToString(sum[:8])
}
func sha256Hex(seed string) string {
sum := sha256.Sum256([]byte(seed))
return hex.EncodeToString(sum[:])
}
func isHexString(value string) bool {
for _, c := range value {
if (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') {
return false
}
}
return true
}
func normalizeMachineIDOrFallback(machineID, fallbackKey string) string {
if normalized, ok := NormalizeMachineID(machineID); ok {
return normalized
}
return BuildMachineID("", "", fallbackKey)
}
func fingerprintLookupKey(accountKey, fallback string) string {
key := strings.TrimSpace(accountKey)
if key != "" {
return key
}
return fallback
}
func BuildRuntimeUserAgent(accountKey, machineID string) string {
fp := globalRuntimeFingerprints().Get(accountKey, machineID)
return fmt.Sprintf(
"aws-sdk-js/%s ua/2.1 os/%s#%s lang/js md/nodejs#%s api/codewhispererstreaming#%s m/E KiroIDE-%s-%s",
fp.StreamingSDKVersion,
fp.OSType,
fp.OSVersion,
fp.NodeVersion,
fp.StreamingSDKVersion,
fp.KiroVersion,
fp.KiroHash,
)
}
func BuildRuntimeAmzUserAgent(accountKey, machineID string) string {
fp := globalRuntimeFingerprints().Get(accountKey, machineID)
return fmt.Sprintf(
"aws-sdk-js/%s KiroIDE-%s-%s",
fp.StreamingSDKVersion,
fp.KiroVersion,
fp.KiroHash,
)
}
func BuildOIDCHeaders(accountKey, machineID string) map[string]string {
fp := globalRuntimeFingerprints().Get(fingerprintLookupKey(accountKey, "oidc-session"), machineID)
return map[string]string{
"Content-Type": "application/json",
"x-amz-user-agent": fmt.Sprintf("aws-sdk-js/%s KiroIDE", fp.OIDCSDKVersion),
"User-Agent": fmt.Sprintf("aws-sdk-js/%s ua/2.1 os/%s#%s lang/js md/nodejs#%s api/sso-oidc#%s m/E KiroIDE", fp.OIDCSDKVersion, fp.OSType, fp.OSVersion, fp.NodeVersion, fp.OIDCSDKVersion),
"amz-sdk-invocation-id": uuid.NewString(),
"amz-sdk-request": "attempt=1; max=4",
}
}
func BuildLoginHeaders(accountKey, machineID string) map[string]string {
fp := globalRuntimeFingerprints().Get(fingerprintLookupKey(accountKey, "login"), machineID)
return map[string]string{
"Content-Type": "application/json",
"User-Agent": fmt.Sprintf("KiroIDE-%s-%s", fp.KiroVersion, fp.KiroHash),
"Accept": "application/json, text/plain, */*",
}
}
func ExponentialBackoffWithJitter(attempt int, baseDelay, maxDelay time.Duration) time.Duration {
if attempt < 0 {
attempt = 0
}
delay := baseDelay << attempt
if delay > maxDelay {
delay = maxDelay
}
const jitterFactor = 0.3
seed := rand.New(rand.NewSource(time.Now().UnixNano()))
jitter := 1 + ((seed.Float64()*2 - 1) * jitterFactor)
return time.Duration(float64(delay) * jitter)
}