token.go 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. package main
  2. import (
  3. "crypto/aes"
  4. "crypto/cipher"
  5. "crypto/rand"
  6. "encoding/base64"
  7. "errors"
  8. "fmt"
  9. "os"
  10. )
  11. // tokenCodec 使用 AES-256-GCM 把文件相对路径加密成不可读、不可篡改的分享 token。
  12. type tokenCodec struct {
  13. gcm cipher.AEAD
  14. }
  15. func newTokenCodec(key []byte) (*tokenCodec, error) {
  16. block, err := aes.NewCipher(key)
  17. if err != nil {
  18. return nil, err
  19. }
  20. gcm, err := cipher.NewGCM(block)
  21. if err != nil {
  22. return nil, err
  23. }
  24. return &tokenCodec{gcm: gcm}, nil
  25. }
  26. // Seal 加密明文(相对路径),返回 base64url(无填充)字符串。
  27. // 每次使用随机 nonce,因此同一明文的 token 每次不同,但均可被 Open 解密。
  28. func (c *tokenCodec) Seal(plaintext string) string {
  29. nonce := make([]byte, c.gcm.NonceSize())
  30. if _, err := rand.Read(nonce); err != nil {
  31. panic(err) // crypto/rand 失败属于系统级异常
  32. }
  33. ciphertext := c.gcm.Seal(nonce, nonce, []byte(plaintext), nil)
  34. return base64.RawURLEncoding.EncodeToString(ciphertext)
  35. }
  36. // Open 解密并校验 token。token 被篡改或非本服务签发时返回错误。
  37. func (c *tokenCodec) Open(token string) (string, error) {
  38. raw, err := base64.RawURLEncoding.DecodeString(token)
  39. if err != nil {
  40. return "", errors.New("token 编码非法")
  41. }
  42. ns := c.gcm.NonceSize()
  43. if len(raw) < ns {
  44. return "", errors.New("token 长度非法")
  45. }
  46. nonce, ciphertext := raw[:ns], raw[ns:]
  47. plaintext, err := c.gcm.Open(nil, nonce, ciphertext, nil)
  48. if err != nil {
  49. return "", errors.New("token 校验失败")
  50. }
  51. return string(plaintext), nil
  52. }
  53. // loadOrCreateSecret 读取 32 字节密钥;不存在则随机生成并持久化(base64 编码)。
  54. // 密钥文件权限尽量收紧为 0600(Windows 上仅能尽力而为)。
  55. func loadOrCreateSecret(path string) ([]byte, error) {
  56. if data, err := os.ReadFile(path); err == nil {
  57. key := make([]byte, 32)
  58. n, err := base64.StdEncoding.Decode(key, data)
  59. if err == nil && n == 32 {
  60. return key, nil
  61. }
  62. // 兼容直接存放的原始 32 字节
  63. if len(data) == 32 {
  64. return data, nil
  65. }
  66. return nil, fmt.Errorf("密钥文件格式无效: %s", path)
  67. } else if !os.IsNotExist(err) {
  68. return nil, err
  69. }
  70. key := make([]byte, 32)
  71. if _, err := rand.Read(key); err != nil {
  72. return nil, err
  73. }
  74. encoded := []byte(base64.StdEncoding.EncodeToString(key))
  75. if err := os.WriteFile(path, encoded, 0o600); err != nil {
  76. return nil, err
  77. }
  78. return key, nil
  79. }