Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix proxy hash #1157

Merged
merged 6 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions backend/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package backend

import (
"context"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"os"
Expand Down Expand Up @@ -96,15 +98,34 @@ func (c *GrafanaCfg) Equal(c2 *GrafanaCfg) bool {
return true
}

// ProxyHash returns the last four bytes of the PDC client key contents,
// if present, for use in datasource instance caching
// ProxyHash returns the last four characters of the base64-encoded
// // PDC client key contents, if present, for use in datasource instance
njvrzm marked this conversation as resolved.
Show resolved Hide resolved
// caching. The contents should be PEM-encoded, so we try to PEM-decode
// them, and, if successful, return the base-64 encoding of the final three bytes,
// giving a four character hash.
func (c *GrafanaCfg) ProxyHash() string {
if c == nil {
return ""
}
key := c.config[proxy.PluginSecureSocksProxyClientKeyContents]
start := max(len(key)-4, 0)
return key[start:]
contents := c.config[proxy.PluginSecureSocksProxyClientKeyContents]
wbrowne marked this conversation as resolved.
Show resolved Hide resolved
if contents == "" {
return ""
}
block, _ := pem.Decode([]byte(contents))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually need to add any more logic here or can we simplify by using the client key in its entirety?

Copy link
Contributor Author

@njvrzm njvrzm Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we could! It's 120 bytes long currently, or 64 if we extract only the encoded part. Potentially longer later. Granted there shouldn't be ever be more than few hundred of them at a time tops, but it seems wasteful, and the key would be hashed internally for every map access; the longer key adds a small but nonzero latency to lookups, although that would be more than made up for by not doing the PEM decode and base64 decode.

I guess one concern is that the key is a bit more likely to leak out in an error message this way, but without the other values the key is probably not dangerous.

I dunno - it feels odd to me, but if you think it's the best approach I can change it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's go with how it is now and see how we get on 😄

if block == nil {
Logger.Warn("ProxyHash(): key contents are not PEM-encoded")
return ""
}
if block.Type != "PRIVATE KEY" {
Logger.Warn("ProxyHash(): key contents are not PEM-encoded private key")
return ""
}
bl := len(block.Bytes)
if bl < 3 {
Logger.Warn("ProxyHash(): key contents too short")
return ""
}
return base64.StdEncoding.EncodeToString(block.Bytes[bl-3:])
}

type FeatureToggles struct {
Expand Down
34 changes: 34 additions & 0 deletions backend/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package backend

import (
"context"
"crypto/rand"
"encoding/pem"
"fmt"
mathrand "math/rand"
"os"
"testing"

Expand Down Expand Up @@ -358,3 +362,33 @@ func TestPluginAppClientSecret(t *testing.T) {
require.Equal(t, "client-secret", v)
})
}

func randomProxyContents() []byte {
key := make([]byte, 48)
_, _ = rand.Read(key)
pb := pem.Block{
Type: "PRIVATE KEY",
Bytes: key,
}
return pem.EncodeToMemory(&pb)
}

var b64chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/+"

func BenchmarkProxyHash(b *testing.B) {
count := 0
kBytes := randomProxyContents()
cm := map[string]string{
proxy.PluginSecureSocksProxyClientKeyContents: string(kBytes),
}
for i := 0; i < b.N; i++ {
kBytes[88] = b64chars[mathrand.Intn(64)] //nolint:gosec
cm[proxy.PluginSecureSocksProxyClientKeyContents] = string(kBytes)
cfg := NewGrafanaCfg(cm)
hash := cfg.ProxyHash()
if hash[0] == 'a' {
count++
}
}
fmt.Printf("This should be about one in 64: %f\n", float64(count)/float64(b.N))
}
3 changes: 2 additions & 1 deletion backend/datasource/instance_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ func (ip *instanceProvider) GetKey(ctx context.Context, pluginContext backend.Pl
proxyHash := pluginContext.GrafanaConfig.ProxyHash()
tenantID := tenant.IDFromContext(ctx)

return fmt.Sprintf("%d#%s#%s", dsID, tenantID, proxyHash), nil
key := fmt.Sprintf("%d#%s#%s", dsID, tenantID, proxyHash)
return key, nil
njvrzm marked this conversation as resolved.
Show resolved Hide resolved
}

func (ip *instanceProvider) NeedsUpdate(_ context.Context, pluginContext backend.PluginContext, cachedInstance instancemgmt.CachedInstance) bool {
Expand Down
26 changes: 20 additions & 6 deletions backend/datasource/instance_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package datasource

import (
"context"
"encoding/base64"
"encoding/pem"
"fmt"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -35,15 +39,25 @@ func TestInstanceProvider(t *testing.T) {
})

t.Run("When PDC is configured, datasource cache key should include its (so-called) hash", func(t *testing.T) {
// the value of Bytes below must be a multiple of three in length for this test
// to pass, but that's an artifact of how the target value is created. The code
// itself isn't affected by the length of the key as long as it's at least 3 bytes.
contents := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: []byte("this will work."),
})
want := base64.StdEncoding.EncodeToString([]byte("this will work."))
want = strings.TrimRight(want, "=")
want = want[len(want)-4:]
cfg := backend.NewGrafanaCfg(map[string]string{
proxy.PluginSecureSocksProxyClientKeyContents: "This should work",
proxy.PluginSecureSocksProxyClientKeyContents: string(contents),
})
key, err := ip.GetKey(context.Background(), backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ID: 5},
GrafanaConfig: cfg,
})
require.NoError(t, err)
require.Equal(t, "5##work", key)
require.Equal(t, fmt.Sprintf("5##%s", want), key)
})

t.Run("When PDC is configured but the key is empty, no problem", func(t *testing.T) {
Expand All @@ -58,16 +72,16 @@ func TestInstanceProvider(t *testing.T) {
require.Equal(t, "6##", key)
})

t.Run("When PDC is configured, a too-short key doesn't cause an error", func(t *testing.T) {
t.Run("When PDC is configured but the key is not PEM-encoded, no problem", func(t *testing.T) {
cfg := backend.NewGrafanaCfg(map[string]string{
proxy.PluginSecureSocksProxyClientKeyContents: "doh",
proxy.PluginSecureSocksProxyClientKeyContents: "this is not\na valid string\n",
})
key, err := ip.GetKey(context.Background(), backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ID: 7},
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ID: 6},
GrafanaConfig: cfg,
})
require.NoError(t, err)
require.Equal(t, "7##doh", key)
require.Equal(t, "6##", key)
})

t.Run("When both the configuration and updated field of current data source instance settings are equal to the cache, should return false", func(t *testing.T) {
Expand Down