mirror of
https://github.com/rclone/rclone.git
synced 2026-06-29 10:25:00 -04:00
Before this change, if the user changed their password or public-key and the auth proxy script returned updated config parameters for the backend (eg a rotated api_key) rclone would continue to re-use the old backend with the old config parameters out of the fscache. This was because both the VFS cache and the fs/cache key were derived from the user name only, so a change in the user's password or public-key did not invalidate the cached backend. Fix this by deriving the cache key from the user plus a hash of the password/public-key, so a credential change forces a fresh backend. The hash uses a per-process random HMAC key so the fragment that appears in logs cannot be brute-forced offline.
270 lines
8.0 KiB
Go
270 lines
8.0 KiB
Go
package proxy
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"strings"
|
|
"testing"
|
|
|
|
_ "github.com/rclone/rclone/backend/local"
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/config/configmap"
|
|
"github.com/rclone/rclone/fs/config/obscure"
|
|
"github.com/rclone/rclone/vfs/vfscommon"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
func TestRun(t *testing.T) {
|
|
opt := Opt
|
|
cmd := "go run proxy_code.go"
|
|
opt.AuthProxy = cmd
|
|
p := New(context.Background(), &opt, &vfscommon.Opt)
|
|
|
|
t.Run("Normal", func(t *testing.T) {
|
|
config, err := p.run(map[string]string{
|
|
"type": "ftp",
|
|
"user": "me",
|
|
"pass": "pass",
|
|
"host": "127.0.0.1",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, configmap.Simple{
|
|
"type": "ftp",
|
|
"user": "me-test",
|
|
"pass": "pass",
|
|
"host": "127.0.0.1",
|
|
"_root": "",
|
|
}, config)
|
|
})
|
|
|
|
t.Run("Error", func(t *testing.T) {
|
|
config, err := p.run(map[string]string{
|
|
"error": "potato",
|
|
})
|
|
assert.Nil(t, config)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "potato")
|
|
})
|
|
|
|
t.Run("Obscure", func(t *testing.T) {
|
|
config, err := p.run(map[string]string{
|
|
"type": "ftp",
|
|
"user": "me",
|
|
"pass": "pass",
|
|
"host": "127.0.0.1",
|
|
"_obscure": "pass,user",
|
|
})
|
|
require.NoError(t, err)
|
|
config["user"] = obscure.MustReveal(config["user"])
|
|
config["pass"] = obscure.MustReveal(config["pass"])
|
|
assert.Equal(t, configmap.Simple{
|
|
"type": "ftp",
|
|
"user": "me-test",
|
|
"pass": "pass",
|
|
"host": "127.0.0.1",
|
|
"_obscure": "pass,user",
|
|
"_root": "",
|
|
}, config)
|
|
})
|
|
|
|
const testUser = "testUser"
|
|
const testPass = "testPass"
|
|
|
|
t.Run("call w/Password", func(t *testing.T) {
|
|
// check cache empty
|
|
assert.Equal(t, 0, p.vfsCache.Entries())
|
|
defer p.vfsCache.Clear()
|
|
|
|
passwordBytes := []byte(testPass)
|
|
value, err := p.call(testUser, testPass, false)
|
|
require.NoError(t, err)
|
|
entry, ok := value.(cacheEntry)
|
|
require.True(t, ok)
|
|
|
|
// check hash is correct in entry
|
|
assert.Equal(t, entry.pwHash, sha256.Sum256(passwordBytes))
|
|
require.NotNil(t, entry.vfs)
|
|
f := entry.vfs.Fs()
|
|
require.NotNil(t, f)
|
|
cacheKey := generateCacheKey(testUser, testPass)
|
|
assert.Equal(t, "proxy-"+cacheKey, f.Name())
|
|
assert.True(t, strings.HasPrefix(f.String(), "Local file system"))
|
|
|
|
// check it is in the cache
|
|
assert.Equal(t, 1, p.vfsCache.Entries())
|
|
cacheValue, ok := p.vfsCache.GetMaybe(cacheKey)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, value, cacheValue)
|
|
})
|
|
|
|
t.Run("Call w/Password", func(t *testing.T) {
|
|
// check cache empty
|
|
assert.Equal(t, 0, p.vfsCache.Entries())
|
|
defer p.vfsCache.Clear()
|
|
|
|
cacheKey := generateCacheKey(testUser, testPass)
|
|
vfs, vfsKey, err := p.Call(testUser, testPass, false)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, vfs)
|
|
assert.Equal(t, "proxy-"+cacheKey, vfs.Fs().Name())
|
|
assert.Equal(t, cacheKey, vfsKey)
|
|
|
|
// check it is in the cache
|
|
assert.Equal(t, 1, p.vfsCache.Entries())
|
|
cacheValue, ok := p.vfsCache.GetMaybe(cacheKey)
|
|
assert.True(t, ok)
|
|
cached, ok := cacheValue.(cacheEntry)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, vfs, cached.vfs)
|
|
|
|
// Test Get works while we have something in the cache
|
|
t.Run("Get", func(t *testing.T) {
|
|
assert.Equal(t, vfs, p.Get(cacheKey))
|
|
assert.Nil(t, p.Get("unknown"))
|
|
})
|
|
|
|
// now try again from the cache
|
|
vfs, vfsKey, err = p.Call(testUser, testPass, false)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, vfs)
|
|
assert.Equal(t, "proxy-"+cacheKey, vfs.Fs().Name())
|
|
assert.Equal(t, cacheKey, vfsKey)
|
|
|
|
// check cache is at the same level
|
|
assert.Equal(t, 1, p.vfsCache.Entries())
|
|
|
|
// A different password produces a different cache key, so it
|
|
// creates a fresh cache entry rather than hitting the existing
|
|
// one. Authentication itself is the proxy script's job.
|
|
vfs2, vfsKey2, err := p.Call(testUser, testPass+"different", false)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, vfs2)
|
|
assert.NotEqual(t, cacheKey, vfsKey2)
|
|
assert.Equal(t, 2, p.vfsCache.Entries())
|
|
|
|
// The underlying fs.Fs must also be a fresh instance from fs/cache
|
|
if vfs.Fs() == vfs2.Fs() {
|
|
t.Error("fs/cache returned the stale backend after auth change")
|
|
}
|
|
|
|
// If a cached entry's pwHash somehow doesn't match the supplied
|
|
// auth (eg a hash collision on the cache key), Call must reject
|
|
// it. Simulate by corrupting the cached pwHash.
|
|
entry := cacheEntry{vfs: vfs, pwHash: sha256.Sum256([]byte("tampered"))}
|
|
p.vfsCache.Put(cacheKey, entry)
|
|
vfs, vfsKey, err = p.Call(testUser, testPass, false)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "incorrect password")
|
|
require.Nil(t, vfs)
|
|
require.Equal(t, "", vfsKey)
|
|
})
|
|
|
|
privateKey, privateKeyErr := rsa.GenerateKey(rand.Reader, 2048)
|
|
if privateKeyErr != nil {
|
|
fs.Fatal(nil, "error generating test private key "+privateKeyErr.Error())
|
|
}
|
|
publicKey, publicKeyError := ssh.NewPublicKey(&privateKey.PublicKey)
|
|
if publicKeyError != nil {
|
|
fs.Fatal(nil, "error generating test public key "+publicKeyError.Error())
|
|
}
|
|
|
|
publicKeyString := base64.StdEncoding.EncodeToString(publicKey.Marshal())
|
|
|
|
t.Run("Call w/PublicKey", func(t *testing.T) {
|
|
// check cache empty
|
|
assert.Equal(t, 0, p.vfsCache.Entries())
|
|
defer p.vfsCache.Clear()
|
|
|
|
value, err := p.call(testUser, publicKeyString, true)
|
|
require.NoError(t, err)
|
|
entry, ok := value.(cacheEntry)
|
|
require.True(t, ok)
|
|
|
|
// check publicKey is correct in entry
|
|
require.NoError(t, err)
|
|
require.NotNil(t, entry.vfs)
|
|
f := entry.vfs.Fs()
|
|
require.NotNil(t, f)
|
|
cacheKey := generateCacheKey(testUser, publicKeyString)
|
|
assert.Equal(t, "proxy-"+cacheKey, f.Name())
|
|
assert.True(t, strings.HasPrefix(f.String(), "Local file system"))
|
|
|
|
// check it is in the cache
|
|
assert.Equal(t, 1, p.vfsCache.Entries())
|
|
cacheValue, ok := p.vfsCache.GetMaybe(cacheKey)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, value, cacheValue)
|
|
})
|
|
|
|
t.Run("call w/PublicKey", func(t *testing.T) {
|
|
// check cache empty
|
|
assert.Equal(t, 0, p.vfsCache.Entries())
|
|
defer p.vfsCache.Clear()
|
|
|
|
cacheKey := generateCacheKey(testUser, publicKeyString)
|
|
vfs, vfsKey, err := p.Call(
|
|
testUser,
|
|
publicKeyString,
|
|
true,
|
|
)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, vfs)
|
|
assert.Equal(t, "proxy-"+cacheKey, vfs.Fs().Name())
|
|
assert.Equal(t, cacheKey, vfsKey)
|
|
|
|
// check it is in the cache
|
|
assert.Equal(t, 1, p.vfsCache.Entries())
|
|
cacheValue, ok := p.vfsCache.GetMaybe(cacheKey)
|
|
assert.True(t, ok)
|
|
cached, ok := cacheValue.(cacheEntry)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, vfs, cached.vfs)
|
|
|
|
// Test Get works while we have something in the cache
|
|
t.Run("Get", func(t *testing.T) {
|
|
assert.Equal(t, vfs, p.Get(cacheKey))
|
|
assert.Nil(t, p.Get("unknown"))
|
|
})
|
|
|
|
// now try again from the cache
|
|
vfs, vfsKey, err = p.Call(testUser, publicKeyString, true)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, vfs)
|
|
assert.Equal(t, "proxy-"+cacheKey, vfs.Fs().Name())
|
|
assert.Equal(t, cacheKey, vfsKey)
|
|
|
|
// check cache is at the same level
|
|
assert.Equal(t, 1, p.vfsCache.Entries())
|
|
|
|
// A different public key produces a different cache key, so it
|
|
// creates a fresh cache entry rather than hitting the existing
|
|
// one. Authentication itself is the proxy script's job.
|
|
vfs2, vfsKey2, err := p.Call(testUser, publicKeyString+"different", true)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, vfs2)
|
|
assert.NotEqual(t, cacheKey, vfsKey2)
|
|
assert.Equal(t, 2, p.vfsCache.Entries())
|
|
|
|
// The underlying fs.Fs must be a fresh instance from fs/cache
|
|
if vfs.Fs() == vfs2.Fs() {
|
|
t.Error("fs/cache returned the stale backend after public key change")
|
|
}
|
|
|
|
// If a cached entry's pwHash somehow doesn't match the supplied
|
|
// auth (eg a hash collision on the cache key), Call must reject
|
|
// it. Simulate by corrupting the cached pwHash.
|
|
entry := cacheEntry{vfs: vfs, pwHash: sha256.Sum256([]byte("tampered"))}
|
|
p.vfsCache.Put(cacheKey, entry)
|
|
vfs, vfsKey, err = p.Call(testUser, publicKeyString, true)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "incorrect public key")
|
|
require.Nil(t, vfs)
|
|
require.Equal(t, "", vfsKey)
|
|
})
|
|
}
|