From efba8ea09881de4df157a567ef920d352665070f Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Tue, 12 May 2026 13:59:38 +0200 Subject: [PATCH 01/24] feat: add master key config and validation --- server/server.go | 44 ++++++++++++++++++++++++++++++++++ server/server_test.go | 56 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/server/server.go b/server/server.go index d80c1ae65c..98b69c2eeb 100644 --- a/server/server.go +++ b/server/server.go @@ -5,6 +5,7 @@ import ( "crypto/rsa" "crypto/tls" "crypto/x509" + "encoding/base64" "encoding/pem" "errors" "fmt" @@ -150,6 +151,8 @@ type Server struct { ReportingDisabled bool `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"` CustomAutoRefresh string `long:"custom-auto-refresh" description:"Adds custom auto refresh options using semicolon separated list of label=milliseconds pairs" env:"CUSTOM_AUTO_REFRESH"` LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"error" default:"info" description:"Set the logging level" env:"LOG_LEVEL"` + SecretsMasterKey string `long:"secrets-master-key" description:"Base64-encoded 32-byte master key used to wrap/unwrap the data encryption key for secret-field encryption" env:"SECRETS_MASTER_KEY"` + SecretsMasterKeyFile flags.Filename `long:"secrets-master-key-file" description:"Path to file containing a base64-encoded 32-byte master key used to wrap/unwrap the data encryption key for secret-field encryption" env:"SECRETS_MASTER_KEY_FILE"` Basepath string `short:"p" long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted. (Note: PREFIX_ROUTES has been deprecated. Now, if basepath is set, all routes will be prefixed with it.)" env:"BASE_PATH"` ShowVersion bool `short:"v" long:"version" description:"Show Chronograf version info"` BuildInfo chronograf.BuildInfo @@ -162,6 +165,8 @@ type Server struct { TLSMaxVersion string `long:"tls-max-version" description:"Maximum version of the TLS protocol that will be negotiated." env:"TLS_MAX_VERSION"` oauthClient http.Client + + secretsMasterKey []byte } func provide(p oauth2.Provider, m oauth2.Mux, ok func() error) func(func(oauth2.Provider, oauth2.Mux)) { @@ -641,6 +646,38 @@ func (s *Server) setPubkey() error { return err } +func (s *Server) loadSecretsMasterKey() ([]byte, error) { + if s.SecretsMasterKey != "" && s.SecretsMasterKeyFile != "" { + return nil, errors.New("secrets master key must be provided by either --secrets-master-key or --secrets-master-key-file, not both") + } + + if s.SecretsMasterKey == "" && s.SecretsMasterKeyFile == "" { + return nil, nil + } + + raw := strings.TrimSpace(s.SecretsMasterKey) + if s.SecretsMasterKeyFile != "" { + b, err := os.ReadFile(string(s.SecretsMasterKeyFile)) + if err != nil { + return nil, fmt.Errorf("reading secrets master key file: %w", err) + } + raw = strings.TrimSpace(string(b)) + } + + if raw == "" { + return nil, errors.New("secrets master key is empty") + } + + key, err := base64.StdEncoding.DecodeString(raw) + if err != nil { + return nil, fmt.Errorf("decoding secrets master key: %w", err) + } + if len(key) != 32 { + return nil, fmt.Errorf("invalid secrets master key length: got %d bytes, expected 32", len(key)) + } + return key, nil +} + // Serve starts and runs the chronograf server func (s *Server) Serve(ctx context.Context) { go rotateSuperAdminNonce(ctx, s.NonceExpiration) @@ -689,6 +726,13 @@ func (s *Server) Serve(ctx context.Context) { os.Exit(1) } + secretsMasterKey, err := s.loadSecretsMasterKey() + if err != nil { + logger.Error("Invalid secrets master key configuration: ", err) + os.Exit(1) + } + s.secretsMasterKey = secretsMasterKey + var db kv.Store if len(s.EtcdEndpoints) == 0 { db, err = bolt.NewClient(ctx, diff --git a/server/server_test.go b/server/server_test.go index 5ce3e800b2..f6fee8d856 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -4,13 +4,16 @@ import ( "bytes" "context" "crypto/tls" + "encoding/base64" "encoding/pem" "fmt" "net/http" "net/http/httptest" + "os" "testing" "github.com/bouk/httprouter" + flags "github.com/jessevdk/go-flags" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -131,6 +134,59 @@ func Test_useSecureCookies(t *testing.T) { } } +func Test_loadSecretsMasterKey(t *testing.T) { + validKey := []byte("0123456789abcdef0123456789abcdef") + validB64 := base64.StdEncoding.EncodeToString(validKey) + + t.Run("empty config returns nil", func(t *testing.T) { + s := Server{} + got, err := s.loadSecretsMasterKey() + require.NoError(t, err) + require.Nil(t, got) + }) + + t.Run("fails when both direct and file are set", func(t *testing.T) { + s := Server{ + SecretsMasterKey: validB64, + SecretsMasterKeyFile: "master-key.txt", + } + _, err := s.loadSecretsMasterKey() + require.Error(t, err) + require.Contains(t, err.Error(), "either --secrets-master-key or --secrets-master-key-file") + }) + + t.Run("fails on invalid base64", func(t *testing.T) { + s := Server{SecretsMasterKey: "not-base64"} + _, err := s.loadSecretsMasterKey() + require.Error(t, err) + require.Contains(t, err.Error(), "decoding secrets master key") + }) + + t.Run("fails on wrong decoded length", func(t *testing.T) { + tooShort := base64.StdEncoding.EncodeToString([]byte("short")) + s := Server{SecretsMasterKey: tooShort} + _, err := s.loadSecretsMasterKey() + require.Error(t, err) + require.Contains(t, err.Error(), "invalid secrets master key length") + }) + + t.Run("loads direct key", func(t *testing.T) { + s := Server{SecretsMasterKey: validB64} + got, err := s.loadSecretsMasterKey() + require.NoError(t, err) + require.Equal(t, validKey, got) + }) + + t.Run("loads key from file", func(t *testing.T) { + f := t.TempDir() + "/master-key.txt" + require.NoError(t, os.WriteFile(f, []byte(validB64+"\n"), 0600)) + s := Server{SecretsMasterKeyFile: flags.Filename(f)} + got, err := s.loadSecretsMasterKey() + require.NoError(t, err) + require.Equal(t, validKey, got) + }) +} + func TestValidAuth(t *testing.T) { tests := []struct { desc string From 787f11d4da9ea89ada63b37abe17522c7f3596a2 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Tue, 12 May 2026 16:07:59 +0200 Subject: [PATCH 02/24] feat(kv): add DEK/secret crypto primitives --- kv/internal/crypto.go | 97 +++++++++++++++++++++++++++++++++ kv/internal/crypto_test.go | 106 +++++++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 kv/internal/crypto.go create mode 100644 kv/internal/crypto_test.go diff --git a/kv/internal/crypto.go b/kv/internal/crypto.go new file mode 100644 index 0000000000..978e53071b --- /dev/null +++ b/kv/internal/crypto.go @@ -0,0 +1,97 @@ +package internal + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "fmt" + "io" +) + +const ( + secretCryptoVersionV1 byte = 1 + aes256KeySize = 32 + gcmNonceSize = 12 +) + +func generateDEK() ([]byte, error) { + dek := make([]byte, aes256KeySize) + if _, err := io.ReadFull(rand.Reader, dek); err != nil { + return nil, fmt.Errorf("generate DEK: %w", err) + } + return dek, nil +} + +func wrapDEK(masterKey, dek []byte) ([]byte, error) { + return sealV1(masterKey, dek) +} + +func unwrapDEK(masterKey, wrappedDEK []byte) ([]byte, error) { + return openV1(masterKey, wrappedDEK) +} + +func encryptSecret(dek, plaintextSecret []byte) ([]byte, error) { + return sealV1(dek, plaintextSecret) +} + +func decryptSecret(dek, encryptedSecret []byte) ([]byte, error) { + return openV1(dek, encryptedSecret) +} + +func sealV1(key, plaintext []byte) ([]byte, error) { + if len(key) != aes256KeySize { + return nil, fmt.Errorf("invalid key length: got %d bytes, expected %d", len(key), aes256KeySize) + } + + aesBlock, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("create AES cipher: %w", err) + } + gcmCipher, err := cipher.NewGCM(aesBlock) + if err != nil { + return nil, fmt.Errorf("create GCM: %w", err) + } + + nonce := make([]byte, gcmNonceSize) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, fmt.Errorf("generate nonce: %w", err) + } + + // Envelope format: [version|nonce|ciphertext] + ciphertext := gcmCipher.Seal(nil, nonce, plaintext, nil) + payload := make([]byte, 0, 1+len(nonce)+len(ciphertext)) + payload = append(payload, secretCryptoVersionV1) + payload = append(payload, nonce...) + payload = append(payload, ciphertext...) + return payload, nil +} + +func openV1(key, payload []byte) ([]byte, error) { + if len(key) != aes256KeySize { + return nil, fmt.Errorf("invalid key length: got %d bytes, expected %d", len(key), aes256KeySize) + } + if len(payload) < 1+gcmNonceSize+16 { + return nil, fmt.Errorf("invalid payload length: %d", len(payload)) + } + if payload[0] != secretCryptoVersionV1 { + return nil, fmt.Errorf("unsupported payload version: %d", payload[0]) + } + + nonce := payload[1 : 1+gcmNonceSize] + ciphertext := payload[1+gcmNonceSize:] + + aesBlock, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("create AES cipher: %w", err) + } + gcmCipher, err := cipher.NewGCM(aesBlock) + if err != nil { + return nil, fmt.Errorf("create GCM: %w", err) + } + + plaintext, err := gcmCipher.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, fmt.Errorf("decrypt payload: %w", err) + } + return plaintext, nil +} diff --git a/kv/internal/crypto_test.go b/kv/internal/crypto_test.go new file mode 100644 index 0000000000..9570fc97ee --- /dev/null +++ b/kv/internal/crypto_test.go @@ -0,0 +1,106 @@ +package internal + +import ( + "bytes" + "testing" +) + +func TestGenerateDEK(t *testing.T) { + dek, err := generateDEK() + if err != nil { + t.Fatalf("generateDEK returned error: %v", err) + } + if len(dek) != aes256KeySize { + t.Fatalf("unexpected DEK length: got %d, want %d", len(dek), aes256KeySize) + } +} + +func TestWrapUnwrapDEK(t *testing.T) { + masterKey := bytes.Repeat([]byte{0x11}, aes256KeySize) + dek := bytes.Repeat([]byte{0x22}, aes256KeySize) + + wrappedDEK, err := wrapDEK(masterKey, dek) + if err != nil { + t.Fatalf("wrapDEK returned error: %v", err) + } + if bytes.Equal(wrappedDEK, dek) { + t.Fatalf("wrapped DEK must not equal plaintext DEK") + } + + unwrappedDEK, err := unwrapDEK(masterKey, wrappedDEK) + if err != nil { + t.Fatalf("unwrapDEK returned error: %v", err) + } + if !bytes.Equal(unwrappedDEK, dek) { + t.Fatalf("unwrapped DEK mismatch: got %x, want %x", unwrappedDEK, dek) + } +} + +func TestEncryptDecryptSecret(t *testing.T) { + dek := bytes.Repeat([]byte{0x33}, aes256KeySize) + plaintextSecret := []byte("super-secret-value") + + encryptedSecret, err := encryptSecret(dek, plaintextSecret) + if err != nil { + t.Fatalf("encryptSecret returned error: %v", err) + } + if bytes.Equal(encryptedSecret, plaintextSecret) { + t.Fatalf("encrypted secret must not equal plaintext") + } + + decryptedSecret, err := decryptSecret(dek, encryptedSecret) + if err != nil { + t.Fatalf("decryptSecret returned error: %v", err) + } + if !bytes.Equal(decryptedSecret, plaintextSecret) { + t.Fatalf("decrypted secret mismatch: got %q, want %q", decryptedSecret, plaintextSecret) + } +} + +func TestDecryptSecretInvalidCases(t *testing.T) { + dek := bytes.Repeat([]byte{0x44}, aes256KeySize) + validPayload, err := encryptSecret(dek, []byte("value")) + if err != nil { + t.Fatalf("encryptSecret returned error: %v", err) + } + + tests := []struct { + name string + key []byte + payload []byte + }{ + { + name: "invalid key length", + key: []byte("short"), + payload: validPayload, + }, + { + name: "payload too short", + key: dek, + payload: []byte{secretCryptoVersionV1}, + }, + { + name: "unsupported version", + key: dek, + payload: append([]byte{2}, validPayload[1:]...), + }, + { + name: "tampered payload", + key: dek, + payload: func() []byte { + copied := append([]byte(nil), validPayload...) + copied[len(copied)-1] ^= 0xff + return copied + }(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := decryptSecret(tt.key, tt.payload) + if err == nil { + t.Fatalf("expected error, got nil") + } + }) + } +} From 989478eb00b8b8da2908085d744d94612c426ce2 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Wed, 13 May 2026 11:40:40 +0200 Subject: [PATCH 03/24] feat(kv): add secret encoding metadata and DEK-based source/server persistence --- kv/internal/internal.go | 149 +++++- kv/internal/internal.pb.go | 863 ++++++++++++++++++++--------------- kv/internal/internal.proto | 12 +- kv/internal/internal_test.go | 116 +++++ 4 files changed, 758 insertions(+), 382 deletions(-) diff --git a/kv/internal/internal.go b/kv/internal/internal.go index 59850bf048..c5e86b81da 100644 --- a/kv/internal/internal.go +++ b/kv/internal/internal.go @@ -1,8 +1,10 @@ package internal import ( + "encoding/base64" "encoding/json" "fmt" + "sync" "github.com/influxdata/chronograf" "google.golang.org/protobuf/proto" @@ -10,6 +12,81 @@ import ( //go:generate protoc --go_out=. internal.proto +var ( + secretDEKMu sync.RWMutex + secretDEK []byte +) + +// SetSecretDEK configures the DEK used for secret field persistence. +// Pass nil or empty to disable encryption and persist plaintext. +func SetSecretDEK(dek []byte) { + secretDEKMu.Lock() + defer secretDEKMu.Unlock() + + if len(dek) == 0 { + secretDEK = nil + return + } + + secretDEK = append([]byte(nil), dek...) +} + +func currentSecretDEK() []byte { + secretDEKMu.RLock() + defer secretDEKMu.RUnlock() + + if len(secretDEK) == 0 { + return nil + } + + return append([]byte(nil), secretDEK...) +} + +func marshalSecretValue(fieldName, plaintext string, dek []byte) (string, SecretEncoding, error) { + if plaintext == "" { + return "", SecretEncoding_PLAINTEXT, nil + } + if len(dek) == 0 { + return plaintext, SecretEncoding_PLAINTEXT, nil + } + + encryptedPayload, err := encryptSecret(dek, []byte(plaintext)) + if err != nil { + return "", SecretEncoding_PLAINTEXT, fmt.Errorf("%s: %w", fieldName, err) + } + + return base64.StdEncoding.EncodeToString(encryptedPayload), SecretEncoding_ENCRYPTED_V1, nil +} + +func unmarshalSecretValue(fieldName, stored string, encoding SecretEncoding, dek []byte) (string, error) { + if stored == "" { + return "", nil + } + + switch encoding { + case SecretEncoding_PLAINTEXT: + return stored, nil + case SecretEncoding_ENCRYPTED_V1: + if len(dek) == 0 { + return "", fmt.Errorf("%s: encrypted value requires configured DEK", fieldName) + } + + encryptedPayload, err := base64.StdEncoding.DecodeString(stored) + if err != nil { + return "", fmt.Errorf("%s: decode encrypted value: %w", fieldName, err) + } + + plaintext, err := decryptSecret(dek, encryptedPayload) + if err != nil { + return "", fmt.Errorf("%s: %w", fieldName, err) + } + + return string(plaintext), nil + default: + return "", fmt.Errorf("%s: unsupported secret encoding %d", fieldName, encoding) + } +} + // MarshalBuild encodes a build to binary protobuf format. func MarshalBuild(b chronograf.BuildInfo) ([]byte, error) { return proto.Marshal(&BuildInfo{ @@ -32,13 +109,34 @@ func UnmarshalBuild(data []byte, b *chronograf.BuildInfo) error { // MarshalSource encodes a source to binary protobuf format. func MarshalSource(s chronograf.Source) ([]byte, error) { + dek := currentSecretDEK() + + password, passwordEncoding, err := marshalSecretValue("Source.Password", s.Password, dek) + if err != nil { + return nil, err + } + sharedSecret, sharedSecretEncoding, err := marshalSecretValue("Source.SharedSecret", s.SharedSecret, dek) + if err != nil { + return nil, err + } + managementToken, managementTokenEncoding, err := marshalSecretValue("Source.ManagementToken", s.ManagementToken, dek) + if err != nil { + return nil, err + } + databaseToken, databaseTokenEncoding, err := marshalSecretValue("Source.DatabaseToken", s.DatabaseToken, dek) + if err != nil { + return nil, err + } + return proto.Marshal(&Source{ ID: int64(s.ID), Name: s.Name, Type: s.Type, Username: s.Username, - Password: s.Password, - SharedSecret: s.SharedSecret, + Password: password, + PasswordEncoding: passwordEncoding, + SharedSecret: sharedSecret, + SharedSecretEncoding: sharedSecretEncoding, URL: s.URL, MetaURL: s.MetaURL, InsecureSkipVerify: s.InsecureSkipVerify, @@ -50,8 +148,10 @@ func MarshalSource(s chronograf.Source) ([]byte, error) { Version: s.Version, ClusterID: s.ClusterID, AccountID: s.AccountID, - ManagementToken: s.ManagementToken, - DatabaseToken: s.DatabaseToken, + ManagementToken: managementToken, + ManagementTokenEncoding: managementTokenEncoding, + DatabaseToken: databaseToken, + DatabaseTokenEncoding: databaseTokenEncoding, TagsCSVPath: s.TagsCSVPath, DefaultDatabase: s.DefaultDB, }) @@ -63,13 +163,31 @@ func UnmarshalSource(data []byte, s *chronograf.Source) error { if err := proto.Unmarshal(data, &pb); err != nil { return err } + dek := currentSecretDEK() + + password, err := unmarshalSecretValue("Source.Password", pb.Password, pb.GetPasswordEncoding(), dek) + if err != nil { + return err + } + sharedSecret, err := unmarshalSecretValue("Source.SharedSecret", pb.SharedSecret, pb.GetSharedSecretEncoding(), dek) + if err != nil { + return err + } + managementToken, err := unmarshalSecretValue("Source.ManagementToken", pb.ManagementToken, pb.GetManagementTokenEncoding(), dek) + if err != nil { + return err + } + databaseToken, err := unmarshalSecretValue("Source.DatabaseToken", pb.DatabaseToken, pb.GetDatabaseTokenEncoding(), dek) + if err != nil { + return err + } s.ID = int(pb.ID) s.Name = pb.Name s.Type = pb.Type s.Username = pb.Username - s.Password = pb.Password - s.SharedSecret = pb.SharedSecret + s.Password = password + s.SharedSecret = sharedSecret s.URL = pb.URL s.MetaURL = pb.MetaURL s.InsecureSkipVerify = pb.InsecureSkipVerify @@ -81,8 +199,8 @@ func UnmarshalSource(data []byte, s *chronograf.Source) error { s.Version = pb.Version s.ClusterID = pb.ClusterID s.AccountID = pb.AccountID - s.ManagementToken = pb.ManagementToken - s.DatabaseToken = pb.DatabaseToken + s.ManagementToken = managementToken + s.DatabaseToken = databaseToken s.TagsCSVPath = pb.TagsCSVPath s.DefaultDB = pb.DefaultDatabase return nil @@ -94,6 +212,11 @@ func MarshalServer(s chronograf.Server) ([]byte, error) { metadata []byte err error ) + dek := currentSecretDEK() + password, passwordEncoding, err := marshalSecretValue("Server.Password", s.Password, dek) + if err != nil { + return nil, err + } metadata, err = json.Marshal(s.Metadata) if err != nil { return nil, err @@ -103,7 +226,8 @@ func MarshalServer(s chronograf.Server) ([]byte, error) { SrcID: int64(s.SrcID), Name: s.Name, Username: s.Username, - Password: s.Password, + Password: password, + PasswordEncoding: passwordEncoding, URL: s.URL, Active: s.Active, Organization: s.Organization, @@ -119,6 +243,11 @@ func UnmarshalServer(data []byte, s *chronograf.Server) error { if err := proto.Unmarshal(data, &pb); err != nil { return err } + dek := currentSecretDEK() + password, err := unmarshalSecretValue("Server.Password", pb.Password, pb.GetPasswordEncoding(), dek) + if err != nil { + return err + } s.Metadata = make(map[string]interface{}) if len(pb.MetadataJSON) > 0 { @@ -131,7 +260,7 @@ func UnmarshalServer(data []byte, s *chronograf.Server) error { s.SrcID = int(pb.SrcID) s.Name = pb.Name s.Username = pb.Username - s.Password = pb.Password + s.Password = password s.URL = pb.URL s.Active = pb.Active s.Organization = pb.Organization diff --git a/kv/internal/internal.pb.go b/kv/internal/internal.pb.go index 4afb75ee5f..3b7e63349b 100644 --- a/kv/internal/internal.pb.go +++ b/kv/internal/internal.pb.go @@ -20,32 +20,82 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type SecretEncoding int32 + +const ( + SecretEncoding_PLAINTEXT SecretEncoding = 0 + SecretEncoding_ENCRYPTED_V1 SecretEncoding = 1 +) + +// Enum value maps for SecretEncoding. +var ( + SecretEncoding_name = map[int32]string{ + 0: "PLAINTEXT", + 1: "ENCRYPTED_V1", + } + SecretEncoding_value = map[string]int32{ + "PLAINTEXT": 0, + "ENCRYPTED_V1": 1, + } +) + +func (x SecretEncoding) Enum() *SecretEncoding { + p := new(SecretEncoding) + *p = x + return p +} + +func (x SecretEncoding) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SecretEncoding) Descriptor() protoreflect.EnumDescriptor { + return file_internal_proto_enumTypes[0].Descriptor() +} + +func (SecretEncoding) Type() protoreflect.EnumType { + return &file_internal_proto_enumTypes[0] +} + +func (x SecretEncoding) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SecretEncoding.Descriptor instead. +func (SecretEncoding) EnumDescriptor() ([]byte, []int) { + return file_internal_proto_rawDescGZIP(), []int{0} +} + type Source struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` // ID is the unique ID of the source - Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"` // Name is the user-defined name for the source - Type string `protobuf:"bytes,3,opt,name=Type,proto3" json:"Type,omitempty"` // Type specifies which kinds of source (enterprise vs oss) - Username string `protobuf:"bytes,4,opt,name=Username,proto3" json:"Username,omitempty"` // Username is the username to connect to the source - Password string `protobuf:"bytes,5,opt,name=Password,proto3" json:"Password,omitempty"` - URL string `protobuf:"bytes,6,opt,name=URL,proto3" json:"URL,omitempty"` // URL are the connections to the source - Default bool `protobuf:"varint,7,opt,name=Default,proto3" json:"Default,omitempty"` // Flags an source as the default. - Telegraf string `protobuf:"bytes,8,opt,name=Telegraf,proto3" json:"Telegraf,omitempty"` // Telegraf is the db telegraf is written to. By default it is "telegraf" - InsecureSkipVerify bool `protobuf:"varint,9,opt,name=InsecureSkipVerify,proto3" json:"InsecureSkipVerify,omitempty"` // InsecureSkipVerify accepts any certificate from the influx server - MetaURL string `protobuf:"bytes,10,opt,name=MetaURL,proto3" json:"MetaURL,omitempty"` // MetaURL is the connection URL for the meta node. - SharedSecret string `protobuf:"bytes,11,opt,name=SharedSecret,proto3" json:"SharedSecret,omitempty"` // SharedSecret signs the optional InfluxDB JWT Authorization - Organization string `protobuf:"bytes,12,opt,name=Organization,proto3" json:"Organization,omitempty"` // Organization is the organization ID that resource belongs to - Role string `protobuf:"bytes,13,opt,name=Role,proto3" json:"Role,omitempty"` // Role is the name of the miniumum role that a user must possess to access the resource - DefaultRP string `protobuf:"bytes,14,opt,name=DefaultRP,proto3" json:"DefaultRP,omitempty"` // DefaultRP is the default retention policy used in database queries to this source - Version string `protobuf:"bytes,15,opt,name=Version,proto3" json:"Version,omitempty"` // Version of the InfluxDB or Unknown - ClusterID string `protobuf:"bytes,16,opt,name=ClusterID,proto3" json:"ClusterID,omitempty"` // Cluster ID of an InfluxDB Cloud Dedicated source - AccountID string `protobuf:"bytes,17,opt,name=AccountID,proto3" json:"AccountID,omitempty"` // Account ID of an InfluxDB Cloud Dedicated source - ManagementToken string `protobuf:"bytes,18,opt,name=ManagementToken,proto3" json:"ManagementToken,omitempty"` // Management token of an InfluxDB Cloud Dedicated source - DatabaseToken string `protobuf:"bytes,19,opt,name=DatabaseToken,proto3" json:"DatabaseToken,omitempty"` // Database token of an InfluxDB Cloud Dedicated or other InfluxDB 3 source - TagsCSVPath string `protobuf:"bytes,20,opt,name=TagsCSVPath,proto3" json:"TagsCSVPath,omitempty"` // TagsCSVPath is the path to a directory containing CSV files (per db) with tags for the source - DefaultDatabase string `protobuf:"bytes,21,opt,name=DefaultDatabase,proto3" json:"DefaultDatabase,omitempty"` // DefaultDatabase is the default database used in queries for InfluxDB Cloud Dedicated when database list is not available + ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` // ID is the unique ID of the source + Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"` // Name is the user-defined name for the source + Type string `protobuf:"bytes,3,opt,name=Type,proto3" json:"Type,omitempty"` // Type specifies which kinds of source (enterprise vs oss) + Username string `protobuf:"bytes,4,opt,name=Username,proto3" json:"Username,omitempty"` // Username is the username to connect to the source + Password string `protobuf:"bytes,5,opt,name=Password,proto3" json:"Password,omitempty"` + URL string `protobuf:"bytes,6,opt,name=URL,proto3" json:"URL,omitempty"` // URL are the connections to the source + Default bool `protobuf:"varint,7,opt,name=Default,proto3" json:"Default,omitempty"` // Flags an source as the default. + Telegraf string `protobuf:"bytes,8,opt,name=Telegraf,proto3" json:"Telegraf,omitempty"` // Telegraf is the db telegraf is written to. By default it is "telegraf" + InsecureSkipVerify bool `protobuf:"varint,9,opt,name=InsecureSkipVerify,proto3" json:"InsecureSkipVerify,omitempty"` // InsecureSkipVerify accepts any certificate from the influx server + MetaURL string `protobuf:"bytes,10,opt,name=MetaURL,proto3" json:"MetaURL,omitempty"` // MetaURL is the connection URL for the meta node. + SharedSecret string `protobuf:"bytes,11,opt,name=SharedSecret,proto3" json:"SharedSecret,omitempty"` // SharedSecret signs the optional InfluxDB JWT Authorization + Organization string `protobuf:"bytes,12,opt,name=Organization,proto3" json:"Organization,omitempty"` // Organization is the organization ID that resource belongs to + Role string `protobuf:"bytes,13,opt,name=Role,proto3" json:"Role,omitempty"` // Role is the name of the miniumum role that a user must possess to access the resource + DefaultRP string `protobuf:"bytes,14,opt,name=DefaultRP,proto3" json:"DefaultRP,omitempty"` // DefaultRP is the default retention policy used in database queries to this source + Version string `protobuf:"bytes,15,opt,name=Version,proto3" json:"Version,omitempty"` // Version of the InfluxDB or Unknown + ClusterID string `protobuf:"bytes,16,opt,name=ClusterID,proto3" json:"ClusterID,omitempty"` // Cluster ID of an InfluxDB Cloud Dedicated source + AccountID string `protobuf:"bytes,17,opt,name=AccountID,proto3" json:"AccountID,omitempty"` // Account ID of an InfluxDB Cloud Dedicated source + ManagementToken string `protobuf:"bytes,18,opt,name=ManagementToken,proto3" json:"ManagementToken,omitempty"` // Management token of an InfluxDB Cloud Dedicated source + DatabaseToken string `protobuf:"bytes,19,opt,name=DatabaseToken,proto3" json:"DatabaseToken,omitempty"` // Database token of an InfluxDB Cloud Dedicated or other InfluxDB 3 source + TagsCSVPath string `protobuf:"bytes,20,opt,name=TagsCSVPath,proto3" json:"TagsCSVPath,omitempty"` // TagsCSVPath is the path to a directory containing CSV files (per db) with tags for the source + DefaultDatabase string `protobuf:"bytes,21,opt,name=DefaultDatabase,proto3" json:"DefaultDatabase,omitempty"` // DefaultDatabase is the default database used in queries for InfluxDB Cloud Dedicated when database list is not available + PasswordEncoding SecretEncoding `protobuf:"varint,22,opt,name=PasswordEncoding,proto3,enum=internal.SecretEncoding" json:"PasswordEncoding,omitempty"` // Encoding state for Password + SharedSecretEncoding SecretEncoding `protobuf:"varint,23,opt,name=SharedSecretEncoding,proto3,enum=internal.SecretEncoding" json:"SharedSecretEncoding,omitempty"` // Encoding state for SharedSecret + ManagementTokenEncoding SecretEncoding `protobuf:"varint,24,opt,name=ManagementTokenEncoding,proto3,enum=internal.SecretEncoding" json:"ManagementTokenEncoding,omitempty"` // Encoding state for ManagementToken + DatabaseTokenEncoding SecretEncoding `protobuf:"varint,25,opt,name=DatabaseTokenEncoding,proto3,enum=internal.SecretEncoding" json:"DatabaseTokenEncoding,omitempty"` // Encoding state for DatabaseToken } func (x *Source) Reset() { @@ -227,6 +277,34 @@ func (x *Source) GetDefaultDatabase() string { return "" } +func (x *Source) GetPasswordEncoding() SecretEncoding { + if x != nil { + return x.PasswordEncoding + } + return SecretEncoding_PLAINTEXT +} + +func (x *Source) GetSharedSecretEncoding() SecretEncoding { + if x != nil { + return x.SharedSecretEncoding + } + return SecretEncoding_PLAINTEXT +} + +func (x *Source) GetManagementTokenEncoding() SecretEncoding { + if x != nil { + return x.ManagementTokenEncoding + } + return SecretEncoding_PLAINTEXT +} + +func (x *Source) GetDatabaseTokenEncoding() SecretEncoding { + if x != nil { + return x.DatabaseTokenEncoding + } + return SecretEncoding_PLAINTEXT +} + type Dashboard struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1165,17 +1243,18 @@ type Server struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` // ID is the unique ID of the server - Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"` // Name is the user-defined name for the server - Username string `protobuf:"bytes,3,opt,name=Username,proto3" json:"Username,omitempty"` // Username is the username to connect to the server - Password string `protobuf:"bytes,4,opt,name=Password,proto3" json:"Password,omitempty"` - URL string `protobuf:"bytes,5,opt,name=URL,proto3" json:"URL,omitempty"` // URL is the path to the server - SrcID int64 `protobuf:"varint,6,opt,name=SrcID,proto3" json:"SrcID,omitempty"` // SrcID is the ID of the data source - Active bool `protobuf:"varint,7,opt,name=Active,proto3" json:"Active,omitempty"` // is this the currently active server for the source - Organization string `protobuf:"bytes,8,opt,name=Organization,proto3" json:"Organization,omitempty"` // Organization is the organization ID that resource belongs to - InsecureSkipVerify bool `protobuf:"varint,9,opt,name=InsecureSkipVerify,proto3" json:"InsecureSkipVerify,omitempty"` // InsecureSkipVerify accepts any certificate from the client - Type string `protobuf:"bytes,10,opt,name=Type,proto3" json:"Type,omitempty"` // Type is the kind of the server (e.g. flux) - MetadataJSON string `protobuf:"bytes,11,opt,name=MetadataJSON,proto3" json:"MetadataJSON,omitempty"` // JSON byte representation of the metadata + ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` // ID is the unique ID of the server + Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"` // Name is the user-defined name for the server + Username string `protobuf:"bytes,3,opt,name=Username,proto3" json:"Username,omitempty"` // Username is the username to connect to the server + Password string `protobuf:"bytes,4,opt,name=Password,proto3" json:"Password,omitempty"` + URL string `protobuf:"bytes,5,opt,name=URL,proto3" json:"URL,omitempty"` // URL is the path to the server + SrcID int64 `protobuf:"varint,6,opt,name=SrcID,proto3" json:"SrcID,omitempty"` // SrcID is the ID of the data source + Active bool `protobuf:"varint,7,opt,name=Active,proto3" json:"Active,omitempty"` // is this the currently active server for the source + Organization string `protobuf:"bytes,8,opt,name=Organization,proto3" json:"Organization,omitempty"` // Organization is the organization ID that resource belongs to + InsecureSkipVerify bool `protobuf:"varint,9,opt,name=InsecureSkipVerify,proto3" json:"InsecureSkipVerify,omitempty"` // InsecureSkipVerify accepts any certificate from the client + Type string `protobuf:"bytes,10,opt,name=Type,proto3" json:"Type,omitempty"` // Type is the kind of the server (e.g. flux) + MetadataJSON string `protobuf:"bytes,11,opt,name=MetadataJSON,proto3" json:"MetadataJSON,omitempty"` // JSON byte representation of the metadata + PasswordEncoding SecretEncoding `protobuf:"varint,12,opt,name=PasswordEncoding,proto3,enum=internal.SecretEncoding" json:"PasswordEncoding,omitempty"` // Encoding state for Password } func (x *Server) Reset() { @@ -1287,6 +1366,13 @@ func (x *Server) GetMetadataJSON() string { return "" } +func (x *Server) GetPasswordEncoding() SecretEncoding { + if x != nil { + return x.PasswordEncoding + } + return SecretEncoding_PLAINTEXT +} + type Layout struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2466,7 +2552,7 @@ var File_internal_proto protoreflect.FileDescriptor var file_internal_proto_rawDesc = []byte{ 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x12, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x22, 0xf6, 0x04, 0x0a, 0x06, 0x53, + 0x12, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x22, 0xae, 0x07, 0x0a, 0x06, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, @@ -2506,286 +2592,313 @@ var file_internal_proto_rawDesc = []byte{ 0x67, 0x73, 0x43, 0x53, 0x56, 0x50, 0x61, 0x74, 0x68, 0x12, 0x28, 0x0a, 0x0f, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x44, 0x61, 0x74, 0x61, 0x62, - 0x61, 0x73, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, - 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x49, - 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x43, 0x65, 0x6c, 0x6c, 0x52, 0x05, 0x63, - 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x30, 0x0a, 0x09, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, - 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x52, 0x09, 0x74, 0x65, 0x6d, - 0x70, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, - 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x92, 0x05, 0x0a, 0x0d, 0x44, - 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x43, 0x65, 0x6c, 0x6c, 0x12, 0x0c, 0x0a, 0x01, - 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x78, 0x12, 0x0c, 0x0a, 0x01, 0x79, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x79, 0x12, 0x0c, 0x0a, 0x01, 0x77, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x01, 0x77, 0x12, 0x0c, 0x0a, 0x01, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x01, 0x68, 0x12, 0x29, 0x0a, 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, - 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x35, 0x0a, 0x04, 0x61, 0x78, 0x65, 0x73, 0x18, - 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x43, 0x65, 0x6c, 0x6c, 0x2e, 0x41, - 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x61, 0x78, 0x65, 0x73, 0x12, 0x27, - 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x52, - 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, 0x12, 0x28, 0x0a, 0x06, 0x6c, 0x65, 0x67, 0x65, 0x6e, - 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x2e, 0x4c, 0x65, 0x67, 0x65, 0x6e, 0x64, 0x52, 0x06, 0x6c, 0x65, 0x67, 0x65, 0x6e, - 0x64, 0x12, 0x3a, 0x0a, 0x0c, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, - 0x0c, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3c, 0x0a, - 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0d, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, - 0x65, 0x6e, 0x61, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x0c, 0x66, - 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x74, - 0x69, 0x6d, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x3d, 0x0a, 0x0d, 0x64, - 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x65, - 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x52, 0x0d, 0x64, 0x65, 0x63, - 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x6f, - 0x74, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x12, 0x26, - 0x0a, 0x0e, 0x6e, 0x6f, 0x74, 0x65, 0x56, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, - 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x6f, 0x74, 0x65, 0x56, 0x69, 0x73, 0x69, - 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x1a, 0x47, 0x0a, 0x09, 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x41, 0x78, 0x69, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, - 0x47, 0x0a, 0x0d, 0x44, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x73, - 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x73, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x64, - 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x22, 0xbc, 0x01, 0x0a, 0x0c, 0x54, 0x61, 0x62, - 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x76, 0x65, 0x72, - 0x74, 0x69, 0x63, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x41, 0x78, 0x69, 0x73, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x10, 0x76, 0x65, 0x72, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x54, 0x69, 0x6d, - 0x65, 0x41, 0x78, 0x69, 0x73, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, - 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x72, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x72, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x66, 0x69, 0x78, 0x46, 0x69, 0x72, 0x73, 0x74, 0x43, - 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x66, 0x69, 0x78, - 0x46, 0x69, 0x72, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x4a, 0x04, 0x08, 0x01, 0x10, - 0x02, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x22, 0x70, 0x0a, 0x0e, 0x52, 0x65, 0x6e, 0x61, 0x6d, - 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0c, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, - 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x18, 0x0a, 0x07, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x07, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x22, 0x67, 0x0a, 0x05, 0x43, 0x6f, 0x6c, - 0x6f, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x48, 0x65, 0x78, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x48, 0x65, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x22, 0x3e, 0x0a, 0x06, 0x4c, 0x65, 0x67, 0x65, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, - 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x20, 0x0a, 0x0b, 0x4f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x4f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0xb2, 0x01, 0x0a, 0x04, 0x41, 0x78, 0x69, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x6c, - 0x65, 0x67, 0x61, 0x63, 0x79, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x03, 0x52, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x12, - 0x16, 0x0a, 0x06, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x06, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x16, 0x0a, - 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, - 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x75, 0x66, 0x66, 0x69, 0x78, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x75, 0x66, 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, - 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x62, 0x61, 0x73, - 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x22, 0xdb, 0x01, 0x0a, 0x08, 0x54, 0x65, 0x6d, 0x70, - 0x6c, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x49, 0x44, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x5f, 0x76, 0x61, 0x72, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x65, 0x6d, 0x70, 0x56, 0x61, 0x72, 0x12, - 0x2f, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, - 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x71, 0x75, - 0x65, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x51, 0x75, 0x65, - 0x72, 0x79, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x49, 0x44, 0x22, 0x67, 0x0a, 0x0d, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0xb5, - 0x01, 0x0a, 0x0d, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x51, 0x75, 0x65, 0x72, 0x79, - 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x64, 0x62, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x64, 0x62, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x70, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x72, 0x70, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x65, - 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x17, 0x0a, 0x07, - 0x74, 0x61, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, - 0x61, 0x67, 0x4b, 0x65, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6b, - 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4b, - 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x75, 0x78, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x66, 0x6c, 0x75, 0x78, 0x22, 0xb0, 0x02, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x49, - 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, - 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x10, 0x0a, - 0x03, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, - 0x14, 0x0a, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, - 0x53, 0x72, 0x63, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x22, 0x0a, - 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x2e, 0x0a, 0x12, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, 0x6b, 0x69, - 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x49, - 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, 0x6b, 0x69, 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, - 0x79, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x4a, 0x53, 0x4f, 0x4e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e, 0x22, 0x9e, 0x01, 0x0a, 0x06, 0x4c, 0x61, - 0x79, 0x6f, 0x75, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x49, 0x44, 0x12, 0x20, 0x0a, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x4d, 0x65, 0x61, - 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x05, 0x43, 0x65, 0x6c, 0x6c, - 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x2e, 0x43, 0x65, 0x6c, 0x6c, 0x52, 0x05, 0x43, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x1a, - 0x0a, 0x08, 0x41, 0x75, 0x74, 0x6f, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x08, 0x41, 0x75, 0x74, 0x6f, 0x66, 0x6c, 0x6f, 0x77, 0x22, 0xca, 0x02, 0x0a, 0x04, 0x43, - 0x65, 0x6c, 0x6c, 0x12, 0x0c, 0x0a, 0x01, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, - 0x78, 0x12, 0x0c, 0x0a, 0x01, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x79, 0x12, - 0x0c, 0x0a, 0x01, 0x77, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x77, 0x12, 0x0c, 0x0a, - 0x01, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x68, 0x12, 0x29, 0x0a, 0x07, 0x71, - 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x07, 0x71, - 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x12, 0x0c, 0x0a, 0x01, 0x69, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x01, 0x69, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x79, 0x72, 0x61, 0x6e, - 0x67, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x03, 0x52, 0x07, 0x79, 0x72, 0x61, 0x6e, 0x67, - 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x79, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x09, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x07, 0x79, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x12, 0x0a, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x12, 0x2c, 0x0a, 0x04, 0x61, 0x78, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x65, 0x6c, 0x6c, 0x2e, 0x41, - 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x61, 0x78, 0x65, 0x73, 0x1a, 0x47, + 0x61, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x10, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x45, + 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x16, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x45, + 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x10, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x4c, 0x0a, 0x14, 0x53, 0x68, 0x61, + 0x72, 0x65, 0x64, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, + 0x67, 0x18, 0x17, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, + 0x67, 0x52, 0x14, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x45, + 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x52, 0x0a, 0x17, 0x4d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, + 0x6e, 0x67, 0x18, 0x18, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, + 0x6e, 0x67, 0x52, 0x17, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x4e, 0x0a, 0x15, 0x44, + 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x63, 0x6f, + 0x64, 0x69, 0x6e, 0x67, 0x18, 0x19, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x45, 0x6e, 0x63, 0x6f, + 0x64, 0x69, 0x6e, 0x67, 0x52, 0x15, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x22, 0xb4, 0x01, 0x0a, 0x09, + 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, + 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, + 0x64, 0x43, 0x65, 0x6c, 0x6c, 0x52, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x30, 0x0a, 0x09, + 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x12, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x52, 0x09, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x12, 0x22, + 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x22, 0x92, 0x05, 0x0a, 0x0d, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, + 0x43, 0x65, 0x6c, 0x6c, 0x12, 0x0c, 0x0a, 0x01, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x01, 0x78, 0x12, 0x0c, 0x0a, 0x01, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x79, + 0x12, 0x0c, 0x0a, 0x01, 0x77, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x77, 0x12, 0x0c, + 0x0a, 0x01, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x68, 0x12, 0x29, 0x0a, 0x07, + 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x07, + 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, + 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, + 0x35, 0x0a, 0x04, 0x61, 0x78, 0x65, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, + 0x72, 0x64, 0x43, 0x65, 0x6c, 0x6c, 0x2e, 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x04, 0x61, 0x78, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, + 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, 0x12, + 0x28, 0x0a, 0x06, 0x6c, 0x65, 0x67, 0x65, 0x6e, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x10, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x4c, 0x65, 0x67, 0x65, 0x6e, + 0x64, 0x52, 0x06, 0x6c, 0x65, 0x67, 0x65, 0x6e, 0x64, 0x12, 0x3a, 0x0a, 0x0c, 0x74, 0x61, 0x62, + 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x16, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, + 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0c, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3c, 0x0a, 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x61, 0x62, 0x6c, 0x65, + 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, + 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x46, 0x6f, 0x72, + 0x6d, 0x61, 0x74, 0x12, 0x3d, 0x0a, 0x0d, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, + 0x61, 0x63, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, + 0x63, 0x65, 0x73, 0x52, 0x0d, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, + 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x6f, 0x74, 0x65, 0x56, 0x69, + 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, + 0x6e, 0x6f, 0x74, 0x65, 0x56, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x1a, 0x47, 0x0a, 0x09, 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x41, 0x78, 0x69, 0x73, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x8b, 0x02, 0x0a, 0x05, 0x51, 0x75, 0x65, 0x72, - 0x79, 0x12, 0x18, 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x44, - 0x42, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x44, 0x42, 0x12, 0x0e, 0x0a, 0x02, 0x52, - 0x50, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x52, 0x50, 0x12, 0x1a, 0x0a, 0x08, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x57, 0x68, 0x65, 0x72, 0x65, - 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x57, 0x68, 0x65, 0x72, 0x65, 0x73, 0x12, - 0x14, 0x0a, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x25, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, - 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x53, 0x68, 0x69, 0x66, 0x74, 0x73, 0x18, 0x09, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x53, 0x68, 0x69, 0x66, 0x74, 0x52, 0x06, 0x53, 0x68, 0x69, 0x66, 0x74, - 0x73, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x54, 0x79, 0x70, 0x65, 0x22, 0x51, 0x0a, 0x09, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x68, 0x69, - 0x66, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x55, 0x6e, 0x69, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x55, 0x6e, 0x69, 0x74, 0x12, 0x1a, 0x0a, 0x08, - 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x33, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, - 0x65, 0x12, 0x14, 0x0a, 0x05, 0x55, 0x70, 0x70, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x05, 0x55, 0x70, 0x70, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x6f, 0x77, 0x65, 0x72, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x4c, 0x6f, 0x77, 0x65, 0x72, 0x22, 0x5d, 0x0a, - 0x09, 0x41, 0x6c, 0x65, 0x72, 0x74, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4a, 0x53, - 0x4f, 0x4e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4a, 0x53, 0x4f, 0x4e, 0x12, 0x14, - 0x0a, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x53, - 0x72, 0x63, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x4b, 0x61, 0x70, 0x61, 0x49, 0x44, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4b, 0x61, 0x70, 0x61, 0x49, 0x44, 0x22, 0xa4, 0x01, 0x0a, - 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x24, 0x0a, - 0x05, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x05, 0x52, 0x6f, - 0x6c, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x53, 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, - 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x53, 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, - 0x6d, 0x69, 0x6e, 0x22, 0x3e, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4f, - 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, - 0x61, 0x6d, 0x65, 0x22, 0xa5, 0x01, 0x0a, 0x07, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, - 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x53, - 0x63, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x63, 0x68, - 0x65, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4f, - 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x14, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x61, 0x6e, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, - 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x54, 0x0a, 0x0c, 0x4f, - 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x49, - 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, - 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x20, 0x0a, 0x0b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x6f, 0x6c, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x6f, 0x6c, - 0x65, 0x22, 0x32, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, 0x0a, 0x04, 0x41, - 0x75, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x04, 0x41, 0x75, 0x74, 0x68, 0x22, 0x3c, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x2e, 0x0a, 0x12, 0x53, 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, - 0x6e, 0x4e, 0x65, 0x77, 0x55, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x12, 0x53, 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x4e, 0x65, 0x77, 0x55, 0x73, - 0x65, 0x72, 0x73, 0x22, 0x75, 0x0a, 0x12, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x4f, 0x72, 0x67, - 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, - 0x44, 0x12, 0x37, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x09, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x22, 0x46, 0x0a, 0x0f, 0x4c, 0x6f, - 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x33, 0x0a, - 0x07, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, - 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x52, 0x07, 0x43, 0x6f, 0x6c, 0x75, 0x6d, - 0x6e, 0x73, 0x22, 0x79, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, - 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x73, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x50, 0x6f, 0x73, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x09, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, - 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, - 0x6e, 0x67, 0x52, 0x09, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x4e, 0x0a, - 0x0e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x12, - 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, - 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3d, 0x0a, - 0x09, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x56, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x42, 0x0c, 0x5a, 0x0a, - 0x2e, 0x3b, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x47, 0x0a, 0x0d, 0x44, 0x65, 0x63, 0x69, 0x6d, + 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x73, 0x45, 0x6e, + 0x66, 0x6f, 0x72, 0x63, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, + 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x69, + 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, + 0x22, 0xbc, 0x01, 0x0a, 0x0c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x76, 0x65, 0x72, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x54, 0x69, 0x6d, + 0x65, 0x41, 0x78, 0x69, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x76, 0x65, 0x72, + 0x74, 0x69, 0x63, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x41, 0x78, 0x69, 0x73, 0x12, 0x30, 0x0a, + 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x61, 0x62, + 0x6c, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, + 0x1a, 0x0a, 0x08, 0x77, 0x72, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x77, 0x72, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x66, + 0x69, 0x78, 0x46, 0x69, 0x72, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0e, 0x66, 0x69, 0x78, 0x46, 0x69, 0x72, 0x73, 0x74, 0x43, 0x6f, 0x6c, + 0x75, 0x6d, 0x6e, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x22, + 0x70, 0x0a, 0x0e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x65, 0x6c, + 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x4e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, + 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, + 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x69, 0x73, 0x69, 0x62, + 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, + 0x65, 0x22, 0x67, 0x0a, 0x05, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, + 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x10, + 0x0a, 0x03, 0x48, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x48, 0x65, 0x78, + 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x3e, 0x0a, 0x06, 0x4c, 0x65, + 0x67, 0x65, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x4f, 0x72, 0x69, 0x65, + 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x4f, + 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xb2, 0x01, 0x0a, 0x04, 0x41, + 0x78, 0x69, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x42, 0x6f, 0x75, + 0x6e, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, + 0x79, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x12, + 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x75, 0x66, 0x66, 0x69, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x75, 0x66, 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x61, + 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x22, + 0xdb, 0x01, 0x0a, 0x08, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, + 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x19, 0x0a, 0x08, + 0x74, 0x65, 0x6d, 0x70, 0x5f, 0x76, 0x61, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x74, 0x65, 0x6d, 0x70, 0x56, 0x61, 0x72, 0x12, 0x2f, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, + 0x65, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x65, 0x6d, + 0x70, 0x6c, 0x61, 0x74, 0x65, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, + 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x22, 0x67, 0x0a, + 0x0d, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, + 0x63, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, + 0x63, 0x74, 0x65, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x54, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x64, 0x62, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x64, 0x62, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x72, 0x70, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x67, 0x4b, 0x65, 0x79, 0x12, 0x1b, 0x0a, + 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, + 0x75, 0x78, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, 0x75, 0x78, 0x22, 0xf6, + 0x02, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, + 0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, 0x12, 0x16, 0x0a, + 0x06, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x41, + 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, + 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x12, 0x49, 0x6e, 0x73, + 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, 0x6b, 0x69, 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, + 0x6b, 0x69, 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, + 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, + 0x0c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x53, 0x4f, + 0x4e, 0x12, 0x44, 0x0a, 0x10, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x45, 0x6e, 0x63, + 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x45, 0x6e, 0x63, + 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x10, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x45, + 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x22, 0x9e, 0x01, 0x0a, 0x06, 0x4c, 0x61, 0x79, 0x6f, + 0x75, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x49, 0x44, 0x12, 0x20, 0x0a, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x4d, 0x65, 0x61, 0x73, 0x75, + 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x05, 0x43, 0x65, 0x6c, 0x6c, 0x73, 0x18, + 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2e, 0x43, 0x65, 0x6c, 0x6c, 0x52, 0x05, 0x43, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x1a, 0x0a, 0x08, + 0x41, 0x75, 0x74, 0x6f, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, + 0x41, 0x75, 0x74, 0x6f, 0x66, 0x6c, 0x6f, 0x77, 0x22, 0xca, 0x02, 0x0a, 0x04, 0x43, 0x65, 0x6c, + 0x6c, 0x12, 0x0c, 0x0a, 0x01, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x78, 0x12, + 0x0c, 0x0a, 0x01, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x79, 0x12, 0x0c, 0x0a, + 0x01, 0x77, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x77, 0x12, 0x0c, 0x0a, 0x01, 0x68, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x68, 0x12, 0x29, 0x0a, 0x07, 0x71, 0x75, 0x65, + 0x72, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x07, 0x71, 0x75, 0x65, + 0x72, 0x69, 0x65, 0x73, 0x12, 0x0c, 0x0a, 0x01, 0x69, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x01, 0x69, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x79, 0x72, 0x61, 0x6e, 0x67, 0x65, + 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x03, 0x52, 0x07, 0x79, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, + 0x12, 0x18, 0x0a, 0x07, 0x79, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x07, 0x79, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2c, + 0x0a, 0x04, 0x61, 0x78, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x65, 0x6c, 0x6c, 0x2e, 0x41, 0x78, 0x65, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x61, 0x78, 0x65, 0x73, 0x1a, 0x47, 0x0a, 0x09, + 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x41, 0x78, 0x69, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x8b, 0x02, 0x0a, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, + 0x18, 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x44, 0x42, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x44, 0x42, 0x12, 0x0e, 0x0a, 0x02, 0x52, 0x50, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x52, 0x50, 0x12, 0x1a, 0x0a, 0x08, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x42, 0x79, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x42, 0x79, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x57, 0x68, 0x65, 0x72, 0x65, 0x73, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x57, 0x68, 0x65, 0x72, 0x65, 0x73, 0x12, 0x14, 0x0a, + 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4c, 0x61, + 0x62, 0x65, 0x6c, 0x12, 0x25, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x61, + 0x6e, 0x67, 0x65, 0x52, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x53, 0x68, 0x69, 0x66, 0x74, 0x73, 0x18, 0x09, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x53, 0x68, 0x69, 0x66, 0x74, 0x52, 0x06, 0x53, 0x68, 0x69, 0x66, 0x74, 0x73, 0x12, + 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, + 0x79, 0x70, 0x65, 0x22, 0x51, 0x0a, 0x09, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x68, 0x69, 0x66, 0x74, + 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x55, 0x6e, 0x69, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x55, 0x6e, 0x69, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x51, 0x75, + 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x51, 0x75, + 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x33, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, + 0x14, 0x0a, 0x05, 0x55, 0x70, 0x70, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, + 0x55, 0x70, 0x70, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x6f, 0x77, 0x65, 0x72, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x4c, 0x6f, 0x77, 0x65, 0x72, 0x22, 0x5d, 0x0a, 0x09, 0x41, + 0x6c, 0x65, 0x72, 0x74, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4a, 0x53, 0x4f, 0x4e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4a, 0x53, 0x4f, 0x4e, 0x12, 0x14, 0x0a, 0x05, + 0x53, 0x72, 0x63, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x53, 0x72, 0x63, + 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x4b, 0x61, 0x70, 0x61, 0x49, 0x44, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x06, 0x4b, 0x61, 0x70, 0x61, 0x49, 0x44, 0x22, 0xa4, 0x01, 0x0a, 0x04, 0x55, + 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x05, 0x52, + 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x05, 0x52, 0x6f, 0x6c, 0x65, + 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x53, 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x53, 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, + 0x6e, 0x22, 0x3e, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4f, 0x72, 0x67, + 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, + 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, + 0x65, 0x22, 0xa5, 0x01, 0x0a, 0x07, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x1a, 0x0a, + 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x63, 0x68, + 0x65, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, + 0x65, 0x12, 0x32, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4f, 0x72, 0x67, + 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x14, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, + 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x54, 0x0a, 0x0c, 0x4f, 0x72, 0x67, + 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, + 0x0b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x6f, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x6f, 0x6c, 0x65, 0x22, + 0x32, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, 0x0a, 0x04, 0x41, 0x75, 0x74, + 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x41, + 0x75, 0x74, 0x68, 0x22, 0x3c, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x2e, 0x0a, 0x12, 0x53, 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x4e, + 0x65, 0x77, 0x55, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x53, + 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x4e, 0x65, 0x77, 0x55, 0x73, 0x65, 0x72, + 0x73, 0x22, 0x75, 0x0a, 0x12, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x4f, 0x72, 0x67, 0x61, 0x6e, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, + 0x37, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x4c, 0x6f, + 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x4c, + 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x22, 0x46, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x56, + 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x33, 0x0a, 0x07, 0x43, + 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, + 0x72, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x52, 0x07, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, + 0x22, 0x79, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6c, + 0x75, 0x6d, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x73, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x50, 0x6f, 0x73, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x09, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, + 0x52, 0x09, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x4e, 0x0a, 0x0e, 0x43, + 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x12, 0x0a, + 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3d, 0x0a, 0x09, 0x42, + 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x2a, 0x31, 0x0a, 0x0e, 0x53, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x0d, 0x0a, 0x09, + 0x50, 0x4c, 0x41, 0x49, 0x4e, 0x54, 0x45, 0x58, 0x54, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x45, + 0x4e, 0x43, 0x52, 0x59, 0x50, 0x54, 0x45, 0x44, 0x5f, 0x56, 0x31, 0x10, 0x01, 0x42, 0x0c, 0x5a, + 0x0a, 0x2e, 0x3b, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -2800,71 +2913,78 @@ func file_internal_proto_rawDescGZIP() []byte { return file_internal_proto_rawDescData } +var file_internal_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_internal_proto_msgTypes = make([]protoimpl.MessageInfo, 32) var file_internal_proto_goTypes = []any{ - (*Source)(nil), // 0: internal.Source - (*Dashboard)(nil), // 1: internal.Dashboard - (*DashboardCell)(nil), // 2: internal.DashboardCell - (*DecimalPlaces)(nil), // 3: internal.DecimalPlaces - (*TableOptions)(nil), // 4: internal.TableOptions - (*RenamableField)(nil), // 5: internal.RenamableField - (*Color)(nil), // 6: internal.Color - (*Legend)(nil), // 7: internal.Legend - (*Axis)(nil), // 8: internal.Axis - (*Template)(nil), // 9: internal.Template - (*TemplateValue)(nil), // 10: internal.TemplateValue - (*TemplateQuery)(nil), // 11: internal.TemplateQuery - (*Server)(nil), // 12: internal.Server - (*Layout)(nil), // 13: internal.Layout - (*Cell)(nil), // 14: internal.Cell - (*Query)(nil), // 15: internal.Query - (*TimeShift)(nil), // 16: internal.TimeShift - (*Range)(nil), // 17: internal.Range - (*AlertRule)(nil), // 18: internal.AlertRule - (*User)(nil), // 19: internal.User - (*Role)(nil), // 20: internal.Role - (*Mapping)(nil), // 21: internal.Mapping - (*Organization)(nil), // 22: internal.Organization - (*Config)(nil), // 23: internal.Config - (*AuthConfig)(nil), // 24: internal.AuthConfig - (*OrganizationConfig)(nil), // 25: internal.OrganizationConfig - (*LogViewerConfig)(nil), // 26: internal.LogViewerConfig - (*LogViewerColumn)(nil), // 27: internal.LogViewerColumn - (*ColumnEncoding)(nil), // 28: internal.ColumnEncoding - (*BuildInfo)(nil), // 29: internal.BuildInfo - nil, // 30: internal.DashboardCell.AxesEntry - nil, // 31: internal.Cell.AxesEntry + (SecretEncoding)(0), // 0: internal.SecretEncoding + (*Source)(nil), // 1: internal.Source + (*Dashboard)(nil), // 2: internal.Dashboard + (*DashboardCell)(nil), // 3: internal.DashboardCell + (*DecimalPlaces)(nil), // 4: internal.DecimalPlaces + (*TableOptions)(nil), // 5: internal.TableOptions + (*RenamableField)(nil), // 6: internal.RenamableField + (*Color)(nil), // 7: internal.Color + (*Legend)(nil), // 8: internal.Legend + (*Axis)(nil), // 9: internal.Axis + (*Template)(nil), // 10: internal.Template + (*TemplateValue)(nil), // 11: internal.TemplateValue + (*TemplateQuery)(nil), // 12: internal.TemplateQuery + (*Server)(nil), // 13: internal.Server + (*Layout)(nil), // 14: internal.Layout + (*Cell)(nil), // 15: internal.Cell + (*Query)(nil), // 16: internal.Query + (*TimeShift)(nil), // 17: internal.TimeShift + (*Range)(nil), // 18: internal.Range + (*AlertRule)(nil), // 19: internal.AlertRule + (*User)(nil), // 20: internal.User + (*Role)(nil), // 21: internal.Role + (*Mapping)(nil), // 22: internal.Mapping + (*Organization)(nil), // 23: internal.Organization + (*Config)(nil), // 24: internal.Config + (*AuthConfig)(nil), // 25: internal.AuthConfig + (*OrganizationConfig)(nil), // 26: internal.OrganizationConfig + (*LogViewerConfig)(nil), // 27: internal.LogViewerConfig + (*LogViewerColumn)(nil), // 28: internal.LogViewerColumn + (*ColumnEncoding)(nil), // 29: internal.ColumnEncoding + (*BuildInfo)(nil), // 30: internal.BuildInfo + nil, // 31: internal.DashboardCell.AxesEntry + nil, // 32: internal.Cell.AxesEntry } var file_internal_proto_depIdxs = []int32{ - 2, // 0: internal.Dashboard.cells:type_name -> internal.DashboardCell - 9, // 1: internal.Dashboard.templates:type_name -> internal.Template - 15, // 2: internal.DashboardCell.queries:type_name -> internal.Query - 30, // 3: internal.DashboardCell.axes:type_name -> internal.DashboardCell.AxesEntry - 6, // 4: internal.DashboardCell.colors:type_name -> internal.Color - 7, // 5: internal.DashboardCell.legend:type_name -> internal.Legend - 4, // 6: internal.DashboardCell.tableOptions:type_name -> internal.TableOptions - 5, // 7: internal.DashboardCell.fieldOptions:type_name -> internal.RenamableField - 3, // 8: internal.DashboardCell.decimalPlaces:type_name -> internal.DecimalPlaces - 5, // 9: internal.TableOptions.sortBy:type_name -> internal.RenamableField - 10, // 10: internal.Template.values:type_name -> internal.TemplateValue - 11, // 11: internal.Template.query:type_name -> internal.TemplateQuery - 14, // 12: internal.Layout.Cells:type_name -> internal.Cell - 15, // 13: internal.Cell.queries:type_name -> internal.Query - 31, // 14: internal.Cell.axes:type_name -> internal.Cell.AxesEntry - 17, // 15: internal.Query.Range:type_name -> internal.Range - 16, // 16: internal.Query.Shifts:type_name -> internal.TimeShift - 20, // 17: internal.User.Roles:type_name -> internal.Role - 24, // 18: internal.Config.Auth:type_name -> internal.AuthConfig - 26, // 19: internal.OrganizationConfig.LogViewer:type_name -> internal.LogViewerConfig - 27, // 20: internal.LogViewerConfig.Columns:type_name -> internal.LogViewerColumn - 28, // 21: internal.LogViewerColumn.Encodings:type_name -> internal.ColumnEncoding - 8, // 22: internal.DashboardCell.AxesEntry.value:type_name -> internal.Axis - 8, // 23: internal.Cell.AxesEntry.value:type_name -> internal.Axis - 24, // [24:24] is the sub-list for method output_type - 24, // [24:24] is the sub-list for method input_type - 24, // [24:24] is the sub-list for extension type_name - 24, // [24:24] is the sub-list for extension extendee - 0, // [0:24] is the sub-list for field type_name + 0, // 0: internal.Source.PasswordEncoding:type_name -> internal.SecretEncoding + 0, // 1: internal.Source.SharedSecretEncoding:type_name -> internal.SecretEncoding + 0, // 2: internal.Source.ManagementTokenEncoding:type_name -> internal.SecretEncoding + 0, // 3: internal.Source.DatabaseTokenEncoding:type_name -> internal.SecretEncoding + 3, // 4: internal.Dashboard.cells:type_name -> internal.DashboardCell + 10, // 5: internal.Dashboard.templates:type_name -> internal.Template + 16, // 6: internal.DashboardCell.queries:type_name -> internal.Query + 31, // 7: internal.DashboardCell.axes:type_name -> internal.DashboardCell.AxesEntry + 7, // 8: internal.DashboardCell.colors:type_name -> internal.Color + 8, // 9: internal.DashboardCell.legend:type_name -> internal.Legend + 5, // 10: internal.DashboardCell.tableOptions:type_name -> internal.TableOptions + 6, // 11: internal.DashboardCell.fieldOptions:type_name -> internal.RenamableField + 4, // 12: internal.DashboardCell.decimalPlaces:type_name -> internal.DecimalPlaces + 6, // 13: internal.TableOptions.sortBy:type_name -> internal.RenamableField + 11, // 14: internal.Template.values:type_name -> internal.TemplateValue + 12, // 15: internal.Template.query:type_name -> internal.TemplateQuery + 0, // 16: internal.Server.PasswordEncoding:type_name -> internal.SecretEncoding + 15, // 17: internal.Layout.Cells:type_name -> internal.Cell + 16, // 18: internal.Cell.queries:type_name -> internal.Query + 32, // 19: internal.Cell.axes:type_name -> internal.Cell.AxesEntry + 18, // 20: internal.Query.Range:type_name -> internal.Range + 17, // 21: internal.Query.Shifts:type_name -> internal.TimeShift + 21, // 22: internal.User.Roles:type_name -> internal.Role + 25, // 23: internal.Config.Auth:type_name -> internal.AuthConfig + 27, // 24: internal.OrganizationConfig.LogViewer:type_name -> internal.LogViewerConfig + 28, // 25: internal.LogViewerConfig.Columns:type_name -> internal.LogViewerColumn + 29, // 26: internal.LogViewerColumn.Encodings:type_name -> internal.ColumnEncoding + 9, // 27: internal.DashboardCell.AxesEntry.value:type_name -> internal.Axis + 9, // 28: internal.Cell.AxesEntry.value:type_name -> internal.Axis + 29, // [29:29] is the sub-list for method output_type + 29, // [29:29] is the sub-list for method input_type + 29, // [29:29] is the sub-list for extension type_name + 29, // [29:29] is the sub-list for extension extendee + 0, // [0:29] is the sub-list for field type_name } func init() { file_internal_proto_init() } @@ -3239,13 +3359,14 @@ func file_internal_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_internal_proto_rawDesc, - NumEnums: 0, + NumEnums: 1, NumMessages: 32, NumExtensions: 0, NumServices: 0, }, GoTypes: file_internal_proto_goTypes, DependencyIndexes: file_internal_proto_depIdxs, + EnumInfos: file_internal_proto_enumTypes, MessageInfos: file_internal_proto_msgTypes, }.Build() File_internal_proto = out.File diff --git a/kv/internal/internal.proto b/kv/internal/internal.proto index 4e56bbfc46..d7f5ed57d7 100644 --- a/kv/internal/internal.proto +++ b/kv/internal/internal.proto @@ -2,6 +2,11 @@ syntax = "proto3"; package internal; option go_package = ".;internal"; +enum SecretEncoding { + PLAINTEXT = 0; + ENCRYPTED_V1 = 1; +} + message Source { int64 ID = 1; // ID is the unique ID of the source string Name = 2; // Name is the user-defined name for the source @@ -23,7 +28,11 @@ message Source { string ManagementToken = 18; // Management token of an InfluxDB Cloud Dedicated source string DatabaseToken = 19; // Database token of an InfluxDB Cloud Dedicated or other InfluxDB 3 source string TagsCSVPath = 20; // TagsCSVPath is the path to a directory containing CSV files (per db) with tags for the source - string DefaultDatabase = 21; // DefaultDatabase is the default database used in queries for InfluxDB Cloud Dedicated when database list is not available + string DefaultDatabase = 21; // DefaultDatabase is the default database used in queries for InfluxDB Cloud Dedicated when database list is not available + SecretEncoding PasswordEncoding = 22; // Encoding state for Password + SecretEncoding SharedSecretEncoding = 23; // Encoding state for SharedSecret + SecretEncoding ManagementTokenEncoding = 24; // Encoding state for ManagementToken + SecretEncoding DatabaseTokenEncoding = 25; // Encoding state for DatabaseToken } message Dashboard { @@ -136,6 +145,7 @@ message Server { bool InsecureSkipVerify = 9; // InsecureSkipVerify accepts any certificate from the client string Type = 10; // Type is the kind of the server (e.g. flux) string MetadataJSON = 11; // JSON byte representation of the metadata + SecretEncoding PasswordEncoding = 12; // Encoding state for Password } message Layout { diff --git a/kv/internal/internal_test.go b/kv/internal/internal_test.go index 22332ca72a..008680118f 100644 --- a/kv/internal/internal_test.go +++ b/kv/internal/internal_test.go @@ -1,15 +1,20 @@ package internal_test import ( + "bytes" "reflect" "testing" "github.com/google/go-cmp/cmp" "github.com/influxdata/chronograf" "github.com/influxdata/chronograf/kv/internal" + "google.golang.org/protobuf/proto" ) func TestMarshalSource(t *testing.T) { + internal.SetSecretDEK(nil) + t.Cleanup(func() { internal.SetSecretDEK(nil) }) + tests := []struct { name string src chronograf.Source @@ -88,7 +93,75 @@ func TestMarshalSource(t *testing.T) { } } +func TestMarshalSourceWithSecretDEK(t *testing.T) { + internal.SetSecretDEK(bytes.Repeat([]byte{0x41}, 32)) + t.Cleanup(func() { internal.SetSecretDEK(nil) }) + + in := chronograf.Source{ + ID: 12, + Name: "Fountain of Truth", + Type: "influx-v3-cloud-dedicated", + Password: "pw", + SharedSecret: "shared", + ManagementToken: "mgmt-token", + DatabaseToken: "database-token", + } + + data, err := internal.MarshalSource(in) + if err != nil { + t.Fatalf("marshal source: %v", err) + } + + var pb internal.Source + if err := proto.Unmarshal(data, &pb); err != nil { + t.Fatalf("unmarshal source proto: %v", err) + } + + if pb.GetPasswordEncoding() != internal.SecretEncoding_ENCRYPTED_V1 || + pb.GetSharedSecretEncoding() != internal.SecretEncoding_ENCRYPTED_V1 || + pb.GetManagementTokenEncoding() != internal.SecretEncoding_ENCRYPTED_V1 || + pb.GetDatabaseTokenEncoding() != internal.SecretEncoding_ENCRYPTED_V1 { + t.Fatalf("expected encrypted encodings, got %#v", pb) + } + + if pb.Password == in.Password || pb.SharedSecret == in.SharedSecret || pb.ManagementToken == in.ManagementToken || pb.DatabaseToken == in.DatabaseToken { + t.Fatal("expected encrypted stored values to differ from plaintext") + } + + var out chronograf.Source + if err := internal.UnmarshalSource(data, &out); err != nil { + t.Fatalf("unmarshal source: %v", err) + } + if !reflect.DeepEqual(in, out) { + t.Fatalf("source roundtrip mismatch: got %#v, expected %#v", out, in) + } +} + +func TestUnmarshalSourceEncryptedWithoutDEK(t *testing.T) { + internal.SetSecretDEK(bytes.Repeat([]byte{0x42}, 32)) + + data, err := internal.MarshalSource(chronograf.Source{ + ID: 1, + Name: "source", + Type: "influx", + Password: "secret", + }) + if err != nil { + t.Fatalf("marshal source: %v", err) + } + + internal.SetSecretDEK(nil) + + var out chronograf.Source + if err := internal.UnmarshalSource(data, &out); err == nil { + t.Fatal("expected error when encrypted source is unmarshaled without DEK") + } +} + func TestMarshalServer(t *testing.T) { + internal.SetSecretDEK(nil) + t.Cleanup(func() { internal.SetSecretDEK(nil) }) + v := chronograf.Server{ ID: 12, SrcID: 2, @@ -109,6 +182,49 @@ func TestMarshalServer(t *testing.T) { } } +func TestMarshalServerWithSecretDEK(t *testing.T) { + internal.SetSecretDEK(bytes.Repeat([]byte{0x43}, 32)) + t.Cleanup(func() { internal.SetSecretDEK(nil) }) + + in := chronograf.Server{ + ID: 7, + SrcID: 2, + Name: "kap", + Username: "docbrown", + Password: "super-secret", + URL: "http://oldmanpeabody.mall.io:9092", + } + + data, err := internal.MarshalServer(in) + if err != nil { + t.Fatalf("marshal server: %v", err) + } + + var pb internal.Server + if err := proto.Unmarshal(data, &pb); err != nil { + t.Fatalf("unmarshal server proto: %v", err) + } + if pb.GetPasswordEncoding() != internal.SecretEncoding_ENCRYPTED_V1 { + t.Fatalf("expected encrypted password encoding, got %v", pb.GetPasswordEncoding()) + } + if pb.Password == in.Password { + t.Fatal("expected encrypted stored password to differ from plaintext") + } + + var out chronograf.Server + if err := internal.UnmarshalServer(data, &out); err != nil { + t.Fatalf("unmarshal server: %v", err) + } + if !reflect.DeepEqual(in, out) { + t.Fatalf("server roundtrip mismatch: got %#v, expected %#v", out, in) + } + + internal.SetSecretDEK(nil) + if err := internal.UnmarshalServer(data, &out); err == nil { + t.Fatal("expected error when encrypted server is unmarshaled without DEK") + } +} + func TestMarshalLayout(t *testing.T) { layout := chronograf.Layout{ ID: "id", From 64d7fef5203fa3ac3dc17404dbaf5d48a2e79601 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Wed, 13 May 2026 12:12:39 +0200 Subject: [PATCH 04/24] feat(kv): bootstrap wrapped DEK and load secret DEK on startup --- kv/internal/crypto.go | 15 +++++ kv/internal/internal.go | 48 +++++++-------- kv/kv_test.go | 132 +++++++++++++++++++++++++++++++++++++++- server/server.go | 27 +++++--- 4 files changed, 186 insertions(+), 36 deletions(-) diff --git a/kv/internal/crypto.go b/kv/internal/crypto.go index 978e53071b..ccfc53bdbb 100644 --- a/kv/internal/crypto.go +++ b/kv/internal/crypto.go @@ -14,6 +14,21 @@ const ( gcmNonceSize = 12 ) +// GenerateDEK returns a random 32-byte data encryption key. +func GenerateDEK() ([]byte, error) { + return generateDEK() +} + +// WrapDEK encrypts a DEK with the master key. +func WrapDEK(masterKey, dek []byte) ([]byte, error) { + return wrapDEK(masterKey, dek) +} + +// UnwrapDEK decrypts a wrapped DEK with the master key. +func UnwrapDEK(masterKey, wrappedDEK []byte) ([]byte, error) { + return unwrapDEK(masterKey, wrappedDEK) +} + func generateDEK() ([]byte, error) { dek := make([]byte, aes256KeySize) if _, err := io.ReadFull(rand.Reader, dek); err != nil { diff --git a/kv/internal/internal.go b/kv/internal/internal.go index c5e86b81da..07a6a1e429 100644 --- a/kv/internal/internal.go +++ b/kv/internal/internal.go @@ -129,31 +129,31 @@ func MarshalSource(s chronograf.Source) ([]byte, error) { } return proto.Marshal(&Source{ - ID: int64(s.ID), - Name: s.Name, - Type: s.Type, - Username: s.Username, - Password: password, - PasswordEncoding: passwordEncoding, - SharedSecret: sharedSecret, - SharedSecretEncoding: sharedSecretEncoding, - URL: s.URL, - MetaURL: s.MetaURL, - InsecureSkipVerify: s.InsecureSkipVerify, - Default: s.Default, - Telegraf: s.Telegraf, - Organization: s.Organization, - Role: s.Role, - DefaultRP: s.DefaultRP, - Version: s.Version, - ClusterID: s.ClusterID, - AccountID: s.AccountID, - ManagementToken: managementToken, + ID: int64(s.ID), + Name: s.Name, + Type: s.Type, + Username: s.Username, + Password: password, + PasswordEncoding: passwordEncoding, + SharedSecret: sharedSecret, + SharedSecretEncoding: sharedSecretEncoding, + URL: s.URL, + MetaURL: s.MetaURL, + InsecureSkipVerify: s.InsecureSkipVerify, + Default: s.Default, + Telegraf: s.Telegraf, + Organization: s.Organization, + Role: s.Role, + DefaultRP: s.DefaultRP, + Version: s.Version, + ClusterID: s.ClusterID, + AccountID: s.AccountID, + ManagementToken: managementToken, ManagementTokenEncoding: managementTokenEncoding, - DatabaseToken: databaseToken, - DatabaseTokenEncoding: databaseTokenEncoding, - TagsCSVPath: s.TagsCSVPath, - DefaultDatabase: s.DefaultDB, + DatabaseToken: databaseToken, + DatabaseTokenEncoding: databaseTokenEncoding, + TagsCSVPath: s.TagsCSVPath, + DefaultDatabase: s.DefaultDB, }) } diff --git a/kv/kv_test.go b/kv/kv_test.go index d0bbbc73cd..45517c3028 100644 --- a/kv/kv_test.go +++ b/kv/kv_test.go @@ -1,9 +1,12 @@ package kv_test import ( + "bytes" "context" "errors" "io/ioutil" + "os" + "testing" "github.com/influxdata/chronograf" "github.com/influxdata/chronograf/kv" @@ -17,8 +20,15 @@ func NewTestClient() (*kv.Service, error) { if err != nil { return nil, errors.New("unable to open temporary boltdb file") } - f.Close() + if err := f.Close(); err != nil { + return nil, err + } + + return NewTestClientAtPath(f.Name()) +} +// NewTestClientAtPath creates new *bolt.Client using the provided boltdb path. +func NewTestClientAtPath(path string) (*kv.Service, error) { build := chronograf.BuildInfo{ Version: "version", Commit: "commit", @@ -26,7 +36,7 @@ func NewTestClient() (*kv.Service, error) { ctx := context.TODO() b, err := bolt.NewClient(ctx, - bolt.WithPath(f.Name()), + bolt.WithPath(path), bolt.WithBuildInfo(build), ) if err != nil { @@ -35,3 +45,121 @@ func NewTestClient() (*kv.Service, error) { return kv.NewService(ctx, b, kv.WithLogger(mocks.NewLogger())) } + +func TestInitializeSecretDEK(t *testing.T) { + ctx := context.TODO() + keyA := bytes.Repeat([]byte{0x11}, 32) + keyB := bytes.Repeat([]byte{0x22}, 32) + + t.Run("Round Trip With Persisted Wrapped DEK", func(t *testing.T) { + f, err := ioutil.TempFile("", "chronograf-bolt-") + if err != nil { + t.Fatal(err) + } + path := f.Name() + if err := f.Close(); err != nil { + t.Fatal(err) + } + defer os.Remove(path) + + svc, err := NewTestClientAtPath(path) + if err != nil { + t.Fatal(err) + } + if err := svc.InitializeSecretDEK(ctx, keyA); err != nil { + t.Fatal(err) + } + src, err := svc.SourcesStore().Add(ctx, chronograf.Source{ + Name: "src", + Type: "influx", + Password: "p@ssw0rd", + URL: "http://localhost:8086", + }) + if err != nil { + t.Fatal(err) + } + if err := svc.Close(); err != nil { + t.Fatal(err) + } + + svc, err = NewTestClientAtPath(path) + if err != nil { + t.Fatal(err) + } + defer svc.Close() + if err := svc.InitializeSecretDEK(ctx, keyA); err != nil { + t.Fatal(err) + } + got, err := svc.SourcesStore().Get(ctx, src.ID) + if err != nil { + t.Fatal(err) + } + if got.Password != "p@ssw0rd" { + t.Fatalf("unexpected password after restart: got %q", got.Password) + } + }) + + t.Run("Wrapped DEK Requires Master Key", func(t *testing.T) { + f, err := ioutil.TempFile("", "chronograf-bolt-") + if err != nil { + t.Fatal(err) + } + path := f.Name() + if err := f.Close(); err != nil { + t.Fatal(err) + } + defer os.Remove(path) + + svc, err := NewTestClientAtPath(path) + if err != nil { + t.Fatal(err) + } + if err := svc.InitializeSecretDEK(ctx, keyA); err != nil { + t.Fatal(err) + } + if err := svc.Close(); err != nil { + t.Fatal(err) + } + + svc, err = NewTestClientAtPath(path) + if err != nil { + t.Fatal(err) + } + defer svc.Close() + if err := svc.InitializeSecretDEK(ctx, nil); err == nil { + t.Fatal("expected error when wrapped DEK exists and master key is missing") + } + }) + + t.Run("Wrong Master Key Is Rejected", func(t *testing.T) { + f, err := ioutil.TempFile("", "chronograf-bolt-") + if err != nil { + t.Fatal(err) + } + path := f.Name() + if err := f.Close(); err != nil { + t.Fatal(err) + } + defer os.Remove(path) + + svc, err := NewTestClientAtPath(path) + if err != nil { + t.Fatal(err) + } + if err := svc.InitializeSecretDEK(ctx, keyA); err != nil { + t.Fatal(err) + } + if err := svc.Close(); err != nil { + t.Fatal(err) + } + + svc, err = NewTestClientAtPath(path) + if err != nil { + t.Fatal(err) + } + defer svc.Close() + if err := svc.InitializeSecretDEK(ctx, keyB); err == nil { + t.Fatal("expected error when master key does not match wrapped DEK") + } + }) +} diff --git a/server/server.go b/server/server.go index 98b69c2eeb..91b306865d 100644 --- a/server/server.go +++ b/server/server.go @@ -147,15 +147,15 @@ type Server struct { CustomLinks map[string]string `long:"custom-link" description:"Custom link to be added to the client User menu. Multiple links can be added by using multiple of the same flag with different 'name:url' values, or as an environment variable with comma-separated 'name:url' values. E.g. via flags: '--custom-link=InfluxData:https://www.influxdata.com --custom-link=Chronograf:https://github.com/influxdata/chronograf'. E.g. via environment variable: 'export CUSTOM_LINKS=InfluxData:https://www.influxdata.com,Chronograf:https://github.com/influxdata/chronograf'" env:"CUSTOM_LINKS" env-delim:","` TelegrafSystemInterval time.Duration `long:"telegraf-system-interval" default:"1m" description:"Duration used in the GROUP BY time interval for the hosts list" env:"TELEGRAF_SYSTEM_INTERVAL"` - HostPageDisabled bool `short:"H" long:"host-page-disabled" description:"Disable the host list page" env:"HOST_PAGE_DISABLED"` - ReportingDisabled bool `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"` - CustomAutoRefresh string `long:"custom-auto-refresh" description:"Adds custom auto refresh options using semicolon separated list of label=milliseconds pairs" env:"CUSTOM_AUTO_REFRESH"` - LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"error" default:"info" description:"Set the logging level" env:"LOG_LEVEL"` - SecretsMasterKey string `long:"secrets-master-key" description:"Base64-encoded 32-byte master key used to wrap/unwrap the data encryption key for secret-field encryption" env:"SECRETS_MASTER_KEY"` + HostPageDisabled bool `short:"H" long:"host-page-disabled" description:"Disable the host list page" env:"HOST_PAGE_DISABLED"` + ReportingDisabled bool `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"` + CustomAutoRefresh string `long:"custom-auto-refresh" description:"Adds custom auto refresh options using semicolon separated list of label=milliseconds pairs" env:"CUSTOM_AUTO_REFRESH"` + LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"error" default:"info" description:"Set the logging level" env:"LOG_LEVEL"` + SecretsMasterKey string `long:"secrets-master-key" description:"Base64-encoded 32-byte master key used to wrap/unwrap the data encryption key for secret-field encryption" env:"SECRETS_MASTER_KEY"` SecretsMasterKeyFile flags.Filename `long:"secrets-master-key-file" description:"Path to file containing a base64-encoded 32-byte master key used to wrap/unwrap the data encryption key for secret-field encryption" env:"SECRETS_MASTER_KEY_FILE"` - Basepath string `short:"p" long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted. (Note: PREFIX_ROUTES has been deprecated. Now, if basepath is set, all routes will be prefixed with it.)" env:"BASE_PATH"` - ShowVersion bool `short:"v" long:"version" description:"Show Chronograf version info"` - BuildInfo chronograf.BuildInfo + Basepath string `short:"p" long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted. (Note: PREFIX_ROUTES has been deprecated. Now, if basepath is set, all routes will be prefixed with it.)" env:"BASE_PATH"` + ShowVersion bool `short:"v" long:"version" description:"Show Chronograf version info"` + BuildInfo chronograf.BuildInfo BasicAuthRealm string `long:"basic-auth-realm" default:"Chronograf" description:"User visible basic authentication realm" env:"BASICAUTH_REALM"` BasicAuthHtpasswd flags.Filename `long:"htpasswd" description:"File location of .htpasswd file, turns on HTTP basic authentication when specified." env:"HTPASSWD"` @@ -791,7 +791,7 @@ func (s *Server) Serve(ctx context.Context) { Info("InfluxDB v3 time condition validated and configured") } - service := openService(ctx, db, s.newBuilders(logger), logger, s.useAuth(), + service := openService(ctx, db, s.newBuilders(logger), logger, s.useAuth(), s.secretsMasterKey, chronograf.V3Config{ CloudDedicatedManagementURL: s.InfluxDBCloudDedicatedMgmtURL, ClusteredAccountID: s.InfluxDBClusteredAccountID, @@ -928,13 +928,20 @@ func (s *Server) Serve(ctx context.Context) { Info("Stopped serving chronograf at ", scheme, "://", listener.Addr()) } -func openService(ctx context.Context, db kv.Store, builder builders, logger chronograf.Logger, useAuth bool, v3Config chronograf.V3Config) Service { +func openService(ctx context.Context, db kv.Store, builder builders, logger chronograf.Logger, useAuth bool, secretsMasterKey []byte, v3Config chronograf.V3Config) Service { svc, err := kv.NewService(ctx, db, kv.WithLogger(logger)) if err != nil { logger.Error("Unable to create kv service", err) os.Exit(1) } + if err := svc.InitializeSecretDEK(ctx, secretsMasterKey); err != nil { + logger. + WithField("component", "Secrets"). + Error("Unable to initialize secret encryption", err) + os.Exit(1) + } + dashboards, err := builder.Dashboards.Build(svc.DashboardsStore()) if err != nil { logger. From 10c6c3c52207d0d69306d55eef2cc59a9c0d83bb Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Wed, 13 May 2026 12:28:00 +0200 Subject: [PATCH 05/24] feat(kv): bootstrap wrapped DEK and load secret DEK on startup --- kv/secrets.go | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 kv/secrets.go diff --git a/kv/secrets.go b/kv/secrets.go new file mode 100644 index 0000000000..3745479c7e --- /dev/null +++ b/kv/secrets.go @@ -0,0 +1,187 @@ +package kv + +import ( + "context" + "errors" + "fmt" + + "github.com/influxdata/chronograf" + "github.com/influxdata/chronograf/kv/internal" + "google.golang.org/protobuf/proto" +) + +var wrappedDEKID = []byte("secrets/wrapped-dek/v1") + +// InitializeSecretDEK loads or creates a wrapped DEK in KV and configures +// kv/internal secret encryption for this process. +func (s *Service) InitializeSecretDEK(ctx context.Context, masterKey []byte) error { + // Fail closed across restarts/reconfigurations in the same process. + internal.SetSecretDEK(nil) + + var wrappedDEK []byte + if err := s.kv.View(ctx, func(tx Tx) error { + v, err := tx.Bucket(configBucket).Get(wrappedDEKID) + if err != nil { + return err + } + if len(v) > 0 { + wrappedDEK = append([]byte(nil), v...) + } + return nil + }); err != nil { + return err + } + + if len(wrappedDEK) == 0 { + if len(masterKey) == 0 { + return nil + } + + dek, err := internal.GenerateDEK() + if err != nil { + return err + } + wrapped, err := internal.WrapDEK(masterKey, dek) + if err != nil { + return err + } + + if err := s.kv.Update(ctx, func(tx Tx) error { + return tx.Bucket(configBucket).Put(wrappedDEKID, wrapped) + }); err != nil { + return err + } + + internal.SetSecretDEK(dek) + if err := s.migrateLegacyPlaintextSecrets(ctx); err != nil { + return err + } + return nil + } + + if len(masterKey) == 0 { + return errors.New("wrapped DEK exists but no secrets master key is configured") + } + + dek, err := internal.UnwrapDEK(masterKey, wrappedDEK) + if err != nil { + return fmt.Errorf("unable to unwrap stored DEK: %w", err) + } + + internal.SetSecretDEK(dek) + if err := s.migrateLegacyPlaintextSecrets(ctx); err != nil { + return err + } + return nil +} + +func (s *Service) migrateLegacyPlaintextSecrets(ctx context.Context) error { + return s.kv.Update(ctx, func(tx Tx) error { + if err := migrateSourcesBucket(tx); err != nil { + return err + } + if err := migrateServersBucket(tx); err != nil { + return err + } + return nil + }) +} + +func migrateSourcesBucket(tx Tx) error { + b := tx.Bucket(sourcesBucket) + + var rewrites []struct { + key []byte + val []byte + } + + if err := b.ForEach(func(k, v []byte) error { + var pb internal.Source + if err := proto.Unmarshal(v, &pb); err != nil { + return err + } + if !sourceNeedsSecretMigration(pb) { + return nil + } + + var src chronograf.Source + if err := internal.UnmarshalSource(v, &src); err != nil { + return err + } + migrated, err := internal.MarshalSource(src) + if err != nil { + return err + } + + rewrites = append(rewrites, struct { + key []byte + val []byte + }{ + key: append([]byte(nil), k...), + val: migrated, + }) + return nil + }); err != nil { + return err + } + + for _, rw := range rewrites { + if err := b.Put(rw.key, rw.val); err != nil { + return err + } + } + return nil +} + +func migrateServersBucket(tx Tx) error { + b := tx.Bucket(serversBucket) + + var rewrites []struct { + key []byte + val []byte + } + + if err := b.ForEach(func(k, v []byte) error { + var pb internal.Server + if err := proto.Unmarshal(v, &pb); err != nil { + return err + } + if pb.Password == "" || pb.GetPasswordEncoding() == internal.SecretEncoding_ENCRYPTED_V1 { + return nil + } + + var srv chronograf.Server + if err := internal.UnmarshalServer(v, &srv); err != nil { + return err + } + migrated, err := internal.MarshalServer(srv) + if err != nil { + return err + } + + rewrites = append(rewrites, struct { + key []byte + val []byte + }{ + key: append([]byte(nil), k...), + val: migrated, + }) + return nil + }); err != nil { + return err + } + + for _, rw := range rewrites { + if err := b.Put(rw.key, rw.val); err != nil { + return err + } + } + return nil +} + +func sourceNeedsSecretMigration(pb internal.Source) bool { + return (pb.Password != "" && pb.GetPasswordEncoding() != internal.SecretEncoding_ENCRYPTED_V1) || + (pb.SharedSecret != "" && pb.GetSharedSecretEncoding() != internal.SecretEncoding_ENCRYPTED_V1) || + (pb.ManagementToken != "" && pb.GetManagementTokenEncoding() != internal.SecretEncoding_ENCRYPTED_V1) || + (pb.DatabaseToken != "" && pb.GetDatabaseTokenEncoding() != internal.SecretEncoding_ENCRYPTED_V1) +} From 0d93861c66ee505704786cd5fea9a5d4d599c89a Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Wed, 13 May 2026 12:28:19 +0200 Subject: [PATCH 06/24] feat(kv): auto-migrate legacy plaintext secrets after DEK init --- kv/kv_test.go | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/kv/kv_test.go b/kv/kv_test.go index 45517c3028..55af843fe6 100644 --- a/kv/kv_test.go +++ b/kv/kv_test.go @@ -3,6 +3,7 @@ package kv_test import ( "bytes" "context" + "encoding/binary" "errors" "io/ioutil" "os" @@ -11,9 +12,23 @@ import ( "github.com/influxdata/chronograf" "github.com/influxdata/chronograf/kv" "github.com/influxdata/chronograf/kv/bolt" + "github.com/influxdata/chronograf/kv/internal" "github.com/influxdata/chronograf/mocks" + boltDB "go.etcd.io/bbolt" + "google.golang.org/protobuf/proto" ) +var ( + sourcesBucket = []byte("Sources") + serversBucket = []byte("Servers") +) + +func idKey(id int) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, uint64(id)) + return b +} + // NewTestClient creates new *bolt.Client with a set time and temp path. func NewTestClient() (*kv.Service, error) { f, err := ioutil.TempFile("", "chronograf-bolt-") @@ -52,6 +67,8 @@ func TestInitializeSecretDEK(t *testing.T) { keyB := bytes.Repeat([]byte{0x22}, 32) t.Run("Round Trip With Persisted Wrapped DEK", func(t *testing.T) { + internal.SetSecretDEK(nil) + f, err := ioutil.TempFile("", "chronograf-bolt-") if err != nil { t.Fatal(err) @@ -100,6 +117,8 @@ func TestInitializeSecretDEK(t *testing.T) { }) t.Run("Wrapped DEK Requires Master Key", func(t *testing.T) { + internal.SetSecretDEK(nil) + f, err := ioutil.TempFile("", "chronograf-bolt-") if err != nil { t.Fatal(err) @@ -132,6 +151,8 @@ func TestInitializeSecretDEK(t *testing.T) { }) t.Run("Wrong Master Key Is Rejected", func(t *testing.T) { + internal.SetSecretDEK(nil) + f, err := ioutil.TempFile("", "chronograf-bolt-") if err != nil { t.Fatal(err) @@ -162,4 +183,91 @@ func TestInitializeSecretDEK(t *testing.T) { t.Fatal("expected error when master key does not match wrapped DEK") } }) + + t.Run("Legacy Plaintext Records Are Migrated On Init", func(t *testing.T) { + internal.SetSecretDEK(nil) + + f, err := ioutil.TempFile("", "chronograf-bolt-") + if err != nil { + t.Fatal(err) + } + path := f.Name() + if err := f.Close(); err != nil { + t.Fatal(err) + } + defer os.Remove(path) + + svc, err := NewTestClientAtPath(path) + if err != nil { + t.Fatal(err) + } + + src, err := svc.SourcesStore().Add(ctx, chronograf.Source{ + Name: "legacy-source", + Type: "influx-v3-cloud-dedicated", + Password: "pw", + SharedSecret: "shared", + ManagementToken: "mgmt", + DatabaseToken: "db", + URL: "http://localhost:8086", + }) + if err != nil { + t.Fatal(err) + } + + srv, err := svc.ServersStore().Add(ctx, chronograf.Server{ + Name: "legacy-server", + SrcID: src.ID, + Password: "kap-pass", + URL: "http://localhost:9092", + }) + if err != nil { + t.Fatal(err) + } + + if err := svc.InitializeSecretDEK(ctx, keyA); err != nil { + t.Fatal(err) + } + if err := svc.Close(); err != nil { + t.Fatal(err) + } + + db, err := boltDB.Open(path, 0600, &boltDB.Options{ReadOnly: true}) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + if err := db.View(func(tx *boltDB.Tx) error { + sourceRaw := tx.Bucket(sourcesBucket).Get(idKey(src.ID)) + if sourceRaw == nil { + t.Fatalf("expected migrated source row") + } + var sourcePB internal.Source + if err := proto.Unmarshal(sourceRaw, &sourcePB); err != nil { + return err + } + if sourcePB.GetPasswordEncoding() != internal.SecretEncoding_ENCRYPTED_V1 || + sourcePB.GetSharedSecretEncoding() != internal.SecretEncoding_ENCRYPTED_V1 || + sourcePB.GetManagementTokenEncoding() != internal.SecretEncoding_ENCRYPTED_V1 || + sourcePB.GetDatabaseTokenEncoding() != internal.SecretEncoding_ENCRYPTED_V1 { + t.Fatalf("source secret encodings were not migrated") + } + + serverRaw := tx.Bucket(serversBucket).Get(idKey(srv.ID)) + if serverRaw == nil { + t.Fatalf("expected migrated server row") + } + var serverPB internal.Server + if err := proto.Unmarshal(serverRaw, &serverPB); err != nil { + return err + } + if serverPB.GetPasswordEncoding() != internal.SecretEncoding_ENCRYPTED_V1 { + t.Fatalf("server password encoding was not migrated") + } + return nil + }); err != nil { + t.Fatal(err) + } + }) } From 2fe66570a5fb06a38feb9ca8dfd32b4fdca8d7b6 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Wed, 13 May 2026 12:32:24 +0200 Subject: [PATCH 07/24] feat(kv): fail startup when encrypted secrets exist without key state --- kv/kv_test.go | 55 ++++++++++++++++++++++++++++++++++++++++ kv/secrets.go | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/kv/kv_test.go b/kv/kv_test.go index 55af843fe6..143676d49d 100644 --- a/kv/kv_test.go +++ b/kv/kv_test.go @@ -184,6 +184,61 @@ func TestInitializeSecretDEK(t *testing.T) { } }) + t.Run("Encrypted Records Without Wrapped DEK Are Rejected", func(t *testing.T) { + internal.SetSecretDEK(nil) + + f, err := ioutil.TempFile("", "chronograf-bolt-") + if err != nil { + t.Fatal(err) + } + path := f.Name() + if err := f.Close(); err != nil { + t.Fatal(err) + } + defer os.Remove(path) + + svc, err := NewTestClientAtPath(path) + if err != nil { + t.Fatal(err) + } + if err := svc.InitializeSecretDEK(ctx, keyA); err != nil { + t.Fatal(err) + } + if _, err := svc.SourcesStore().Add(ctx, chronograf.Source{ + Name: "encrypted-source", + Type: "influx", + Password: "pw", + URL: "http://localhost:8086", + }); err != nil { + t.Fatal(err) + } + if err := svc.Close(); err != nil { + t.Fatal(err) + } + + db, err := boltDB.Open(path, 0600, nil) + if err != nil { + t.Fatal(err) + } + if err := db.Update(func(tx *boltDB.Tx) error { + return tx.Bucket([]byte("ConfigV1")).Delete([]byte("secrets/wrapped-dek/v1")) + }); err != nil { + t.Fatal(err) + } + if err := db.Close(); err != nil { + t.Fatal(err) + } + + svc, err = NewTestClientAtPath(path) + if err != nil { + t.Fatal(err) + } + defer svc.Close() + if err := svc.InitializeSecretDEK(ctx, nil); err == nil { + t.Fatal("expected error when encrypted records exist without wrapped DEK and master key") + } + }) + t.Run("Legacy Plaintext Records Are Migrated On Init", func(t *testing.T) { internal.SetSecretDEK(nil) diff --git a/kv/secrets.go b/kv/secrets.go index 3745479c7e..46b386e62d 100644 --- a/kv/secrets.go +++ b/kv/secrets.go @@ -34,6 +34,13 @@ func (s *Service) InitializeSecretDEK(ctx context.Context, masterKey []byte) err if len(wrappedDEK) == 0 { if len(masterKey) == 0 { + encryptedExists, err := s.hasEncryptedSecrets(ctx) + if err != nil { + return err + } + if encryptedExists { + return errors.New("encrypted secrets exist but no wrapped DEK or secrets master key is configured") + } return nil } @@ -75,6 +82,69 @@ func (s *Service) InitializeSecretDEK(ctx context.Context, masterKey []byte) err return nil } +func (s *Service) hasEncryptedSecrets(ctx context.Context) (bool, error) { + encrypted := false + if err := s.kv.View(ctx, func(tx Tx) error { + sourcesEncrypted, err := hasEncryptedSources(tx) + if err != nil { + return err + } + if sourcesEncrypted { + encrypted = true + return nil + } + + serversEncrypted, err := hasEncryptedServers(tx) + if err != nil { + return err + } + if serversEncrypted { + encrypted = true + } + return nil + }); err != nil { + return false, err + } + return encrypted, nil +} + +func hasEncryptedSources(tx Tx) (bool, error) { + encrypted := false + if err := tx.Bucket(sourcesBucket).ForEach(func(k, v []byte) error { + var pb internal.Source + if err := proto.Unmarshal(v, &pb); err != nil { + return err + } + if pb.GetPasswordEncoding() == internal.SecretEncoding_ENCRYPTED_V1 || + pb.GetSharedSecretEncoding() == internal.SecretEncoding_ENCRYPTED_V1 || + pb.GetManagementTokenEncoding() == internal.SecretEncoding_ENCRYPTED_V1 || + pb.GetDatabaseTokenEncoding() == internal.SecretEncoding_ENCRYPTED_V1 { + encrypted = true + } + return nil + }); err != nil { + return false, err + } + return encrypted, nil +} + +func hasEncryptedServers(tx Tx) (bool, error) { + encrypted := false + if err := tx.Bucket(serversBucket).ForEach(func(k, v []byte) error { + var pb internal.Server + if err := proto.Unmarshal(v, &pb); err != nil { + return err + } + if pb.GetPasswordEncoding() == internal.SecretEncoding_ENCRYPTED_V1 { + encrypted = true + } + return nil + }); err != nil { + return false, err + } + return encrypted, nil +} + func (s *Service) migrateLegacyPlaintextSecrets(ctx context.Context) error { return s.kv.Update(ctx, func(tx Tx) error { if err := migrateSourcesBucket(tx); err != nil { From 127af0b9147f20738fa689a70493d7a65c4e28c5 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Wed, 13 May 2026 12:56:30 +0200 Subject: [PATCH 08/24] feat(chronoctl): add gen-secrets-master-key command --- cmd/chronoctl/README.md | 1 + cmd/chronoctl/main_test.go | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/cmd/chronoctl/README.md b/cmd/chronoctl/README.md index 949ad02332..da0647c97a 100644 --- a/cmd/chronoctl/README.md +++ b/cmd/chronoctl/README.md @@ -5,6 +5,7 @@ Chronoctl is a tool to interact with an instance of a chronograf's bolt database ``` Available commands: add-superadmin Creates a new superadmin user (bolt specific) + gen-secrets-master-key Generates a secrets master key list-users Lists users (bolt specific) migrate Migrate db (beta) ``` diff --git a/cmd/chronoctl/main_test.go b/cmd/chronoctl/main_test.go index 3e0877b373..e934cd5a32 100644 --- a/cmd/chronoctl/main_test.go +++ b/cmd/chronoctl/main_test.go @@ -23,6 +23,10 @@ func TestChronoctlCommands(t *testing.T) { args: []string{"gen-keypair", "-h"}, err: false, }, + { + args: []string{"gen-secrets-master-key", "-h"}, + err: false, + }, { args: []string{"list-users", "-h"}, err: false, From 5321b1527d8d3cd1cc7de9fdbd819d337640c184 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Wed, 13 May 2026 13:14:45 +0200 Subject: [PATCH 09/24] feat(secrets): add DEK rewrap workflow and chronoctl rotation command --- cmd/chronoctl/README.md | 1 + cmd/chronoctl/main_test.go | 4 +++ kv/kv_test.go | 51 ++++++++++++++++++++++++++++++++++++++ kv/secrets.go | 31 +++++++++++++++++++++++ 4 files changed, 87 insertions(+) diff --git a/cmd/chronoctl/README.md b/cmd/chronoctl/README.md index da0647c97a..7fddd8ea91 100644 --- a/cmd/chronoctl/README.md +++ b/cmd/chronoctl/README.md @@ -7,6 +7,7 @@ Available commands: add-superadmin Creates a new superadmin user (bolt specific) gen-secrets-master-key Generates a secrets master key list-users Lists users (bolt specific) + rewrap-secrets-master-key Rewraps stored DEK with new master key migrate Migrate db (beta) ``` diff --git a/cmd/chronoctl/main_test.go b/cmd/chronoctl/main_test.go index e934cd5a32..ee2c2c6451 100644 --- a/cmd/chronoctl/main_test.go +++ b/cmd/chronoctl/main_test.go @@ -27,6 +27,10 @@ func TestChronoctlCommands(t *testing.T) { args: []string{"gen-secrets-master-key", "-h"}, err: false, }, + { + args: []string{"rewrap-secrets-master-key", "-h"}, + err: false, + }, { args: []string{"list-users", "-h"}, err: false, diff --git a/kv/kv_test.go b/kv/kv_test.go index 143676d49d..a99415d8c0 100644 --- a/kv/kv_test.go +++ b/kv/kv_test.go @@ -184,6 +184,57 @@ func TestInitializeSecretDEK(t *testing.T) { } }) + t.Run("Rewrap Secret DEK", func(t *testing.T) { + internal.SetSecretDEK(nil) + + f, err := ioutil.TempFile("", "chronograf-bolt-") + if err != nil { + t.Fatal(err) + } + path := f.Name() + if err := f.Close(); err != nil { + t.Fatal(err) + } + defer os.Remove(path) + + svc, err := NewTestClientAtPath(path) + if err != nil { + t.Fatal(err) + } + if err := svc.InitializeSecretDEK(ctx, keyA); err != nil { + t.Fatal(err) + } + + src, err := svc.SourcesStore().Add(ctx, chronograf.Source{ + Name: "src", + Type: "influx", + Password: "pw", + URL: "http://localhost:8086", + }) + if err != nil { + t.Fatal(err) + } + + if err := svc.RewrapSecretDEK(ctx, keyA, keyB); err != nil { + t.Fatal(err) + } + if err := svc.Close(); err != nil { + t.Fatal(err) + } + + svc, err = NewTestClientAtPath(path) + if err != nil { + t.Fatal(err) + } + defer svc.Close() + if err := svc.InitializeSecretDEK(ctx, keyB); err != nil { + t.Fatal(err) + } + if _, err := svc.SourcesStore().Get(ctx, src.ID); err != nil { + t.Fatal(err) + } + }) + t.Run("Encrypted Records Without Wrapped DEK Are Rejected", func(t *testing.T) { internal.SetSecretDEK(nil) diff --git a/kv/secrets.go b/kv/secrets.go index 46b386e62d..a2c5d84121 100644 --- a/kv/secrets.go +++ b/kv/secrets.go @@ -82,6 +82,37 @@ func (s *Service) InitializeSecretDEK(ctx context.Context, masterKey []byte) err return nil } +// RewrapSecretDEK rotates the master key by rewrapping the stored DEK. +// It does not re-encrypt persisted source/server secrets. +func (s *Service) RewrapSecretDEK(ctx context.Context, oldMasterKey, newMasterKey []byte) error { + var wrappedDEK []byte + if err := s.kv.View(ctx, func(tx Tx) error { + v, err := tx.Bucket(configBucket).Get(wrappedDEKID) + if err != nil { + return err + } + if len(v) == 0 { + return errors.New("wrapped DEK not found") + } + wrappedDEK = append([]byte(nil), v...) + return nil + }); err != nil { + return err + } + + dek, err := internal.UnwrapDEK(oldMasterKey, wrappedDEK) + if err != nil { + return fmt.Errorf("unable to unwrap stored DEK with old master key: %w", err) + } + rewrapped, err := internal.WrapDEK(newMasterKey, dek) + if err != nil { + return fmt.Errorf("unable to wrap DEK with new master key: %w", err) + } + return s.kv.Update(ctx, func(tx Tx) error { + return tx.Bucket(configBucket).Put(wrappedDEKID, rewrapped) + }) +} + func (s *Service) hasEncryptedSecrets(ctx context.Context) (bool, error) { encrypted := false if err := s.kv.View(ctx, func(tx Tx) error { From c40e280fbd3b784c48a23763d5a9fb857c9a47a3 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Wed, 13 May 2026 13:22:47 +0200 Subject: [PATCH 10/24] feat(secrets): add DEK rewrap workflow and chronoctl rotation command --- cmd/chronoctl/gen_secrets_master_key.go | 53 +++++++++++++ cmd/chronoctl/rewrap_secrets_master_key.go | 86 ++++++++++++++++++++++ cmd/chronoctl/strings.go | 7 ++ 3 files changed, 146 insertions(+) create mode 100644 cmd/chronoctl/gen_secrets_master_key.go create mode 100644 cmd/chronoctl/rewrap_secrets_master_key.go create mode 100644 cmd/chronoctl/strings.go diff --git a/cmd/chronoctl/gen_secrets_master_key.go b/cmd/chronoctl/gen_secrets_master_key.go new file mode 100644 index 0000000000..3526b17857 --- /dev/null +++ b/cmd/chronoctl/gen_secrets_master_key.go @@ -0,0 +1,53 @@ +package main + +import ( + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "os" + + flags "github.com/jessevdk/go-flags" +) + +func init() { + parser.AddCommand("gen-secrets-master-key", + "Generate secrets master key.", + "Generate base64-encoded 32-byte key for Chronograf --secrets-master-key / --secrets-master-key-file.", + &genSecretsMasterKeyCommand{}) +} + +type genSecretsMasterKeyCommand struct { + Out flags.Filename `long:"out" description:"File to save the generated key (0600 permissions). If omitted, key is printed to stdout."` +} + +func generateSecretsMasterKey() (string, error) { + raw := make([]byte, 32) + if _, err := rand.Read(raw); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(raw), nil +} + +func (c *genSecretsMasterKeyCommand) Execute(args []string) error { + key, err := generateSecretsMasterKey() + if err != nil { + errExit(err) + } + + if c.Out == "" { + fmt.Println(key) + return nil + } + + if _, err := os.Stat(string(c.Out)); err == nil { + errExit(errors.New("Specify non-existant file to write to.")) + } + + if err := os.WriteFile(string(c.Out), []byte(key+"\n"), 0600); err != nil { + errExit(err) + } + + fmt.Printf("Secrets master key generated and saved at %s\n", c.Out) + return nil +} diff --git a/cmd/chronoctl/rewrap_secrets_master_key.go b/cmd/chronoctl/rewrap_secrets_master_key.go new file mode 100644 index 0000000000..b99ea7c6f3 --- /dev/null +++ b/cmd/chronoctl/rewrap_secrets_master_key.go @@ -0,0 +1,86 @@ +package main + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "os" +) + +func init() { + parser.AddCommand("rewrap-secrets-master-key", + "Rewrap stored DEK with new secrets master key.", + "Rotate Chronograf secrets master key by rewrapping the stored DEK in BoltDB.", + &rewrapSecretsMasterKeyCommand{}) +} + +type rewrapSecretsMasterKeyCommand struct { + BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"` + + OldSecretsMasterKey string `long:"old-secrets-master-key" description:"Current base64-encoded 32-byte secrets master key." env:"OLD_SECRETS_MASTER_KEY"` + OldSecretsMasterKeyFile string `long:"old-secrets-master-key-file" description:"Path to file containing current base64-encoded 32-byte secrets master key." env:"OLD_SECRETS_MASTER_KEY_FILE"` + + NewSecretsMasterKey string `long:"new-secrets-master-key" description:"New base64-encoded 32-byte secrets master key." env:"NEW_SECRETS_MASTER_KEY"` + NewSecretsMasterKeyFile string `long:"new-secrets-master-key-file" description:"Path to file containing new base64-encoded 32-byte secrets master key." env:"NEW_SECRETS_MASTER_KEY_FILE"` +} + +func (c *rewrapSecretsMasterKeyCommand) Execute(args []string) error { + oldKey, err := loadCLISecretsMasterKey(c.OldSecretsMasterKey, c.OldSecretsMasterKeyFile, "old") + if err != nil { + errExit(err) + } + newKey, err := loadCLISecretsMasterKey(c.NewSecretsMasterKey, c.NewSecretsMasterKeyFile, "new") + if err != nil { + errExit(err) + } + + store, err := NewBoltClient(c.BoltPath) + if err != nil { + return err + } + defer store.Close() + + svc, err := NewService(store) + if err != nil { + return err + } + + if err := svc.RewrapSecretDEK(context.Background(), oldKey, newKey); err != nil { + errExit(err) + } + + fmt.Println("Successfully rewrapped DEK with new secrets master key") + return nil +} + +func loadCLISecretsMasterKey(value, filePath, label string) ([]byte, error) { + if value != "" && filePath != "" { + return nil, fmt.Errorf("%s secrets master key must be provided by value or file, not both", label) + } + if value == "" && filePath == "" { + return nil, fmt.Errorf("%s secrets master key is required", label) + } + + raw := value + if filePath != "" { + b, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("reading %s secrets master key file: %w", label, err) + } + raw = string(b) + } + raw = trimSpace(raw) + if raw == "" { + return nil, fmt.Errorf("%s secrets master key is empty", label) + } + + key, err := base64.StdEncoding.DecodeString(raw) + if err != nil { + return nil, fmt.Errorf("decoding %s secrets master key: %w", label, err) + } + if len(key) != 32 { + return nil, errors.New("secrets master key must decode to 32 bytes") + } + return key, nil +} diff --git a/cmd/chronoctl/strings.go b/cmd/chronoctl/strings.go new file mode 100644 index 0000000000..bf3e3fd538 --- /dev/null +++ b/cmd/chronoctl/strings.go @@ -0,0 +1,7 @@ +package main + +import "strings" + +func trimSpace(v string) string { + return strings.TrimSpace(v) +} From 11c5f3c8ac2c70255f9caed4f503bbfeabd25aea Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Wed, 13 May 2026 13:32:54 +0200 Subject: [PATCH 11/24] feat(secrets): add disable-secrets-encryption workflow and chronoctl command --- cmd/chronoctl/README.md | 1 + cmd/chronoctl/disable_secrets_encryption.go | 45 +++++++ cmd/chronoctl/main_test.go | 4 + kv/kv_test.go | 57 +++++++++ kv/secrets.go | 129 +++++++++++++++++++- 5 files changed, 232 insertions(+), 4 deletions(-) create mode 100644 cmd/chronoctl/disable_secrets_encryption.go diff --git a/cmd/chronoctl/README.md b/cmd/chronoctl/README.md index 7fddd8ea91..6352863c81 100644 --- a/cmd/chronoctl/README.md +++ b/cmd/chronoctl/README.md @@ -7,6 +7,7 @@ Available commands: add-superadmin Creates a new superadmin user (bolt specific) gen-secrets-master-key Generates a secrets master key list-users Lists users (bolt specific) + disable-secrets-encryption Disables secrets encryption and removes wrapped DEK rewrap-secrets-master-key Rewraps stored DEK with new master key migrate Migrate db (beta) ``` diff --git a/cmd/chronoctl/disable_secrets_encryption.go b/cmd/chronoctl/disable_secrets_encryption.go new file mode 100644 index 0000000000..6390ef477c --- /dev/null +++ b/cmd/chronoctl/disable_secrets_encryption.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + "fmt" +) + +func init() { + parser.AddCommand("disable-secrets-encryption", + "Disable secrets encryption and rewrite persisted secrets to plaintext.", + "Decrypt stored secrets in BoltDB and remove wrapped DEK. Requires current secrets master key.", + &disableSecretsEncryptionCommand{}) +} + +type disableSecretsEncryptionCommand struct { + BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"` + + SecretsMasterKey string `long:"secrets-master-key" description:"Current base64-encoded 32-byte secrets master key." env:"SECRETS_MASTER_KEY"` + SecretsMasterKeyFile string `long:"secrets-master-key-file" description:"Path to file containing current base64-encoded 32-byte secrets master key." env:"SECRETS_MASTER_KEY_FILE"` +} + +func (c *disableSecretsEncryptionCommand) Execute(args []string) error { + key, err := loadCLISecretsMasterKey(c.SecretsMasterKey, c.SecretsMasterKeyFile, "current") + if err != nil { + errExit(err) + } + + store, err := NewBoltClient(c.BoltPath) + if err != nil { + return err + } + defer store.Close() + + svc, err := NewService(store) + if err != nil { + return err + } + + if err := svc.DisableSecretEncryption(context.Background(), key); err != nil { + errExit(err) + } + + fmt.Println("Successfully disabled secrets encryption and removed wrapped DEK") + return nil +} diff --git a/cmd/chronoctl/main_test.go b/cmd/chronoctl/main_test.go index ee2c2c6451..a707638ac3 100644 --- a/cmd/chronoctl/main_test.go +++ b/cmd/chronoctl/main_test.go @@ -27,6 +27,10 @@ func TestChronoctlCommands(t *testing.T) { args: []string{"gen-secrets-master-key", "-h"}, err: false, }, + { + args: []string{"disable-secrets-encryption", "-h"}, + err: false, + }, { args: []string{"rewrap-secrets-master-key", "-h"}, err: false, diff --git a/kv/kv_test.go b/kv/kv_test.go index a99415d8c0..bd7c0914ed 100644 --- a/kv/kv_test.go +++ b/kv/kv_test.go @@ -235,6 +235,63 @@ func TestInitializeSecretDEK(t *testing.T) { } }) + t.Run("Disable Secret Encryption", func(t *testing.T) { + internal.SetSecretDEK(nil) + + f, err := ioutil.TempFile("", "chronograf-bolt-") + if err != nil { + t.Fatal(err) + } + path := f.Name() + if err := f.Close(); err != nil { + t.Fatal(err) + } + defer os.Remove(path) + + svc, err := NewTestClientAtPath(path) + if err != nil { + t.Fatal(err) + } + if err := svc.InitializeSecretDEK(ctx, keyA); err != nil { + t.Fatal(err) + } + src, err := svc.SourcesStore().Add(ctx, chronograf.Source{ + Name: "src", + Type: "influx-v3-cloud-dedicated", + Password: "pw", + SharedSecret: "shared", + ManagementToken: "mgmt", + DatabaseToken: "db", + URL: "http://localhost:8086", + }) + if err != nil { + t.Fatal(err) + } + + if err := svc.DisableSecretEncryption(ctx, keyA); err != nil { + t.Fatal(err) + } + if err := svc.Close(); err != nil { + t.Fatal(err) + } + + svc, err = NewTestClientAtPath(path) + if err != nil { + t.Fatal(err) + } + defer svc.Close() + if err := svc.InitializeSecretDEK(ctx, nil); err != nil { + t.Fatalf("expected startup without master key after disable, got: %v", err) + } + got, err := svc.SourcesStore().Get(ctx, src.ID) + if err != nil { + t.Fatal(err) + } + if got.Password != "pw" || got.SharedSecret != "shared" || got.ManagementToken != "mgmt" || got.DatabaseToken != "db" { + t.Fatalf("unexpected plaintext values after disable: %+v", got) + } + }) + t.Run("Encrypted Records Without Wrapped DEK Are Rejected", func(t *testing.T) { internal.SetSecretDEK(nil) diff --git a/kv/secrets.go b/kv/secrets.go index a2c5d84121..6c1ad719a7 100644 --- a/kv/secrets.go +++ b/kv/secrets.go @@ -113,6 +113,123 @@ func (s *Service) RewrapSecretDEK(ctx context.Context, oldMasterKey, newMasterKe }) } +// DisableSecretEncryption decrypts persisted secrets back to plaintext and +// removes the wrapped DEK. Requires the current master key. +func (s *Service) DisableSecretEncryption(ctx context.Context, masterKey []byte) error { + if len(masterKey) == 0 { + return errors.New("secrets master key is required") + } + + var wrappedDEK []byte + if err := s.kv.View(ctx, func(tx Tx) error { + v, err := tx.Bucket(configBucket).Get(wrappedDEKID) + if err != nil { + return err + } + if len(v) == 0 { + return errors.New("wrapped DEK not found") + } + wrappedDEK = append([]byte(nil), v...) + return nil + }); err != nil { + return err + } + + dek, err := internal.UnwrapDEK(masterKey, wrappedDEK) + if err != nil { + return fmt.Errorf("unable to unwrap stored DEK: %w", err) + } + + if err := s.kv.Update(ctx, func(tx Tx) error { + if err := rewriteEncryptedSecretsToPlaintext(tx, dek); err != nil { + return err + } + return tx.Bucket(configBucket).Delete(wrappedDEKID) + }); err != nil { + return err + } + + internal.SetSecretDEK(nil) + return nil +} + +func rewriteEncryptedSecretsToPlaintext(tx Tx, dek []byte) error { + type sourceRewrite struct { + key []byte + src chronograf.Source + } + type serverRewrite struct { + key []byte + srv chronograf.Server + } + + var sourceRewrites []sourceRewrite + var serverRewrites []serverRewrite + + internal.SetSecretDEK(dek) + defer internal.SetSecretDEK(nil) + + if err := tx.Bucket(sourcesBucket).ForEach(func(k, v []byte) error { + var pb internal.Source + if err := proto.Unmarshal(v, &pb); err != nil { + return err + } + if !sourceHasEncryptedEncoding(pb) { + return nil + } + var src chronograf.Source + if err := internal.UnmarshalSource(v, &src); err != nil { + return err + } + sourceRewrites = append(sourceRewrites, sourceRewrite{key: append([]byte(nil), k...), src: src}) + return nil + }); err != nil { + return err + } + + if err := tx.Bucket(serversBucket).ForEach(func(k, v []byte) error { + var pb internal.Server + if err := proto.Unmarshal(v, &pb); err != nil { + return err + } + if pb.GetPasswordEncoding() != internal.SecretEncoding_ENCRYPTED_V1 { + return nil + } + var srv chronograf.Server + if err := internal.UnmarshalServer(v, &srv); err != nil { + return err + } + serverRewrites = append(serverRewrites, serverRewrite{key: append([]byte(nil), k...), srv: srv}) + return nil + }); err != nil { + return err + } + + internal.SetSecretDEK(nil) + + for _, rw := range sourceRewrites { + plaintext, err := internal.MarshalSource(rw.src) + if err != nil { + return err + } + if err := tx.Bucket(sourcesBucket).Put(rw.key, plaintext); err != nil { + return err + } + } + + for _, rw := range serverRewrites { + plaintext, err := internal.MarshalServer(rw.srv) + if err != nil { + return err + } + if err := tx.Bucket(serversBucket).Put(rw.key, plaintext); err != nil { + return err + } + } + + return nil +} + func (s *Service) hasEncryptedSecrets(ctx context.Context) (bool, error) { encrypted := false if err := s.kv.View(ctx, func(tx Tx) error { @@ -146,10 +263,7 @@ func hasEncryptedSources(tx Tx) (bool, error) { if err := proto.Unmarshal(v, &pb); err != nil { return err } - if pb.GetPasswordEncoding() == internal.SecretEncoding_ENCRYPTED_V1 || - pb.GetSharedSecretEncoding() == internal.SecretEncoding_ENCRYPTED_V1 || - pb.GetManagementTokenEncoding() == internal.SecretEncoding_ENCRYPTED_V1 || - pb.GetDatabaseTokenEncoding() == internal.SecretEncoding_ENCRYPTED_V1 { + if sourceHasEncryptedEncoding(pb) { encrypted = true } return nil @@ -286,3 +400,10 @@ func sourceNeedsSecretMigration(pb internal.Source) bool { (pb.ManagementToken != "" && pb.GetManagementTokenEncoding() != internal.SecretEncoding_ENCRYPTED_V1) || (pb.DatabaseToken != "" && pb.GetDatabaseTokenEncoding() != internal.SecretEncoding_ENCRYPTED_V1) } + +func sourceHasEncryptedEncoding(pb internal.Source) bool { + return pb.GetPasswordEncoding() == internal.SecretEncoding_ENCRYPTED_V1 || + pb.GetSharedSecretEncoding() == internal.SecretEncoding_ENCRYPTED_V1 || + pb.GetManagementTokenEncoding() == internal.SecretEncoding_ENCRYPTED_V1 || + pb.GetDatabaseTokenEncoding() == internal.SecretEncoding_ENCRYPTED_V1 +} From bb9e50c9b68d65b8f96fca8a81d552f14614e593 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Wed, 13 May 2026 13:37:57 +0200 Subject: [PATCH 12/24] docs: document secrets key generation, rewrap, and disable workflows --- cmd/chronoctl/README.md | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/cmd/chronoctl/README.md b/cmd/chronoctl/README.md index 6352863c81..9c54277e5b 100644 --- a/cmd/chronoctl/README.md +++ b/cmd/chronoctl/README.md @@ -12,6 +12,53 @@ Available commands: migrate Migrate db (beta) ``` +### Secrets Encryption Commands + +Use these commands when Chronograf secret-at-rest encryption is enabled. + +##### Generate Secrets Master Key +Generate a base64-encoded 32-byte key: + +```sh +$ chronoctl gen-secrets-master-key +``` + +Write the generated key to a file (0600): + +```sh +$ chronoctl gen-secrets-master-key --out ./chronograf-secrets.key +``` + +##### Rewrap Secrets Master Key +Rotate the secrets master key by rewrapping the stored DEK: + +```sh +$ chronoctl rewrap-secrets-master-key \ + --bolt-path ./chronograf-v1.db \ + --old-secrets-master-key-file ./old.key \ + --new-secrets-master-key-file ./new.key +``` + +After successful rewrap, start Chronograf with the new key. + +##### Disable Secrets Encryption +Disable secret encryption by decrypting persisted secrets to plaintext and +removing the wrapped DEK: + +```sh +$ chronoctl disable-secrets-encryption \ + --bolt-path ./chronograf-v1.db \ + --secrets-master-key-file ./current.key +``` + +After successful disable: +- Chronograf no longer requires `--secrets-master-key` / `--secrets-master-key-file` +- persisted secrets are plaintext again + +Important: +- `rewrap-secrets-master-key` changes only master-key wrapping and does not re-encrypt secret records. +- `disable-secrets-encryption` is a destructive security downgrade. + ### Migrate From 941675a4d9db15b7fea08cfe95d7357e7062d1d6 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Thu, 14 May 2026 17:20:24 +0200 Subject: [PATCH 13/24] test: add tests --- cmd/chronoctl/main_test.go | 97 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/cmd/chronoctl/main_test.go b/cmd/chronoctl/main_test.go index a707638ac3..1c11fbd7ce 100644 --- a/cmd/chronoctl/main_test.go +++ b/cmd/chronoctl/main_test.go @@ -1,6 +1,10 @@ package main import ( + "bytes" + "encoding/base64" + "os" + "path/filepath" "testing" flags "github.com/jessevdk/go-flags" @@ -60,3 +64,96 @@ func TestChronoctlCommands(t *testing.T) { } } } + +func TestLoadCLISecretsMasterKey(t *testing.T) { + validRaw := bytes.Repeat([]byte{0x11}, 32) + validB64 := base64.StdEncoding.EncodeToString(validRaw) + + tests := []struct { + name string + value string + fileBody string + useFile bool + wantErr bool + }{ + { + name: "value and file both set", + value: validB64, + fileBody: validB64, + useFile: true, + wantErr: true, + }, + { + name: "value and file both empty", + wantErr: true, + }, + { + name: "invalid base64 in value", + value: "%%%not-base64%%%", + wantErr: true, + }, + { + name: "wrong decoded length", + value: base64.StdEncoding.EncodeToString([]byte("short")), + wantErr: true, + }, + { + name: "valid value", + value: validB64, + wantErr: false, + }, + { + name: "valid file", + fileBody: validB64 + "\n", + useFile: true, + wantErr: false, + }, + { + name: "invalid base64 file", + fileBody: "invalid@@@", + useFile: true, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var filePath string + if tt.useFile { + dir := t.TempDir() + filePath = filepath.Join(dir, "secrets.key") + if err := os.WriteFile(filePath, []byte(tt.fileBody), 0600); err != nil { + t.Fatal(err) + } + } + + key, err := loadCLISecretsMasterKey(tt.value, filePath, "test") + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(key) != 32 { + t.Fatalf("unexpected key length: got %d, want 32", len(key)) + } + }) + } +} + +func TestGenerateSecretsMasterKey(t *testing.T) { + key, err := generateSecretsMasterKey() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + raw, err := base64.StdEncoding.DecodeString(key) + if err != nil { + t.Fatalf("generated key is not valid base64: %v", err) + } + if len(raw) != 32 { + t.Fatalf("unexpected decoded key length: got %d, want 32", len(raw)) + } +} From 521e11235c809689dd27ee859379539a4ea6ad44 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Thu, 14 May 2026 17:20:43 +0200 Subject: [PATCH 14/24] fix(server): redact source database and management tokens from API responses --- server/sources.go | 2 ++ server/sources_test.go | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/server/sources.go b/server/sources.go index 4e8dea64ab..8ceb7869cd 100644 --- a/server/sources.go +++ b/server/sources.go @@ -113,6 +113,8 @@ func newSourceResponse(ctx context.Context, src chronograf.Source) sourceRespons // Omit the password and shared secret on response src.Password = "" src.SharedSecret = "" + src.DatabaseToken = "" + src.ManagementToken = "" httpAPISrcs := "/chronograf/v1/sources" res := sourceResponse{ diff --git a/server/sources_test.go b/server/sources_test.go index 850f84362f..804b4cceb2 100644 --- a/server/sources_test.go +++ b/server/sources_test.go @@ -892,7 +892,7 @@ func TestService_UpdateSource(t *testing.T) { wantStatusCode: 200, wantContentType: "application/json", wantBody: func(url string) string { - return fmt.Sprintf(`{"id":"2","name":"v3-core","type":"influx-v3-core","databaseToken":"test-token-123","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"mydb","version":"Unknown","authentication":"unknown","links":{"self":"/chronograf/v1/sources/2","kapacitors":"/chronograf/v1/sources/2/kapacitors","services":"/chronograf/v1/sources/2/services","proxy":"/chronograf/v1/sources/2/proxy","queries":"/chronograf/v1/sources/2/queries","write":"/chronograf/v1/sources/2/write","permissions":"/chronograf/v1/sources/2/permissions","users":"/chronograf/v1/sources/2/users","databases":"/chronograf/v1/sources/2/dbs","annotations":"/chronograf/v1/sources/2/annotations","health":"/chronograf/v1/sources/2/health"}} + return fmt.Sprintf(`{"id":"2","name":"v3-core","type":"influx-v3-core","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"mydb","version":"Unknown","authentication":"unknown","links":{"self":"/chronograf/v1/sources/2","kapacitors":"/chronograf/v1/sources/2/kapacitors","services":"/chronograf/v1/sources/2/services","proxy":"/chronograf/v1/sources/2/proxy","queries":"/chronograf/v1/sources/2/queries","write":"/chronograf/v1/sources/2/write","permissions":"/chronograf/v1/sources/2/permissions","users":"/chronograf/v1/sources/2/users","databases":"/chronograf/v1/sources/2/dbs","annotations":"/chronograf/v1/sources/2/annotations","health":"/chronograf/v1/sources/2/health"}} `, url) }, }, @@ -945,7 +945,7 @@ func TestService_UpdateSource(t *testing.T) { wantStatusCode: 200, wantContentType: "application/json", wantBody: func(url string) string { - return fmt.Sprintf(`{"id":"3","name":"v3-enterprise","type":"influx-v3-enterprise","databaseToken":"enterprise-token-456","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"enterprise_db","version":"Unknown","authentication":"unknown","links":{"self":"/chronograf/v1/sources/3","kapacitors":"/chronograf/v1/sources/3/kapacitors","services":"/chronograf/v1/sources/3/services","proxy":"/chronograf/v1/sources/3/proxy","queries":"/chronograf/v1/sources/3/queries","write":"/chronograf/v1/sources/3/write","permissions":"/chronograf/v1/sources/3/permissions","users":"/chronograf/v1/sources/3/users","databases":"/chronograf/v1/sources/3/dbs","annotations":"/chronograf/v1/sources/3/annotations","health":"/chronograf/v1/sources/3/health"}} + return fmt.Sprintf(`{"id":"3","name":"v3-enterprise","type":"influx-v3-enterprise","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"enterprise_db","version":"Unknown","authentication":"unknown","links":{"self":"/chronograf/v1/sources/3","kapacitors":"/chronograf/v1/sources/3/kapacitors","services":"/chronograf/v1/sources/3/services","proxy":"/chronograf/v1/sources/3/proxy","queries":"/chronograf/v1/sources/3/queries","write":"/chronograf/v1/sources/3/write","permissions":"/chronograf/v1/sources/3/permissions","users":"/chronograf/v1/sources/3/users","databases":"/chronograf/v1/sources/3/dbs","annotations":"/chronograf/v1/sources/3/annotations","health":"/chronograf/v1/sources/3/health"}} `, url) }, }, @@ -999,7 +999,7 @@ func TestService_UpdateSource(t *testing.T) { wantStatusCode: 200, wantContentType: "application/json", wantBody: func(url string) string { - return fmt.Sprintf(`{"id":"4","name":"v3-clustered","type":"influx-v3-clustered","clusterId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","accountId":"f1e2d3c4-b5a6-9870-dcba-fe9876543210","managementToken":"mgmt-token-789","databaseToken":"db-token-789","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"clustered_db","authentication":"unknown","links":{"self":"/chronograf/v1/sources/4","kapacitors":"/chronograf/v1/sources/4/kapacitors","services":"/chronograf/v1/sources/4/services","proxy":"/chronograf/v1/sources/4/proxy","queries":"/chronograf/v1/sources/4/queries","write":"/chronograf/v1/sources/4/write","permissions":"/chronograf/v1/sources/4/permissions","users":"/chronograf/v1/sources/4/users","databases":"/chronograf/v1/sources/4/dbs","annotations":"/chronograf/v1/sources/4/annotations","health":"/chronograf/v1/sources/4/health"}} + return fmt.Sprintf(`{"id":"4","name":"v3-clustered","type":"influx-v3-clustered","clusterId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","accountId":"f1e2d3c4-b5a6-9870-dcba-fe9876543210","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"clustered_db","authentication":"unknown","links":{"self":"/chronograf/v1/sources/4","kapacitors":"/chronograf/v1/sources/4/kapacitors","services":"/chronograf/v1/sources/4/services","proxy":"/chronograf/v1/sources/4/proxy","queries":"/chronograf/v1/sources/4/queries","write":"/chronograf/v1/sources/4/write","permissions":"/chronograf/v1/sources/4/permissions","users":"/chronograf/v1/sources/4/users","databases":"/chronograf/v1/sources/4/dbs","annotations":"/chronograf/v1/sources/4/annotations","health":"/chronograf/v1/sources/4/health"}} `, url) }, }, @@ -1055,7 +1055,7 @@ func TestService_UpdateSource(t *testing.T) { wantStatusCode: 200, wantContentType: "application/json", wantBody: func(url string) string { - return fmt.Sprintf(`{"id":"5","name":"v3-cloud-dedicated","type":"influx-v3-cloud-dedicated","clusterId":"12345678-1234-5678-1234-567812345678","accountId":"87654321-4321-8765-4321-876543218765","managementToken":"cloud-mgmt-abc","databaseToken":"cloud-token-abc","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"cloud_db","authentication":"unknown","links":{"self":"/chronograf/v1/sources/5","kapacitors":"/chronograf/v1/sources/5/kapacitors","services":"/chronograf/v1/sources/5/services","proxy":"/chronograf/v1/sources/5/proxy","queries":"/chronograf/v1/sources/5/queries","write":"/chronograf/v1/sources/5/write","permissions":"/chronograf/v1/sources/5/permissions","users":"/chronograf/v1/sources/5/users","databases":"/chronograf/v1/sources/5/dbs","annotations":"/chronograf/v1/sources/5/annotations","health":"/chronograf/v1/sources/5/health"}} + return fmt.Sprintf(`{"id":"5","name":"v3-cloud-dedicated","type":"influx-v3-cloud-dedicated","clusterId":"12345678-1234-5678-1234-567812345678","accountId":"87654321-4321-8765-4321-876543218765","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"cloud_db","authentication":"unknown","links":{"self":"/chronograf/v1/sources/5","kapacitors":"/chronograf/v1/sources/5/kapacitors","services":"/chronograf/v1/sources/5/services","proxy":"/chronograf/v1/sources/5/proxy","queries":"/chronograf/v1/sources/5/queries","write":"/chronograf/v1/sources/5/write","permissions":"/chronograf/v1/sources/5/permissions","users":"/chronograf/v1/sources/5/users","databases":"/chronograf/v1/sources/5/dbs","annotations":"/chronograf/v1/sources/5/annotations","health":"/chronograf/v1/sources/5/health"}} `, url) }, }, @@ -1111,7 +1111,7 @@ func TestService_UpdateSource(t *testing.T) { wantStatusCode: 200, wantContentType: "application/json", wantBody: func(url string) string { - return fmt.Sprintf(`{"id":"6","name":"v3-cloud-dedicated","type":"influx-v3-cloud-dedicated","databaseToken":"old-cloud-token","url":"%s","default":false,"telegraf":"my-db","organization":"1337","defaultRP":"","defaultDB":"my-db","authentication":"unknown","links":{"self":"/chronograf/v1/sources/6","kapacitors":"/chronograf/v1/sources/6/kapacitors","services":"/chronograf/v1/sources/6/services","proxy":"/chronograf/v1/sources/6/proxy","queries":"/chronograf/v1/sources/6/queries","write":"/chronograf/v1/sources/6/write","permissions":"/chronograf/v1/sources/6/permissions","users":"/chronograf/v1/sources/6/users","databases":"/chronograf/v1/sources/6/dbs","annotations":"/chronograf/v1/sources/6/annotations","health":"/chronograf/v1/sources/6/health"}} + return fmt.Sprintf(`{"id":"6","name":"v3-cloud-dedicated","type":"influx-v3-cloud-dedicated","url":"%s","default":false,"telegraf":"my-db","organization":"1337","defaultRP":"","defaultDB":"my-db","authentication":"unknown","links":{"self":"/chronograf/v1/sources/6","kapacitors":"/chronograf/v1/sources/6/kapacitors","services":"/chronograf/v1/sources/6/services","proxy":"/chronograf/v1/sources/6/proxy","queries":"/chronograf/v1/sources/6/queries","write":"/chronograf/v1/sources/6/write","permissions":"/chronograf/v1/sources/6/permissions","users":"/chronograf/v1/sources/6/users","databases":"/chronograf/v1/sources/6/dbs","annotations":"/chronograf/v1/sources/6/annotations","health":"/chronograf/v1/sources/6/health"}} `, url) }, }, @@ -1164,7 +1164,7 @@ func TestService_UpdateSource(t *testing.T) { wantStatusCode: 200, wantContentType: "application/json", wantBody: func(url string) string { - return fmt.Sprintf(`{"id":"7","name":"v3-serverless","type":"influx-v3-serverless","databaseToken":"serverless-token-xyz","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"serverless_db","authentication":"token","links":{"self":"/chronograf/v1/sources/7","kapacitors":"/chronograf/v1/sources/7/kapacitors","services":"/chronograf/v1/sources/7/services","proxy":"/chronograf/v1/sources/7/proxy","queries":"/chronograf/v1/sources/7/queries","write":"/chronograf/v1/sources/7/write","permissions":"/chronograf/v1/sources/7/permissions","users":"/chronograf/v1/sources/7/users","databases":"/chronograf/v1/sources/7/dbs","annotations":"/chronograf/v1/sources/7/annotations","health":"/chronograf/v1/sources/7/health"}} + return fmt.Sprintf(`{"id":"7","name":"v3-serverless","type":"influx-v3-serverless","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"serverless_db","authentication":"token","links":{"self":"/chronograf/v1/sources/7","kapacitors":"/chronograf/v1/sources/7/kapacitors","services":"/chronograf/v1/sources/7/services","proxy":"/chronograf/v1/sources/7/proxy","queries":"/chronograf/v1/sources/7/queries","write":"/chronograf/v1/sources/7/write","permissions":"/chronograf/v1/sources/7/permissions","users":"/chronograf/v1/sources/7/users","databases":"/chronograf/v1/sources/7/dbs","annotations":"/chronograf/v1/sources/7/annotations","health":"/chronograf/v1/sources/7/health"}} `, url) }, }, From 73a0949f2c36fc0447c35c863a581d63c0e07582 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Thu, 14 May 2026 17:22:04 +0200 Subject: [PATCH 15/24] Revert "test: add tests" This reverts commit 941675a4d9db15b7fea08cfe95d7357e7062d1d6. --- cmd/chronoctl/main_test.go | 97 -------------------------------------- 1 file changed, 97 deletions(-) diff --git a/cmd/chronoctl/main_test.go b/cmd/chronoctl/main_test.go index 1c11fbd7ce..a707638ac3 100644 --- a/cmd/chronoctl/main_test.go +++ b/cmd/chronoctl/main_test.go @@ -1,10 +1,6 @@ package main import ( - "bytes" - "encoding/base64" - "os" - "path/filepath" "testing" flags "github.com/jessevdk/go-flags" @@ -64,96 +60,3 @@ func TestChronoctlCommands(t *testing.T) { } } } - -func TestLoadCLISecretsMasterKey(t *testing.T) { - validRaw := bytes.Repeat([]byte{0x11}, 32) - validB64 := base64.StdEncoding.EncodeToString(validRaw) - - tests := []struct { - name string - value string - fileBody string - useFile bool - wantErr bool - }{ - { - name: "value and file both set", - value: validB64, - fileBody: validB64, - useFile: true, - wantErr: true, - }, - { - name: "value and file both empty", - wantErr: true, - }, - { - name: "invalid base64 in value", - value: "%%%not-base64%%%", - wantErr: true, - }, - { - name: "wrong decoded length", - value: base64.StdEncoding.EncodeToString([]byte("short")), - wantErr: true, - }, - { - name: "valid value", - value: validB64, - wantErr: false, - }, - { - name: "valid file", - fileBody: validB64 + "\n", - useFile: true, - wantErr: false, - }, - { - name: "invalid base64 file", - fileBody: "invalid@@@", - useFile: true, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var filePath string - if tt.useFile { - dir := t.TempDir() - filePath = filepath.Join(dir, "secrets.key") - if err := os.WriteFile(filePath, []byte(tt.fileBody), 0600); err != nil { - t.Fatal(err) - } - } - - key, err := loadCLISecretsMasterKey(tt.value, filePath, "test") - if tt.wantErr { - if err == nil { - t.Fatalf("expected error, got nil") - } - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(key) != 32 { - t.Fatalf("unexpected key length: got %d, want 32", len(key)) - } - }) - } -} - -func TestGenerateSecretsMasterKey(t *testing.T) { - key, err := generateSecretsMasterKey() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - raw, err := base64.StdEncoding.DecodeString(key) - if err != nil { - t.Fatalf("generated key is not valid base64: %v", err) - } - if len(raw) != 32 { - t.Fatalf("unexpected decoded key length: got %d, want 32", len(raw)) - } -} From afeaf65e84d6a4122c774f552dc5bd836036a6ea Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Thu, 14 May 2026 17:26:08 +0200 Subject: [PATCH 16/24] Revert "Revert "test: add tests"" This reverts commit 73a0949f2c36fc0447c35c863a581d63c0e07582. --- cmd/chronoctl/main_test.go | 97 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/cmd/chronoctl/main_test.go b/cmd/chronoctl/main_test.go index a707638ac3..1c11fbd7ce 100644 --- a/cmd/chronoctl/main_test.go +++ b/cmd/chronoctl/main_test.go @@ -1,6 +1,10 @@ package main import ( + "bytes" + "encoding/base64" + "os" + "path/filepath" "testing" flags "github.com/jessevdk/go-flags" @@ -60,3 +64,96 @@ func TestChronoctlCommands(t *testing.T) { } } } + +func TestLoadCLISecretsMasterKey(t *testing.T) { + validRaw := bytes.Repeat([]byte{0x11}, 32) + validB64 := base64.StdEncoding.EncodeToString(validRaw) + + tests := []struct { + name string + value string + fileBody string + useFile bool + wantErr bool + }{ + { + name: "value and file both set", + value: validB64, + fileBody: validB64, + useFile: true, + wantErr: true, + }, + { + name: "value and file both empty", + wantErr: true, + }, + { + name: "invalid base64 in value", + value: "%%%not-base64%%%", + wantErr: true, + }, + { + name: "wrong decoded length", + value: base64.StdEncoding.EncodeToString([]byte("short")), + wantErr: true, + }, + { + name: "valid value", + value: validB64, + wantErr: false, + }, + { + name: "valid file", + fileBody: validB64 + "\n", + useFile: true, + wantErr: false, + }, + { + name: "invalid base64 file", + fileBody: "invalid@@@", + useFile: true, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var filePath string + if tt.useFile { + dir := t.TempDir() + filePath = filepath.Join(dir, "secrets.key") + if err := os.WriteFile(filePath, []byte(tt.fileBody), 0600); err != nil { + t.Fatal(err) + } + } + + key, err := loadCLISecretsMasterKey(tt.value, filePath, "test") + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(key) != 32 { + t.Fatalf("unexpected key length: got %d, want 32", len(key)) + } + }) + } +} + +func TestGenerateSecretsMasterKey(t *testing.T) { + key, err := generateSecretsMasterKey() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + raw, err := base64.StdEncoding.DecodeString(key) + if err != nil { + t.Fatalf("generated key is not valid base64: %v", err) + } + if len(raw) != 32 { + t.Fatalf("unexpected decoded key length: got %d, want 32", len(raw)) + } +} From 2743c0539d4cad2b687549003709746ad98f64b5 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Thu, 14 May 2026 17:42:10 +0200 Subject: [PATCH 17/24] fix: zero key material --- kv/internal/internal.go | 2 ++ kv/internal/internal_test.go | 3 +++ kv/secrets.go | 2 ++ server/server.go | 7 +++---- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/kv/internal/internal.go b/kv/internal/internal.go index 07a6a1e429..5d6d5f5d00 100644 --- a/kv/internal/internal.go +++ b/kv/internal/internal.go @@ -23,6 +23,8 @@ func SetSecretDEK(dek []byte) { secretDEKMu.Lock() defer secretDEKMu.Unlock() + clear(secretDEK) + if len(dek) == 0 { secretDEK = nil return diff --git a/kv/internal/internal_test.go b/kv/internal/internal_test.go index 008680118f..58b0b8a3fa 100644 --- a/kv/internal/internal_test.go +++ b/kv/internal/internal_test.go @@ -210,6 +210,9 @@ func TestMarshalServerWithSecretDEK(t *testing.T) { if pb.Password == in.Password { t.Fatal("expected encrypted stored password to differ from plaintext") } + if pb.Username != in.Username { + t.Fatalf("expected username to remain plaintext in proto: got %q want %q", pb.Username, in.Username) + } var out chronograf.Server if err := internal.UnmarshalServer(data, &out); err != nil { diff --git a/kv/secrets.go b/kv/secrets.go index 6c1ad719a7..e9771afc96 100644 --- a/kv/secrets.go +++ b/kv/secrets.go @@ -154,6 +154,8 @@ func (s *Service) DisableSecretEncryption(ctx context.Context, masterKey []byte) } func rewriteEncryptedSecretsToPlaintext(tx Tx, dek []byte) error { + defer clear(dek) + type sourceRewrite struct { key []byte src chronograf.Source diff --git a/server/server.go b/server/server.go index 91b306865d..686100239d 100644 --- a/server/server.go +++ b/server/server.go @@ -165,8 +165,6 @@ type Server struct { TLSMaxVersion string `long:"tls-max-version" description:"Maximum version of the TLS protocol that will be negotiated." env:"TLS_MAX_VERSION"` oauthClient http.Client - - secretsMasterKey []byte } func provide(p oauth2.Provider, m oauth2.Mux, ok func() error) func(func(oauth2.Provider, oauth2.Mux)) { @@ -731,7 +729,6 @@ func (s *Server) Serve(ctx context.Context) { logger.Error("Invalid secrets master key configuration: ", err) os.Exit(1) } - s.secretsMasterKey = secretsMasterKey var db kv.Store if len(s.EtcdEndpoints) == 0 { @@ -791,7 +788,7 @@ func (s *Server) Serve(ctx context.Context) { Info("InfluxDB v3 time condition validated and configured") } - service := openService(ctx, db, s.newBuilders(logger), logger, s.useAuth(), s.secretsMasterKey, + service := openService(ctx, db, s.newBuilders(logger), logger, s.useAuth(), secretsMasterKey, chronograf.V3Config{ CloudDedicatedManagementURL: s.InfluxDBCloudDedicatedMgmtURL, ClusteredAccountID: s.InfluxDBClusteredAccountID, @@ -929,6 +926,8 @@ func (s *Server) Serve(ctx context.Context) { } func openService(ctx context.Context, db kv.Store, builder builders, logger chronograf.Logger, useAuth bool, secretsMasterKey []byte, v3Config chronograf.V3Config) Service { + defer clear(secretsMasterKey) + svc, err := kv.NewService(ctx, db, kv.WithLogger(logger)) if err != nil { logger.Error("Unable to create kv service", err) From 2db014ac3420b2af95ebc38ce8eab23ef7bbff6e Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Thu, 14 May 2026 17:47:31 +0200 Subject: [PATCH 18/24] refactor: remove unnecessary wrapper --- cmd/chronoctl/rewrap_secrets_master_key.go | 3 ++- cmd/chronoctl/strings.go | 7 ------- 2 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 cmd/chronoctl/strings.go diff --git a/cmd/chronoctl/rewrap_secrets_master_key.go b/cmd/chronoctl/rewrap_secrets_master_key.go index b99ea7c6f3..b7469daa8e 100644 --- a/cmd/chronoctl/rewrap_secrets_master_key.go +++ b/cmd/chronoctl/rewrap_secrets_master_key.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "os" + "strings" ) func init() { @@ -70,7 +71,7 @@ func loadCLISecretsMasterKey(value, filePath, label string) ([]byte, error) { } raw = string(b) } - raw = trimSpace(raw) + raw = strings.TrimSpace(raw) if raw == "" { return nil, fmt.Errorf("%s secrets master key is empty", label) } diff --git a/cmd/chronoctl/strings.go b/cmd/chronoctl/strings.go deleted file mode 100644 index bf3e3fd538..0000000000 --- a/cmd/chronoctl/strings.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "strings" - -func trimSpace(v string) string { - return strings.TrimSpace(v) -} From 1fb3586c46f3de5bc5d7f10623f456a70584bad1 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Thu, 14 May 2026 17:49:07 +0200 Subject: [PATCH 19/24] fix: harden output file checks --- cmd/chronoctl/gen_secrets_master_key.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/chronoctl/gen_secrets_master_key.go b/cmd/chronoctl/gen_secrets_master_key.go index 3526b17857..2eabb061e4 100644 --- a/cmd/chronoctl/gen_secrets_master_key.go +++ b/cmd/chronoctl/gen_secrets_master_key.go @@ -42,6 +42,8 @@ func (c *genSecretsMasterKeyCommand) Execute(args []string) error { if _, err := os.Stat(string(c.Out)); err == nil { errExit(errors.New("Specify non-existant file to write to.")) + } else if !errors.Is(err, os.ErrNotExist) { + errExit(err) } if err := os.WriteFile(string(c.Out), []byte(key+"\n"), 0600); err != nil { From 3bd403c1dc4ca1fb817390d995f4879cc1efad06 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Fri, 15 May 2026 09:15:38 +0200 Subject: [PATCH 20/24] fix: secrets are redacted now --- server/swagger.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/swagger.json b/server/swagger.json index 9b74e63313..2f8beeaf6b 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -3935,7 +3935,7 @@ }, "password": { "type": "string", - "description": "Password is in cleartext." + "description": "Write-only password for kapacitor authentication. Redacted in API responses." }, "url": { "type": "string", @@ -5212,11 +5212,11 @@ }, "password": { "type": "string", - "description": "Password is in cleartext." + "description": "Write-only password for source authentication. Redacted in API responses." }, "sharedSecret": { "type": "string", - "description": "JWT signing secret for optional Authorization: Bearer to InfluxDB" + "description": "Write-only JWT signing secret for optional Authorization: Bearer to InfluxDB. Redacted in API responses." }, "clusterId": { "type": "string", @@ -5228,11 +5228,11 @@ }, "managementToken": { "type": "string", - "description": "Management token for InfluxDB Cloud Dedicated sources" + "description": "Write-only management token for InfluxDB Cloud Dedicated sources. Redacted in API responses." }, "databaseToken": { "type": "string", - "description": "Database token for InfluxDB Cloud Dedicated or other InfluxDB 3 sources" + "description": "Write-only database token for InfluxDB Cloud Dedicated or other InfluxDB 3 sources. Redacted in API responses." }, "tagsCSVPath": { "type": "string", From 5b0702bce61eac4cbb1922613556c40be0636d29 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Fri, 15 May 2026 12:13:26 +0200 Subject: [PATCH 21/24] docs: better phrase for encryption rollback command --- cmd/chronoctl/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/chronoctl/README.md b/cmd/chronoctl/README.md index 9c54277e5b..78614ff879 100644 --- a/cmd/chronoctl/README.md +++ b/cmd/chronoctl/README.md @@ -57,7 +57,7 @@ After successful disable: Important: - `rewrap-secrets-master-key` changes only master-key wrapping and does not re-encrypt secret records. -- `disable-secrets-encryption` is a destructive security downgrade. +- `disable-secrets-encryption` decrypts encrypted secrets and stores them as plaintext. ### Migrate From b8afbfed926f5c8e921b85c806e8dc49fb78f1a2 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Fri, 15 May 2026 12:16:54 +0200 Subject: [PATCH 22/24] docs: update CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f644739e3..b3e424599f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## v1.11.3 [unreleased] + +### Security Fixes + +1. [#6211](https://github.com/influxdata/chronograf/pull/6211): Harden secrets-at-rest protections for persisted source and server credentials using envelope encryption. + * Add startup migration for legacy plaintext secrets when a secrets master key is configured. + * Add `chronoctl` commands for master-key generation, rewrap, and disable workflows. + ## v1.11.2 [2026-05-06] ### Other From 2bb6296a0c99ad862ad83fb954849517388d175e Mon Sep 17 00:00:00 2001 From: alespour <42931850+alespour@users.noreply.github.com> Date: Fri, 15 May 2026 13:03:22 +0200 Subject: [PATCH 23/24] fix: error message Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- cmd/chronoctl/gen_secrets_master_key.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/chronoctl/gen_secrets_master_key.go b/cmd/chronoctl/gen_secrets_master_key.go index 2eabb061e4..ee8cdd2336 100644 --- a/cmd/chronoctl/gen_secrets_master_key.go +++ b/cmd/chronoctl/gen_secrets_master_key.go @@ -41,7 +41,7 @@ func (c *genSecretsMasterKeyCommand) Execute(args []string) error { } if _, err := os.Stat(string(c.Out)); err == nil { - errExit(errors.New("Specify non-existant file to write to.")) + errExit(errors.New("specify a non-existent file to write to")) } else if !errors.Is(err, os.ErrNotExist) { errExit(err) } From b17a357c1914a5d2de01ee08f6748dec1ffa4bc3 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Wed, 27 May 2026 12:42:00 +0200 Subject: [PATCH 24/24] fix: fail fast when --bolt-path DB is missing for secrets ops --- cmd/chronoctl/disable_secrets_encryption.go | 4 ++++ cmd/chronoctl/main_test.go | 20 ++++++++++++++++++++ cmd/chronoctl/rewrap_secrets_master_key.go | 4 ++++ cmd/chronoctl/util.go | 14 ++++++++++++++ 4 files changed, 42 insertions(+) diff --git a/cmd/chronoctl/disable_secrets_encryption.go b/cmd/chronoctl/disable_secrets_encryption.go index 6390ef477c..5c8089df58 100644 --- a/cmd/chronoctl/disable_secrets_encryption.go +++ b/cmd/chronoctl/disable_secrets_encryption.go @@ -25,6 +25,10 @@ func (c *disableSecretsEncryptionCommand) Execute(args []string) error { errExit(err) } + if err := validateExistingBoltPath(c.BoltPath); err != nil { + errExit(err) + } + store, err := NewBoltClient(c.BoltPath) if err != nil { return err diff --git a/cmd/chronoctl/main_test.go b/cmd/chronoctl/main_test.go index 1c11fbd7ce..83db8786a3 100644 --- a/cmd/chronoctl/main_test.go +++ b/cmd/chronoctl/main_test.go @@ -157,3 +157,23 @@ func TestGenerateSecretsMasterKey(t *testing.T) { t.Fatalf("unexpected decoded key length: got %d, want 32", len(raw)) } } + +func TestValidateExistingBoltPath(t *testing.T) { + t.Run("missing path", func(t *testing.T) { + err := validateExistingBoltPath(filepath.Join(t.TempDir(), "missing.db")) + if err == nil { + t.Fatalf("expected error for missing path") + } + }) + + t.Run("existing path", func(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "chronograf-v1.db") + if err := os.WriteFile(path, []byte{}, 0600); err != nil { + t.Fatal(err) + } + if err := validateExistingBoltPath(path); err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) +} diff --git a/cmd/chronoctl/rewrap_secrets_master_key.go b/cmd/chronoctl/rewrap_secrets_master_key.go index b7469daa8e..4e46846b7f 100644 --- a/cmd/chronoctl/rewrap_secrets_master_key.go +++ b/cmd/chronoctl/rewrap_secrets_master_key.go @@ -36,6 +36,10 @@ func (c *rewrapSecretsMasterKeyCommand) Execute(args []string) error { errExit(err) } + if err := validateExistingBoltPath(c.BoltPath); err != nil { + errExit(err) + } + store, err := NewBoltClient(c.BoltPath) if err != nil { return err diff --git a/cmd/chronoctl/util.go b/cmd/chronoctl/util.go index e299129f7a..601f21d99f 100644 --- a/cmd/chronoctl/util.go +++ b/cmd/chronoctl/util.go @@ -23,6 +23,20 @@ func NewService(s kv.Store) (*kv.Service, error) { return kv.NewService(context.TODO(), s) } +func validateExistingBoltPath(path string) error { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf( + "bolt db %q not found; use --bolt-path to target an existing chronograf database", + path, + ) + } + return fmt.Errorf("checking bolt db %q: %w", path, err) + } + + return nil +} + func NewTabWriter() *tabwriter.Writer { return tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) }