// auth_static_key — static key auth primitive // // 讀取 auth_recipe:{service} + 解密 required_secrets + 展開 {{secret.X}} 模板, // 回傳 auth_headers / auth_query / auth_body。 // // 所有外部 I/O 都透過 host function: // - u6u.kv_get — 依 key 前綴路由到 RECIPES / CREDENTIALS_KV (host 做越權檢查) // - u6u.crypto_decrypt — AES-GCM 解密 (encryption key 由 host 持有,不暴露給 WASM) // //go:build tinygo package main import ( "encoding/base64" "encoding/json" "io" "os" "strings" "unsafe" ) // ── host function 宣告 ─────────────────────────────────────────────────────── // kv_get(keyPtr, keyLen, outPtr, outLenPtr) → 0 成功 / 1 錯誤 / 2 找不到 // //go:wasmimport u6u kv_get func hostKvGet( keyPtr uintptr, keyLen uint32, outPtr uintptr, outLenPtr uintptr, ) uint32 // crypto_decrypt(encPtr, encLen, ivPtr, ivLen, outPtr, outLenPtr) → 0 成功 // enc/iv 為 base64 字串(即 KV 中儲存的格式) // //go:wasmimport u6u crypto_decrypt func hostCryptoDecrypt( encPtr uintptr, encLen uint32, ivPtr uintptr, ivLen uint32, outPtr uintptr, outLenPtr uintptr, ) uint32 // ── 型別 ───────────────────────────────────────────────────────────────────── type Input struct { Action string `json:"action"` APIKey string `json:"api_key"` Service string `json:"service"` Request json.RawMessage `json:"request,omitempty"` } type SecretRequirement struct { Key string `json:"key"` Label string `json:"label"` Optional bool `json:"optional,omitempty"` } type AuthInjectSpec struct { Header map[string]string `json:"header,omitempty"` Query map[string]string `json:"query,omitempty"` Body map[string]string `json:"body,omitempty"` // Path:要注入 endpoint URL path 的 secret(如 telegram /bot{token}/)。 // key = 模板變數名(recipe endpoint 用 {{auth.K}} 引用),value = {{secret.X}} 模板。 Path map[string]string `json:"path,omitempty"` } type AuthRecipe struct { Kind string `json:"kind"` Service string `json:"service"` Primitive string `json:"primitive"` RequiredSecrets []SecretRequirement `json:"required_secrets"` Inject AuthInjectSpec `json:"inject"` } type EncryptedRecord struct { Encrypted string `json:"encrypted"` IV string `json:"iv"` } // ── main ───────────────────────────────────────────────────────────────────── func main() { raw, err := io.ReadAll(os.Stdin) if err != nil { writeError("failed to read stdin: " + err.Error()) return } var input Input if err := json.Unmarshal(raw, &input); err != nil { writeError("invalid input JSON: " + err.Error()) return } if input.APIKey == "" { writeError("api_key 必填") return } if input.Service == "" { writeError("service 必填") return } if input.Action != "" && input.Action != "authenticate" { writeError("auth_static_key 僅支援 action=authenticate") return } // 1. 讀 auth recipe recipeJSON, status := kvGet("auth_recipe:" + input.Service) if status == 2 { writeError("找不到 auth recipe: " + input.Service) return } if status != 0 { writeError("kv_get 失敗(auth_recipe)") return } var recipe AuthRecipe if err := json.Unmarshal([]byte(recipeJSON), &recipe); err != nil { writeError("auth recipe JSON 解析失敗: " + err.Error()) return } if recipe.Primitive != "static_key" { writeError("auth recipe " + input.Service + " 的 primitive 不是 static_key (是 " + recipe.Primitive + ")") return } // 2. 解密所有 non-optional required_secrets secrets := make(map[string]string) for _, req := range recipe.RequiredSecrets { if req.Optional { continue } kvKey := input.APIKey + ":cred:" + req.Key encJSON, s := kvGet(kvKey) if s == 2 { writeError("缺少 credential: " + req.Key + " (" + req.Label + ")。修復: 編輯 credentials.yaml 後執行 acr creds push") return } if s != 0 { writeError("kv_get 失敗(credential " + req.Key + ")") return } var rec EncryptedRecord if err := json.Unmarshal([]byte(encJSON), &rec); err != nil { writeError("credential " + req.Key + " 格式錯誤: " + err.Error()) return } plaintext, ok := cryptoDecrypt(rec.Encrypted, rec.IV) if !ok { writeError("credential " + req.Key + " 解密失敗") return } secrets[req.Key] = plaintext } // 3. 展開模板 (static_key 沒有 runtime,傳空 map) runtime := map[string]string{} authHeaders := interpolateRecord(recipe.Inject.Header, secrets, runtime) authQuery := interpolateRecord(recipe.Inject.Query, secrets, runtime) authBody := interpolateRecord(recipe.Inject.Body, secrets, runtime) authPath := interpolateRecord(recipe.Inject.Path, secrets, runtime) // 3.5 Basic Auth 自動編碼:若 header 值為 "Basic :" (冒號代表未編碼的 user:pass), // 將冒號分隔部分做 base64。這涵蓋 twilio / jira / mailgun 等 Basic Auth recipe。 // "Basic " (無冒號) 維持原樣,向後相容。 // header key 不分大小寫比對 "authorization"。 for k, v := range authHeaders { if !strings.EqualFold(k, "Authorization") { continue } const prefix = "Basic " if !strings.HasPrefix(v, prefix) { continue } payload := v[len(prefix):] if !strings.Contains(payload, ":") { continue } authHeaders[k] = prefix + base64.StdEncoding.EncodeToString([]byte(payload)) } // 4. 輸出 out, _ := json.Marshal(map[string]interface{}{ "success": true, "auth_headers": authHeaders, "auth_query": authQuery, "auth_body": authBody, "auth_path": authPath, "runtime": runtime, }) os.Stdout.Write(out) } // ── helpers ────────────────────────────────────────────────────────────────── func writeError(msg string) { out, _ := json.Marshal(map[string]interface{}{ "success": false, "error": msg, "auth_headers": map[string]string{}, "auth_query": map[string]string{}, "auth_body": map[string]string{}, }) os.Stdout.Write(out) } // kvGet 呼叫 host function,回傳 (value, status)。status: 0=成功 1=錯誤 2=找不到 func kvGet(key string) (string, uint32) { keyBytes := []byte(key) outBuf := make([]byte, 65536) var outLen uint32 status := hostKvGet( uintptr(unsafe.Pointer(&keyBytes[0])), uint32(len(keyBytes)), uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)), ) if status != 0 { return "", status } return string(outBuf[:outLen]), 0 } // cryptoDecrypt 呼叫 host function 做 AES-GCM 解密 // enc/iv 均為 base64 字串;回傳 UTF-8 plaintext func cryptoDecrypt(encB64, ivB64 string) (string, bool) { encBytes := []byte(encB64) ivBytes := []byte(ivB64) outBuf := make([]byte, 65536) var outLen uint32 // 處理空字串的防呆(TinyGo 取 &[]byte{}[0] 會 panic) if len(encBytes) == 0 || len(ivBytes) == 0 { return "", false } status := hostCryptoDecrypt( uintptr(unsafe.Pointer(&encBytes[0])), uint32(len(encBytes)), uintptr(unsafe.Pointer(&ivBytes[0])), uint32(len(ivBytes)), uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)), ) if status != 0 { return "", false } return string(outBuf[:outLen]), true } // interpolateTemplate 展開 {{secret.X}} 與 {{runtime.X}}。未知 key 展開為空字串(與 TS 版 parity)。 // 其他 namespace 的 {{...}} 原樣保留(static_key 不解析)。 func interpolateTemplate(template string, secrets, runtime map[string]string) string { var b strings.Builder b.Grow(len(template)) i := 0 for i < len(template) { start := strings.Index(template[i:], "{{") if start < 0 { b.WriteString(template[i:]) break } b.WriteString(template[i : i+start]) openIdx := i + start closeRel := strings.Index(template[openIdx+2:], "}}") if closeRel < 0 { b.WriteString(template[openIdx:]) break } inner := template[openIdx+2 : openIdx+2+closeRel] advance := openIdx + 2 + closeRel + 2 switch { case strings.HasPrefix(inner, "secret."): key := inner[len("secret."):] b.WriteString(secrets[key]) case strings.HasPrefix(inner, "runtime."): key := inner[len("runtime."):] b.WriteString(runtime[key]) default: // 非本 primitive 負責的 namespace,原樣寫回 b.WriteString(template[openIdx:advance]) } i = advance } return b.String() } func interpolateRecord( record map[string]string, secrets, runtime map[string]string, ) map[string]string { if record == nil { return map[string]string{} } result := make(map[string]string, len(record)) for k, v := range record { result[k] = interpolateTemplate(v, secrets, runtime) } return result }