gcp_kms.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. package gcp
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "encoding/base64"
  6. "fmt"
  7. "strings"
  8. "time"
  9. "google.golang.org/api/option"
  10. kms "cloud.google.com/go/kms/apiv1"
  11. "cloud.google.com/go/kms/apiv1/kmspb"
  12. "github.com/seaweedfs/seaweedfs/weed/glog"
  13. seaweedkms "github.com/seaweedfs/seaweedfs/weed/kms"
  14. "github.com/seaweedfs/seaweedfs/weed/util"
  15. )
  16. func init() {
  17. // Register the Google Cloud KMS provider
  18. seaweedkms.RegisterProvider("gcp", NewGCPKMSProvider)
  19. }
  20. // GCPKMSProvider implements the KMSProvider interface using Google Cloud KMS
  21. type GCPKMSProvider struct {
  22. client *kms.KeyManagementClient
  23. projectID string
  24. }
  25. // GCPKMSConfig contains configuration for the Google Cloud KMS provider
  26. type GCPKMSConfig struct {
  27. ProjectID string `json:"project_id"` // GCP project ID
  28. CredentialsFile string `json:"credentials_file"` // Path to service account JSON file
  29. CredentialsJSON string `json:"credentials_json"` // Service account JSON content (base64 encoded)
  30. UseDefaultCredentials bool `json:"use_default_credentials"` // Use default GCP credentials (metadata service, gcloud, etc.)
  31. RequestTimeout int `json:"request_timeout"` // Request timeout in seconds (default: 30)
  32. }
  33. // NewGCPKMSProvider creates a new Google Cloud KMS provider
  34. func NewGCPKMSProvider(config util.Configuration) (seaweedkms.KMSProvider, error) {
  35. if config == nil {
  36. return nil, fmt.Errorf("Google Cloud KMS configuration is required")
  37. }
  38. // Extract configuration
  39. projectID := config.GetString("project_id")
  40. if projectID == "" {
  41. return nil, fmt.Errorf("project_id is required for Google Cloud KMS provider")
  42. }
  43. credentialsFile := config.GetString("credentials_file")
  44. credentialsJSON := config.GetString("credentials_json")
  45. useDefaultCredentials := config.GetBool("use_default_credentials")
  46. requestTimeout := config.GetInt("request_timeout")
  47. if requestTimeout == 0 {
  48. requestTimeout = 30 // Default 30 seconds
  49. }
  50. // Prepare client options
  51. var clientOptions []option.ClientOption
  52. // Configure credentials
  53. if credentialsFile != "" {
  54. clientOptions = append(clientOptions, option.WithCredentialsFile(credentialsFile))
  55. glog.V(1).Infof("GCP KMS: Using credentials file %s", credentialsFile)
  56. } else if credentialsJSON != "" {
  57. // Decode base64 credentials if provided
  58. credBytes, err := base64.StdEncoding.DecodeString(credentialsJSON)
  59. if err != nil {
  60. return nil, fmt.Errorf("failed to decode credentials JSON: %w", err)
  61. }
  62. clientOptions = append(clientOptions, option.WithCredentialsJSON(credBytes))
  63. glog.V(1).Infof("GCP KMS: Using provided credentials JSON")
  64. } else if !useDefaultCredentials {
  65. return nil, fmt.Errorf("either credentials_file, credentials_json, or use_default_credentials=true must be provided")
  66. } else {
  67. glog.V(1).Infof("GCP KMS: Using default credentials")
  68. }
  69. // Set request timeout
  70. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(requestTimeout)*time.Second)
  71. defer cancel()
  72. // Create KMS client
  73. client, err := kms.NewKeyManagementClient(ctx, clientOptions...)
  74. if err != nil {
  75. return nil, fmt.Errorf("failed to create Google Cloud KMS client: %w", err)
  76. }
  77. provider := &GCPKMSProvider{
  78. client: client,
  79. projectID: projectID,
  80. }
  81. glog.V(1).Infof("Google Cloud KMS provider initialized for project %s", projectID)
  82. return provider, nil
  83. }
  84. // GenerateDataKey generates a new data encryption key using Google Cloud KMS
  85. func (p *GCPKMSProvider) GenerateDataKey(ctx context.Context, req *seaweedkms.GenerateDataKeyRequest) (*seaweedkms.GenerateDataKeyResponse, error) {
  86. if req == nil {
  87. return nil, fmt.Errorf("GenerateDataKeyRequest cannot be nil")
  88. }
  89. if req.KeyID == "" {
  90. return nil, fmt.Errorf("KeyID is required")
  91. }
  92. // Validate key spec
  93. var keySize int
  94. switch req.KeySpec {
  95. case seaweedkms.KeySpecAES256:
  96. keySize = 32 // 256 bits
  97. default:
  98. return nil, fmt.Errorf("unsupported key spec: %s", req.KeySpec)
  99. }
  100. // Generate data key locally (GCP KMS doesn't have GenerateDataKey like AWS)
  101. dataKey := make([]byte, keySize)
  102. if _, err := rand.Read(dataKey); err != nil {
  103. return nil, fmt.Errorf("failed to generate random data key: %w", err)
  104. }
  105. // Encrypt the data key using GCP KMS
  106. glog.V(4).Infof("GCP KMS: Encrypting data key using key %s", req.KeyID)
  107. // Build the encryption request
  108. encryptReq := &kmspb.EncryptRequest{
  109. Name: req.KeyID,
  110. Plaintext: dataKey,
  111. }
  112. // Add additional authenticated data from encryption context
  113. if len(req.EncryptionContext) > 0 {
  114. // Convert encryption context to additional authenticated data
  115. aad := p.encryptionContextToAAD(req.EncryptionContext)
  116. encryptReq.AdditionalAuthenticatedData = []byte(aad)
  117. }
  118. // Call GCP KMS to encrypt the data key
  119. encryptResp, err := p.client.Encrypt(ctx, encryptReq)
  120. if err != nil {
  121. return nil, p.convertGCPError(err, req.KeyID)
  122. }
  123. // Create standardized envelope format for consistent API behavior
  124. envelopeBlob, err := seaweedkms.CreateEnvelope("gcp", encryptResp.Name, string(encryptResp.Ciphertext), nil)
  125. if err != nil {
  126. return nil, fmt.Errorf("failed to create ciphertext envelope: %w", err)
  127. }
  128. response := &seaweedkms.GenerateDataKeyResponse{
  129. KeyID: encryptResp.Name, // GCP returns the full resource name
  130. Plaintext: dataKey,
  131. CiphertextBlob: envelopeBlob, // Store in standardized envelope format
  132. }
  133. glog.V(4).Infof("GCP KMS: Generated and encrypted data key using key %s", req.KeyID)
  134. return response, nil
  135. }
  136. // Decrypt decrypts an encrypted data key using Google Cloud KMS
  137. func (p *GCPKMSProvider) Decrypt(ctx context.Context, req *seaweedkms.DecryptRequest) (*seaweedkms.DecryptResponse, error) {
  138. if req == nil {
  139. return nil, fmt.Errorf("DecryptRequest cannot be nil")
  140. }
  141. if len(req.CiphertextBlob) == 0 {
  142. return nil, fmt.Errorf("CiphertextBlob cannot be empty")
  143. }
  144. // Parse the ciphertext envelope to extract key information
  145. envelope, err := seaweedkms.ParseEnvelope(req.CiphertextBlob)
  146. if err != nil {
  147. return nil, fmt.Errorf("failed to parse ciphertext envelope: %w", err)
  148. }
  149. keyName := envelope.KeyID
  150. if keyName == "" {
  151. return nil, fmt.Errorf("envelope missing key ID")
  152. }
  153. // Convert string back to bytes
  154. ciphertext := []byte(envelope.Ciphertext)
  155. // Build the decryption request
  156. decryptReq := &kmspb.DecryptRequest{
  157. Name: keyName,
  158. Ciphertext: ciphertext,
  159. }
  160. // Add additional authenticated data from encryption context
  161. if len(req.EncryptionContext) > 0 {
  162. aad := p.encryptionContextToAAD(req.EncryptionContext)
  163. decryptReq.AdditionalAuthenticatedData = []byte(aad)
  164. }
  165. // Call GCP KMS to decrypt the data key
  166. glog.V(4).Infof("GCP KMS: Decrypting data key using key %s", keyName)
  167. decryptResp, err := p.client.Decrypt(ctx, decryptReq)
  168. if err != nil {
  169. return nil, p.convertGCPError(err, keyName)
  170. }
  171. response := &seaweedkms.DecryptResponse{
  172. KeyID: keyName,
  173. Plaintext: decryptResp.Plaintext,
  174. }
  175. glog.V(4).Infof("GCP KMS: Decrypted data key using key %s", keyName)
  176. return response, nil
  177. }
  178. // DescribeKey validates that a key exists and returns its metadata
  179. func (p *GCPKMSProvider) DescribeKey(ctx context.Context, req *seaweedkms.DescribeKeyRequest) (*seaweedkms.DescribeKeyResponse, error) {
  180. if req == nil {
  181. return nil, fmt.Errorf("DescribeKeyRequest cannot be nil")
  182. }
  183. if req.KeyID == "" {
  184. return nil, fmt.Errorf("KeyID is required")
  185. }
  186. // Build the request to get the crypto key
  187. getKeyReq := &kmspb.GetCryptoKeyRequest{
  188. Name: req.KeyID,
  189. }
  190. // Call GCP KMS to get key information
  191. glog.V(4).Infof("GCP KMS: Describing key %s", req.KeyID)
  192. key, err := p.client.GetCryptoKey(ctx, getKeyReq)
  193. if err != nil {
  194. return nil, p.convertGCPError(err, req.KeyID)
  195. }
  196. response := &seaweedkms.DescribeKeyResponse{
  197. KeyID: key.Name,
  198. ARN: key.Name, // GCP uses resource names instead of ARNs
  199. Description: "Google Cloud KMS key",
  200. }
  201. // Map GCP key purpose to our usage enum
  202. if key.Purpose == kmspb.CryptoKey_ENCRYPT_DECRYPT {
  203. response.KeyUsage = seaweedkms.KeyUsageEncryptDecrypt
  204. }
  205. // Map GCP key state to our state enum
  206. // Get the primary version to check its state
  207. if key.Primary != nil && key.Primary.State == kmspb.CryptoKeyVersion_ENABLED {
  208. response.KeyState = seaweedkms.KeyStateEnabled
  209. } else {
  210. response.KeyState = seaweedkms.KeyStateDisabled
  211. }
  212. // GCP KMS keys are managed by Google Cloud
  213. response.Origin = seaweedkms.KeyOriginGCP
  214. glog.V(4).Infof("GCP KMS: Described key %s (state: %s)", req.KeyID, response.KeyState)
  215. return response, nil
  216. }
  217. // GetKeyID resolves a key name to the full resource name
  218. func (p *GCPKMSProvider) GetKeyID(ctx context.Context, keyIdentifier string) (string, error) {
  219. if keyIdentifier == "" {
  220. return "", fmt.Errorf("key identifier cannot be empty")
  221. }
  222. // If it's already a full resource name, return as-is
  223. if strings.HasPrefix(keyIdentifier, "projects/") {
  224. return keyIdentifier, nil
  225. }
  226. // Otherwise, try to construct the full resource name or validate via DescribeKey
  227. descReq := &seaweedkms.DescribeKeyRequest{KeyID: keyIdentifier}
  228. descResp, err := p.DescribeKey(ctx, descReq)
  229. if err != nil {
  230. return "", fmt.Errorf("failed to resolve key identifier %s: %w", keyIdentifier, err)
  231. }
  232. return descResp.KeyID, nil
  233. }
  234. // Close cleans up any resources used by the provider
  235. func (p *GCPKMSProvider) Close() error {
  236. if p.client != nil {
  237. err := p.client.Close()
  238. if err != nil {
  239. glog.Errorf("Error closing GCP KMS client: %v", err)
  240. return err
  241. }
  242. }
  243. glog.V(2).Infof("Google Cloud KMS provider closed")
  244. return nil
  245. }
  246. // encryptionContextToAAD converts encryption context map to additional authenticated data
  247. // This is a simplified implementation - in production, you might want a more robust serialization
  248. func (p *GCPKMSProvider) encryptionContextToAAD(context map[string]string) string {
  249. if len(context) == 0 {
  250. return ""
  251. }
  252. // Simple key=value&key=value format
  253. var parts []string
  254. for k, v := range context {
  255. parts = append(parts, fmt.Sprintf("%s=%s", k, v))
  256. }
  257. return strings.Join(parts, "&")
  258. }
  259. // convertGCPError converts Google Cloud KMS errors to our standard KMS errors
  260. func (p *GCPKMSProvider) convertGCPError(err error, keyID string) error {
  261. // Google Cloud SDK uses gRPC status codes
  262. errMsg := err.Error()
  263. if strings.Contains(errMsg, "not found") || strings.Contains(errMsg, "NotFound") {
  264. return &seaweedkms.KMSError{
  265. Code: seaweedkms.ErrCodeNotFoundException,
  266. Message: fmt.Sprintf("Key not found in Google Cloud KMS: %v", err),
  267. KeyID: keyID,
  268. }
  269. }
  270. if strings.Contains(errMsg, "permission") || strings.Contains(errMsg, "access") || strings.Contains(errMsg, "Forbidden") {
  271. return &seaweedkms.KMSError{
  272. Code: seaweedkms.ErrCodeAccessDenied,
  273. Message: fmt.Sprintf("Access denied to Google Cloud KMS: %v", err),
  274. KeyID: keyID,
  275. }
  276. }
  277. if strings.Contains(errMsg, "disabled") || strings.Contains(errMsg, "unavailable") {
  278. return &seaweedkms.KMSError{
  279. Code: seaweedkms.ErrCodeKeyUnavailable,
  280. Message: fmt.Sprintf("Key unavailable in Google Cloud KMS: %v", err),
  281. KeyID: keyID,
  282. }
  283. }
  284. // For unknown errors, wrap as internal failure
  285. return &seaweedkms.KMSError{
  286. Code: seaweedkms.ErrCodeKMSInternalFailure,
  287. Message: fmt.Sprintf("Google Cloud KMS error: %v", err),
  288. KeyID: keyID,
  289. }
  290. }