go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/appengine/gaemiddleware/aead.go (about) 1 // Copyright 2021 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package gaemiddleware 16 17 import ( 18 "bytes" 19 "context" 20 "fmt" 21 "os" 22 "path/filepath" 23 "strings" 24 "sync" 25 "time" 26 27 secretmanager "cloud.google.com/go/secretmanager/apiv1" 28 "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" 29 "google.golang.org/api/option" 30 "google.golang.org/appengine" 31 32 "github.com/google/tink/go/aead" 33 "github.com/google/tink/go/insecurecleartextkeyset" 34 "github.com/google/tink/go/keyset" 35 "github.com/google/tink/go/tink" 36 37 "go.chromium.org/luci/common/clock" 38 "go.chromium.org/luci/common/errors" 39 "go.chromium.org/luci/common/logging" 40 "go.chromium.org/luci/server/auth" 41 "go.chromium.org/luci/server/caching" 42 ) 43 44 var ( 45 cachedAEAD = caching.RegisterCacheSlot() 46 settingsCheckPeriod = time.Minute 47 rotationCheckPeriod = time.Hour 48 ) 49 50 // AEADProvider loads the primary encryption key from Google Secret Manager. 51 // 52 // If it is not configured and we are running on a dev server, generates a new 53 // phony one. If it is not configured and we are running in production, returns 54 // nil to indicate AEAD is not available. 55 // 56 // If the key is configured, but can't be loaded, returns a tink.AEAD 57 // implementation that returns errors in all its methods. 58 func AEADProvider(ctx context.Context) tink.AEAD { 59 s := fetchCachedSettings(ctx) 60 if s.EncryptionKey == "" { 61 if appengine.IsDevAppServer() { 62 return useDevServerKey(ctx) 63 } 64 return nil 65 } 66 state, err := cachedAEAD.Fetch(ctx, func(prev any) (updated any, exp time.Duration, err error) { 67 state, _ := prev.(*aeadCachedState) 68 if state == nil || state.keyPath != s.EncryptionKey { 69 state = &aeadCachedState{keyPath: s.EncryptionKey} 70 } 71 err = state.refresh(ctx) 72 return state, settingsCheckPeriod, err 73 }) 74 if err != nil { 75 return brokenAEAD{err} 76 } 77 return state.(*aeadCachedState).aead 78 } 79 80 type brokenAEAD struct { 81 err error 82 } 83 84 func (b brokenAEAD) Encrypt(_, _ []byte) ([]byte, error) { return nil, b.err } 85 func (b brokenAEAD) Decrypt(_, _ []byte) ([]byte, error) { return nil, b.err } 86 87 //////////////////////////////////////////////////////////////////////////////// 88 // Prod caching helpers. 89 90 type aeadCachedState struct { 91 keyPath string 92 rotationCheck time.Time 93 aead tink.AEAD 94 } 95 96 func (s *aeadCachedState) refresh(ctx context.Context) error { 97 if s.aead != nil && clock.Now(ctx).Before(s.rotationCheck) { 98 return nil // have the key and it is fresh enough 99 } 100 101 // Fetch the secret blob from the Secret Manager. 102 chunks := strings.Split(strings.TrimPrefix(s.keyPath, "sm://"), "/") 103 if len(chunks) != 2 { 104 logging.Errorf(ctx, "Bad encryption key URI %q", s.keyPath) 105 return errors.Reason("bad secret URI %q", s.keyPath).Err() 106 } 107 blob, err := fetchSecret(ctx, chunks[0], chunks[1]) 108 if err != nil { 109 logging.Errorf(ctx, "Failed to load Google Secret Manager secret %s: %s", s.keyPath, err) 110 return err 111 } 112 113 // Construct tink.AEAD out of it. 114 kh, err := insecurecleartextkeyset.Read(keyset.NewJSONReader(bytes.NewReader(blob))) 115 if err != nil { 116 logging.Errorf(ctx, "Secret %q doesn't contain a valid Tink keyset: %s", s.keyPath, err) 117 return err 118 } 119 a, err := aead.New(kh) 120 if err != nil { 121 logging.Errorf(ctx, "Secret %q doesn't contain an AEAD Tink key: %s", s.keyPath, err) 122 return err 123 } 124 125 // Record when we should check it again in case it is rotated. 126 s.aead = a 127 s.rotationCheck = clock.Now(ctx).Add(rotationCheckPeriod) 128 return nil 129 } 130 131 func fetchSecret(ctx context.Context, project, secret string) ([]byte, error) { 132 ts, err := auth.GetTokenSource(ctx, auth.AsSelf, auth.WithScopes(auth.CloudOAuthScopes...)) 133 if err != nil { 134 return nil, errors.Annotate(err, "failed to get OAuth2 token source").Err() 135 } 136 137 client, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts)) 138 if err != nil { 139 return nil, errors.Annotate(err, "failed to setup Secret Manager client").Err() 140 } 141 defer client.Close() 142 143 latest, err := client.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{ 144 Name: fmt.Sprintf("projects/%s/secrets/%s/versions/latest", project, secret), 145 }) 146 if err != nil { 147 return nil, err 148 } 149 logging.Infof(ctx, "Loaded secret %q", latest.Name) 150 return latest.Payload.Data, nil 151 } 152 153 //////////////////////////////////////////////////////////////////////////////// 154 // Dev server helpers. 155 156 var devServerLock sync.Mutex 157 158 func useDevServerKey(ctx context.Context) tink.AEAD { 159 devServerLock.Lock() 160 defer devServerLock.Unlock() 161 162 path := filepath.Join(os.TempDir(), "luci-insecure-dev-tink-aead-key.json") 163 164 // Try to load an existing key. 165 switch key, err := loadDevServerKey(path); { 166 case err == nil: 167 return key 168 case !os.IsNotExist(err): 169 logging.Warningf(ctx, "Ignoring bad dev server Tink key %s: %s", path, err) 170 } 171 172 // Generate the new key. 173 kh, err := keyset.NewHandle(aead.AES256GCMKeyTemplate()) 174 if err != nil { 175 panic(err) // e.g. no entropy 176 } 177 out, err := aead.New(kh) 178 if err != nil { 179 panic(err) // not really possible 180 } 181 buf := &bytes.Buffer{} 182 if err = insecurecleartextkeyset.Write(kh, keyset.NewJSONWriter(buf)); err != nil { 183 panic(err) // not really possible 184 } 185 186 // Store it so encrypted blobs survive the dev server restart. 187 logging.Infof(ctx, "Generated new dev server Tink key at %s", path) 188 if err := os.WriteFile(path, buf.Bytes(), 0600); err != nil { 189 logging.Warningf(ctx, "Failed to store dev server Tink key %s: %s", path, err) 190 } 191 192 return out 193 } 194 195 func loadDevServerKey(path string) (tink.AEAD, error) { 196 blob, err := os.ReadFile(path) 197 if err != nil { 198 return nil, err 199 } 200 kh, err := insecurecleartextkeyset.Read(keyset.NewJSONReader(bytes.NewReader(blob))) 201 if err != nil { 202 return nil, err 203 } 204 return aead.New(kh) 205 }