Remove flattening and replace with {off, standard} name encryption

This commit is contained in:
Nick Craig-Wood 2016-08-20 18:46:10 +01:00
parent 5f375a182d
commit 43eadf278c
4 changed files with 289 additions and 275 deletions

View file

@ -6,6 +6,7 @@ import (
gocipher "crypto/cipher" gocipher "crypto/cipher"
"crypto/rand" "crypto/rand"
"encoding/base32" "encoding/base32"
"fmt"
"io" "io"
"strings" "strings"
"sync" "sync"
@ -30,6 +31,7 @@ const (
blockHeaderSize = secretbox.Overhead blockHeaderSize = secretbox.Overhead
blockDataSize = 64 * 1024 blockDataSize = 64 * 1024
blockSize = blockHeaderSize + blockDataSize blockSize = blockHeaderSize + blockDataSize
encryptedSuffix = ".bin" // when file name encryption is off we add this suffix to make sure the cloud provider doesn't process the file
) )
// Errors returned by cipher // Errors returned by cipher
@ -43,10 +45,8 @@ var (
ErrorEncryptedBadMagic = errors.New("not an encrypted file - bad magic string") ErrorEncryptedBadMagic = errors.New("not an encrypted file - bad magic string")
ErrorEncryptedBadBlock = errors.New("failed to authenticate decrypted block - bad password?") ErrorEncryptedBadBlock = errors.New("failed to authenticate decrypted block - bad password?")
ErrorBadBase32Encoding = errors.New("bad base32 filename encoding") ErrorBadBase32Encoding = errors.New("bad base32 filename encoding")
ErrorBadSpreadNotSingleChar = errors.New("bad unspread - not single character")
ErrorBadSpreadResultTooShort = errors.New("bad unspread - result too short")
ErrorBadSpreadDidntMatch = errors.New("bad unspread - directory prefix didn't match")
ErrorFileClosed = errors.New("file already closed") ErrorFileClosed = errors.New("file already closed")
ErrorNotAnEncryptedFile = errors.New("not an encrypted file - no \"" + encryptedSuffix + "\" suffix")
defaultSalt = []byte{0xA8, 0x0D, 0xF4, 0x3A, 0x8F, 0xBD, 0x03, 0x08, 0xA7, 0xCA, 0xB8, 0x3E, 0x58, 0x1F, 0x86, 0xB1} defaultSalt = []byte{0xA8, 0x0D, 0xF4, 0x3A, 0x8F, 0xBD, 0x03, 0x08, 0xA7, 0xCA, 0xB8, 0x3E, 0x58, 0x1F, 0x86, 0xB1}
) )
@ -57,10 +57,14 @@ var (
// Cipher is used to swap out the encryption implementations // Cipher is used to swap out the encryption implementations
type Cipher interface { type Cipher interface {
// EncryptName encrypts a file path // EncryptFileName encrypts a file path
EncryptName(string) string EncryptFileName(string) string
// DecryptName decrypts a file path, returns error if decrypt was invalid // DecryptFileName decrypts a file path, returns error if decrypt was invalid
DecryptName(string) (string, error) DecryptFileName(string) (string, error)
// EncryptDirName encrypts a directory path
EncryptDirName(string) string
// DecryptDirName decrypts a directory path, returns error if decrypt was invalid
DecryptDirName(string) (string, error)
// EncryptData // EncryptData
EncryptData(io.Reader) (io.Reader, error) EncryptData(io.Reader) (io.Reader, error)
// DecryptData // DecryptData
@ -71,20 +75,56 @@ type Cipher interface {
DecryptedSize(int64) (int64, error) DecryptedSize(int64) (int64, error)
} }
// NameEncryptionMode is the type of file name encryption in use
type NameEncryptionMode int
// NameEncryptionMode levels
const (
NameEncryptionOff NameEncryptionMode = iota
NameEncryptionStandard
)
// NewNameEncryptionMode turns a string into a NameEncryptionMode
func NewNameEncryptionMode(s string) (mode NameEncryptionMode, err error) {
s = strings.ToLower(s)
switch s {
case "off":
mode = NameEncryptionOff
case "standard":
mode = NameEncryptionStandard
default:
err = errors.Errorf("Unknown file name encryption mode %q", s)
}
return mode, err
}
// String turns mode into a human readable string
func (mode NameEncryptionMode) String() (out string) {
switch mode {
case NameEncryptionOff:
out = "off"
case NameEncryptionStandard:
out = "standard"
default:
out = fmt.Sprintf("Unknown mode #%d", mode)
}
return out
}
type cipher struct { type cipher struct {
dataKey [32]byte // Key for secretbox dataKey [32]byte // Key for secretbox
nameKey [32]byte // 16,24 or 32 bytes nameKey [32]byte // 16,24 or 32 bytes
nameTweak [nameCipherBlockSize]byte // used to tweak the name crypto nameTweak [nameCipherBlockSize]byte // used to tweak the name crypto
block gocipher.Block block gocipher.Block
flatten int // set flattening level - 0 is off mode NameEncryptionMode
buffers sync.Pool // encrypt/decrypt buffers buffers sync.Pool // encrypt/decrypt buffers
cryptoRand io.Reader // read crypto random numbers from here cryptoRand io.Reader // read crypto random numbers from here
} }
// newCipher initialises the cipher. If salt is "" then it uses a built in salt val // newCipher initialises the cipher. If salt is "" then it uses a built in salt val
func newCipher(flatten int, password, salt string) (*cipher, error) { func newCipher(mode NameEncryptionMode, password, salt string) (*cipher, error) {
c := &cipher{ c := &cipher{
flatten: flatten, mode: mode,
cryptoRand: rand.Reader, cryptoRand: rand.Reader,
} }
c.buffers.New = func() interface{} { c.buffers.New = func() interface{} {
@ -231,50 +271,8 @@ func (c *cipher) decryptSegment(ciphertext string) (string, error) {
return string(plaintext), err return string(plaintext), err
} }
// spread a name over the given number of directory levels // encryptFileName encrypts a file path
// func (c *cipher) encryptFileName(in string) string {
// if in isn't long enough dirs will be reduces
func spreadName(dirs int, in string) string {
if dirs > len(in) {
dirs = len(in)
}
prefix := ""
for i := 0; i < dirs; i++ {
prefix += string(in[i]) + "/"
}
return prefix + in
}
// reverse spreadName, returning an error if not in spread format
//
// This decodes any level of spreading
func unspreadName(in string) (string, error) {
in = strings.ToLower(in)
segments := strings.Split(in, "/")
if len(segments) == 0 {
return in, nil
}
out := segments[len(segments)-1]
segments = segments[:len(segments)-1]
for i, s := range segments {
if len(s) != 1 {
return "", ErrorBadSpreadNotSingleChar
}
if i >= len(out) {
return "", ErrorBadSpreadResultTooShort
}
if s[0] != out[i] {
return "", ErrorBadSpreadDidntMatch
}
}
return out, nil
}
// EncryptName encrypts a file path
func (c *cipher) EncryptName(in string) string {
if c.flatten > 0 {
return spreadName(c.flatten, c.encryptSegment(in))
}
segments := strings.Split(in, "/") segments := strings.Split(in, "/")
for i := range segments { for i := range segments {
segments[i] = c.encryptSegment(segments[i]) segments[i] = c.encryptSegment(segments[i])
@ -282,15 +280,24 @@ func (c *cipher) EncryptName(in string) string {
return strings.Join(segments, "/") return strings.Join(segments, "/")
} }
// DecryptName decrypts a file path // EncryptFileName encrypts a file path
func (c *cipher) DecryptName(in string) (string, error) { func (c *cipher) EncryptFileName(in string) string {
if c.flatten > 0 { if c.mode == NameEncryptionOff {
unspread, err := unspreadName(in) return in + encryptedSuffix
if err != nil {
return "", err
} }
return c.decryptSegment(unspread) return c.encryptFileName(in)
} }
// EncryptDirName encrypts a directory path
func (c *cipher) EncryptDirName(in string) string {
if c.mode == NameEncryptionOff {
return in
}
return c.encryptFileName(in)
}
// decryptFileName decrypts a file path
func (c *cipher) decryptFileName(in string) (string, error) {
segments := strings.Split(in, "/") segments := strings.Split(in, "/")
for i := range segments { for i := range segments {
var err error var err error
@ -302,6 +309,26 @@ func (c *cipher) DecryptName(in string) (string, error) {
return strings.Join(segments, "/"), nil return strings.Join(segments, "/"), nil
} }
// DecryptFileName decrypts a file path
func (c *cipher) DecryptFileName(in string) (string, error) {
if c.mode == NameEncryptionOff {
remainingLength := len(in) - len(encryptedSuffix)
if remainingLength > 0 && strings.HasSuffix(in, encryptedSuffix) {
return in[:remainingLength], nil
}
return "", ErrorNotAnEncryptedFile
}
return c.decryptFileName(in)
}
// DecryptDirName decrypts a directory path
func (c *cipher) DecryptDirName(in string) (string, error) {
if c.mode == NameEncryptionOff {
return in, nil
}
return c.decryptFileName(in)
}
// nonce is an NACL secretbox nonce // nonce is an NACL secretbox nonce
type nonce [fileNonceSize]byte type nonce [fileNonceSize]byte

View file

@ -15,6 +15,32 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestNewNameEncryptionMode(t *testing.T) {
for _, test := range []struct {
in string
expected NameEncryptionMode
expectedErr string
}{
{"off", NameEncryptionOff, ""},
{"standard", NameEncryptionStandard, ""},
{"potato", NameEncryptionMode(0), "Unknown file name encryption mode \"potato\""},
} {
actual, actualErr := NewNameEncryptionMode(test.in)
assert.Equal(t, actual, test.expected)
if test.expectedErr == "" {
assert.NoError(t, actualErr)
} else {
assert.Error(t, actualErr, test.expectedErr)
}
}
}
func TestNewNameEncryptionModeString(t *testing.T) {
assert.Equal(t, NameEncryptionOff.String(), "off")
assert.Equal(t, NameEncryptionStandard.String(), "standard")
assert.Equal(t, NameEncryptionMode(2).String(), "Unknown mode #2")
}
func TestValidString(t *testing.T) { func TestValidString(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
in string in string
@ -129,7 +155,7 @@ func TestDecodeFileName(t *testing.T) {
} }
func TestEncryptSegment(t *testing.T) { func TestEncryptSegment(t *testing.T) {
c, _ := newCipher(0, "", "") c, _ := newCipher(NameEncryptionStandard, "", "")
for _, test := range []struct { for _, test := range []struct {
in string in string
expected string expected string
@ -166,7 +192,7 @@ func TestEncryptSegment(t *testing.T) {
func TestDecryptSegment(t *testing.T) { func TestDecryptSegment(t *testing.T) {
// We've tested the forwards above, now concentrate on the errors // We've tested the forwards above, now concentrate on the errors
c, _ := newCipher(0, "", "") c, _ := newCipher(NameEncryptionStandard, "", "")
for _, test := range []struct { for _, test := range []struct {
in string in string
expectedErr error expectedErr error
@ -184,87 +210,78 @@ func TestDecryptSegment(t *testing.T) {
} }
} }
func TestSpreadName(t *testing.T) { func TestEncryptFileName(t *testing.T) {
for _, test := range []struct { // First standard mode
n int c, _ := newCipher(NameEncryptionStandard, "", "")
in string assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s", c.EncryptFileName("1"))
expected string assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng", c.EncryptFileName("1/12"))
}{ assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng/qgm4avr35m5loi1th53ato71v0", c.EncryptFileName("1/12/123"))
{3, "", ""}, // Now off mode
{0, "abcdefg", "abcdefg"}, c, _ = newCipher(NameEncryptionOff, "", "")
{1, "abcdefg", "a/abcdefg"}, assert.Equal(t, "1/12/123.bin", c.EncryptFileName("1/12/123"))
{2, "abcdefg", "a/b/abcdefg"},
{3, "abcdefg", "a/b/c/abcdefg"},
{4, "abcdefg", "a/b/c/d/abcdefg"},
{4, "abcd", "a/b/c/d/abcd"},
{4, "abc", "a/b/c/abc"},
{4, "ab", "a/b/ab"},
{4, "a", "a/a"},
} {
actual := spreadName(test.n, test.in)
assert.Equal(t, test.expected, actual, fmt.Sprintf("Testing %d,%q", test.n, test.in))
recovered, err := unspreadName(test.expected)
assert.NoError(t, err, fmt.Sprintf("Testing reverse %q", test.expected))
assert.Equal(t, test.in, recovered, fmt.Sprintf("Testing reverse %q", test.expected))
}
} }
func TestUnspreadName(t *testing.T) { func TestDecryptFileName(t *testing.T) {
// We've tested the forwards above, now concentrate on the errors
for _, test := range []struct { for _, test := range []struct {
in string mode NameEncryptionMode
expectedErr error
}{
{"aa/bc", ErrorBadSpreadNotSingleChar},
{"/", ErrorBadSpreadNotSingleChar},
{"a/", ErrorBadSpreadResultTooShort},
{"a/b/c/ab", ErrorBadSpreadResultTooShort},
{"a/b/x/abc", ErrorBadSpreadDidntMatch},
{"a/b/c/ABC", nil},
} {
actual, actualErr := unspreadName(test.in)
assert.Equal(t, test.expectedErr, actualErr, fmt.Sprintf("in=%q got actual=%q, err = %v %T", test.in, actual, actualErr, actualErr))
}
}
func TestEncryptName(t *testing.T) {
// First no flatten
c, _ := newCipher(0, "", "")
assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s", c.EncryptName("1"))
assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng", c.EncryptName("1/12"))
assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng/qgm4avr35m5loi1th53ato71v0", c.EncryptName("1/12/123"))
// Now with flatten
c, _ = newCipher(3, "", "")
assert.Equal(t, "k/g/t/kgtickdcigo7600huebjl3ubu4", c.EncryptName("1/12/123"))
}
func TestDecryptName(t *testing.T) {
for _, test := range []struct {
flatten int
in string in string
expected string expected string
expectedErr error expectedErr error
}{ }{
{0, "p0e52nreeaj0a5ea7s64m4j72s", "1", nil}, {NameEncryptionStandard, "p0e52nreeaj0a5ea7s64m4j72s", "1", nil},
{0, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng", "1/12", nil}, {NameEncryptionStandard, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng", "1/12", nil},
{0, "p0e52nreeAJ0A5EA7S64M4J72S/L42G6771HNv3an9cgc8cr2n1ng", "1/12", nil}, {NameEncryptionStandard, "p0e52nreeAJ0A5EA7S64M4J72S/L42G6771HNv3an9cgc8cr2n1ng", "1/12", nil},
{0, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng/qgm4avr35m5loi1th53ato71v0", "1/12/123", nil}, {NameEncryptionStandard, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng/qgm4avr35m5loi1th53ato71v0", "1/12/123", nil},
{0, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1/qgm4avr35m5loi1th53ato71v0", "", ErrorNotAMultipleOfBlocksize}, {NameEncryptionStandard, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1/qgm4avr35m5loi1th53ato71v0", "", ErrorNotAMultipleOfBlocksize},
{3, "k/g/t/kgtickdcigo7600huebjl3ubu4", "1/12/123", nil}, {NameEncryptionOff, "1/12/123.bin", "1/12/123", nil},
{1, "k/g/t/kgtickdcigo7600huebjl3ubu4", "1/12/123", nil}, {NameEncryptionOff, "1/12/123.bix", "", ErrorNotAnEncryptedFile},
{1, "k/g/t/i/kgtickdcigo7600huebjl3ubu4", "1/12/123", nil}, {NameEncryptionOff, ".bin", "", ErrorNotAnEncryptedFile},
{1, "k/x/t/i/kgtickdcigo7600huebjl3ubu4", "", ErrorBadSpreadDidntMatch},
} { } {
c, _ := newCipher(test.flatten, "", "") c, _ := newCipher(test.mode, "", "")
actual, actualErr := c.DecryptName(test.in) actual, actualErr := c.DecryptFileName(test.in)
what := fmt.Sprintf("Testing %q (flatten=%d)", test.in, test.flatten) what := fmt.Sprintf("Testing %q (mode=%v)", test.in, test.mode)
assert.Equal(t, test.expected, actual, what)
assert.Equal(t, test.expectedErr, actualErr, what)
}
}
func TestEncryptDirName(t *testing.T) {
// First standard mode
c, _ := newCipher(NameEncryptionStandard, "", "")
assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s", c.EncryptDirName("1"))
assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng", c.EncryptDirName("1/12"))
assert.Equal(t, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng/qgm4avr35m5loi1th53ato71v0", c.EncryptDirName("1/12/123"))
// Now off mode
c, _ = newCipher(NameEncryptionOff, "", "")
assert.Equal(t, "1/12/123", c.EncryptDirName("1/12/123"))
}
func TestDecryptDirName(t *testing.T) {
for _, test := range []struct {
mode NameEncryptionMode
in string
expected string
expectedErr error
}{
{NameEncryptionStandard, "p0e52nreeaj0a5ea7s64m4j72s", "1", nil},
{NameEncryptionStandard, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng", "1/12", nil},
{NameEncryptionStandard, "p0e52nreeAJ0A5EA7S64M4J72S/L42G6771HNv3an9cgc8cr2n1ng", "1/12", nil},
{NameEncryptionStandard, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1ng/qgm4avr35m5loi1th53ato71v0", "1/12/123", nil},
{NameEncryptionStandard, "p0e52nreeaj0a5ea7s64m4j72s/l42g6771hnv3an9cgc8cr2n1/qgm4avr35m5loi1th53ato71v0", "", ErrorNotAMultipleOfBlocksize},
{NameEncryptionOff, "1/12/123.bin", "1/12/123.bin", nil},
{NameEncryptionOff, "1/12/123", "1/12/123", nil},
{NameEncryptionOff, ".bin", ".bin", nil},
} {
c, _ := newCipher(test.mode, "", "")
actual, actualErr := c.DecryptDirName(test.in)
what := fmt.Sprintf("Testing %q (mode=%v)", test.in, test.mode)
assert.Equal(t, test.expected, actual, what) assert.Equal(t, test.expected, actual, what)
assert.Equal(t, test.expectedErr, actualErr, what) assert.Equal(t, test.expectedErr, actualErr, what)
} }
} }
func TestEncryptedSize(t *testing.T) { func TestEncryptedSize(t *testing.T) {
c, _ := newCipher(0, "", "") c, _ := newCipher(NameEncryptionStandard, "", "")
for _, test := range []struct { for _, test := range []struct {
in int64 in int64
expected int64 expected int64
@ -288,7 +305,7 @@ func TestEncryptedSize(t *testing.T) {
func TestDecryptedSize(t *testing.T) { func TestDecryptedSize(t *testing.T) {
// Test the errors since we tested the reverse above // Test the errors since we tested the reverse above
c, _ := newCipher(0, "", "") c, _ := newCipher(NameEncryptionStandard, "", "")
for _, test := range []struct { for _, test := range []struct {
in int64 in int64
expectedErr error expectedErr error
@ -521,7 +538,7 @@ func (z *zeroes) Read(p []byte) (n int, err error) {
// Test encrypt decrypt with different buffer sizes // Test encrypt decrypt with different buffer sizes
func testEncryptDecrypt(t *testing.T, bufSize int, copySize int64) { func testEncryptDecrypt(t *testing.T, bufSize int, copySize int64) {
c, err := newCipher(0, "", "") c, err := newCipher(NameEncryptionStandard, "", "")
assert.NoError(t, err) assert.NoError(t, err)
c.cryptoRand = &zeroes{} // zero out the nonce c.cryptoRand = &zeroes{} // zero out the nonce
buf := make([]byte, bufSize) buf := make([]byte, bufSize)
@ -591,7 +608,7 @@ func TestEncryptData(t *testing.T) {
{[]byte{1}, file1}, {[]byte{1}, file1},
{[]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, file16}, {[]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, file16},
} { } {
c, err := newCipher(0, "", "") c, err := newCipher(NameEncryptionStandard, "", "")
assert.NoError(t, err) assert.NoError(t, err)
c.cryptoRand = newRandomSource(1E8) // nodge the crypto rand generator c.cryptoRand = newRandomSource(1E8) // nodge the crypto rand generator
@ -614,7 +631,7 @@ func TestEncryptData(t *testing.T) {
} }
func TestNewEncrypter(t *testing.T) { func TestNewEncrypter(t *testing.T) {
c, err := newCipher(0, "", "") c, err := newCipher(NameEncryptionStandard, "", "")
assert.NoError(t, err) assert.NoError(t, err)
c.cryptoRand = newRandomSource(1E8) // nodge the crypto rand generator c.cryptoRand = newRandomSource(1E8) // nodge the crypto rand generator
@ -658,7 +675,7 @@ func (c *closeDetector) Close() error {
} }
func TestNewDecrypter(t *testing.T) { func TestNewDecrypter(t *testing.T) {
c, err := newCipher(0, "", "") c, err := newCipher(NameEncryptionStandard, "", "")
assert.NoError(t, err) assert.NoError(t, err)
c.cryptoRand = newRandomSource(1E8) // nodge the crypto rand generator c.cryptoRand = newRandomSource(1E8) // nodge the crypto rand generator
@ -700,7 +717,7 @@ func TestNewDecrypter(t *testing.T) {
} }
func TestDecrypterRead(t *testing.T) { func TestDecrypterRead(t *testing.T) {
c, err := newCipher(0, "", "") c, err := newCipher(NameEncryptionStandard, "", "")
assert.NoError(t, err) assert.NoError(t, err)
// Test truncating the header // Test truncating the header
@ -744,7 +761,7 @@ func TestDecrypterRead(t *testing.T) {
} }
func TestDecrypterClose(t *testing.T) { func TestDecrypterClose(t *testing.T) {
c, err := newCipher(0, "", "") c, err := newCipher(NameEncryptionStandard, "", "")
assert.NoError(t, err) assert.NoError(t, err)
cd := newCloseDetector(bytes.NewBuffer(file16)) cd := newCloseDetector(bytes.NewBuffer(file16))
@ -780,7 +797,7 @@ func TestDecrypterClose(t *testing.T) {
} }
func TestPutGetBlock(t *testing.T) { func TestPutGetBlock(t *testing.T) {
c, err := newCipher(0, "", "") c, err := newCipher(NameEncryptionStandard, "", "")
assert.NoError(t, err) assert.NoError(t, err)
block := c.getBlock() block := c.getBlock()
@ -791,7 +808,7 @@ func TestPutGetBlock(t *testing.T) {
} }
func TestKey(t *testing.T) { func TestKey(t *testing.T) {
c, err := newCipher(0, "", "") c, err := newCipher(NameEncryptionStandard, "", "")
assert.NoError(t, err) assert.NoError(t, err)
// Check zero keys OK // Check zero keys OK

View file

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"io" "io"
"path" "path"
"strings"
"sync" "sync"
"github.com/ncw/rclone/fs" "github.com/ncw/rclone/fs"
@ -22,27 +21,15 @@ func init() {
Name: "remote", Name: "remote",
Help: "Remote to encrypt/decrypt.", Help: "Remote to encrypt/decrypt.",
}, { }, {
Name: "flatten", Name: "filename_encryption",
Help: "Flatten the directory structure - more secure, less useful - see docs for tradeoffs.", Help: "How to encrypt the filenames.",
Examples: []fs.OptionExample{ Examples: []fs.OptionExample{
{ {
Value: "0", Value: "off",
Help: "Don't flatten files (default) - good for unlimited files, but doesn't hide directory structure.", Help: "Don't encrypt the file names. Adds a \".bin\" extension only.",
}, { }, {
Value: "1", Value: "standard",
Help: "Spread files over 1 directory good for <10,000 files.", Help: "Encrypt the filenames see the docs for the details.",
}, {
Value: "2",
Help: "Spread files over 32 directories good for <320,000 files.",
}, {
Value: "3",
Help: "Spread files over 1024 directories good for <10,000,000 files.",
}, {
Value: "4",
Help: "Spread files over 32,768 directories good for <320,000,000 files.",
}, {
Value: "5",
Help: "Spread files over 1,048,576 levels good for <10,000,000,000 files.",
}, },
}, },
}, { }, {
@ -60,12 +47,15 @@ func init() {
// NewFs contstructs an Fs from the path, container:path // NewFs contstructs an Fs from the path, container:path
func NewFs(name, rpath string) (fs.Fs, error) { func NewFs(name, rpath string) (fs.Fs, error) {
flatten := fs.ConfigFile.MustInt(name, "flatten", 0) mode, err := NewNameEncryptionMode(fs.ConfigFile.MustValue(name, "filename_encryption", "standard"))
if err != nil {
return nil, err
}
password := fs.ConfigFile.MustValue(name, "password", "") password := fs.ConfigFile.MustValue(name, "password", "")
if password == "" { if password == "" {
return nil, errors.New("password not set in config file") return nil, errors.New("password not set in config file")
} }
password, err := fs.Reveal(password) password, err = fs.Reveal(password)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to decrypt password") return nil, errors.Wrap(err, "failed to decrypt password")
} }
@ -76,20 +66,26 @@ func NewFs(name, rpath string) (fs.Fs, error) {
return nil, errors.Wrap(err, "failed to decrypt password2") return nil, errors.Wrap(err, "failed to decrypt password2")
} }
} }
cipher, err := newCipher(flatten, password, salt) cipher, err := newCipher(mode, password, salt)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to make cipher") return nil, errors.Wrap(err, "failed to make cipher")
} }
remote := fs.ConfigFile.MustValue(name, "remote") remote := fs.ConfigFile.MustValue(name, "remote")
remotePath := path.Join(remote, cipher.EncryptName(rpath)) // Look for a file first
remotePath := path.Join(remote, cipher.EncryptFileName(rpath))
wrappedFs, err := fs.NewFs(remotePath) wrappedFs, err := fs.NewFs(remotePath)
// if that didn't produce a file, look for a directory
if err != fs.ErrorIsFile {
remotePath = path.Join(remote, cipher.EncryptDirName(rpath))
wrappedFs, err = fs.NewFs(remotePath)
}
if err != fs.ErrorIsFile && err != nil { if err != fs.ErrorIsFile && err != nil {
return nil, errors.Wrapf(err, "failed to make remote %q to wrap", remotePath) return nil, errors.Wrapf(err, "failed to make remote %q to wrap", remotePath)
} }
f := &Fs{ f := &Fs{
Fs: wrappedFs, Fs: wrappedFs,
cipher: cipher, cipher: cipher,
flatten: flatten, mode: mode,
} }
return f, err return f, err
} }
@ -98,7 +94,7 @@ func NewFs(name, rpath string) (fs.Fs, error) {
type Fs struct { type Fs struct {
fs.Fs fs.Fs
cipher Cipher cipher Cipher
flatten int mode NameEncryptionMode
} }
// String returns a description of the FS // String returns a description of the FS
@ -108,12 +104,12 @@ func (f *Fs) String() string {
// List the Fs into a channel // List the Fs into a channel
func (f *Fs) List(opts fs.ListOpts, dir string) { func (f *Fs) List(opts fs.ListOpts, dir string) {
f.Fs.List(f.newListOpts(opts, dir), f.cipher.EncryptName(dir)) f.Fs.List(f.newListOpts(opts, dir), f.cipher.EncryptDirName(dir))
} }
// NewObject finds the Object at remote. // NewObject finds the Object at remote.
func (f *Fs) NewObject(remote string) (fs.Object, error) { func (f *Fs) NewObject(remote string) (fs.Object, error) {
o, err := f.Fs.NewObject(f.cipher.EncryptName(remote)) o, err := f.Fs.NewObject(f.cipher.EncryptFileName(remote))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -174,7 +170,7 @@ func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) {
if !ok { if !ok {
return nil, fs.ErrorCantCopy return nil, fs.ErrorCantCopy
} }
oResult, err := do.Copy(o.Object, f.cipher.EncryptName(remote)) oResult, err := do.Copy(o.Object, f.cipher.EncryptFileName(remote))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -199,7 +195,7 @@ func (f *Fs) Move(src fs.Object, remote string) (fs.Object, error) {
if !ok { if !ok {
return nil, fs.ErrorCantCopy return nil, fs.ErrorCantCopy
} }
oResult, err := do.Move(o.Object, f.cipher.EncryptName(remote)) oResult, err := do.Move(o.Object, f.cipher.EncryptFileName(remote))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -242,7 +238,7 @@ func (o *Object) String() string {
// Remote returns the remote path // Remote returns the remote path
func (o *Object) Remote() string { func (o *Object) Remote() string {
remote := o.Object.Remote() remote := o.Object.Remote()
decryptedName, err := o.f.cipher.DecryptName(remote) decryptedName, err := o.f.cipher.DecryptFileName(remote)
if err != nil { if err != nil {
fs.Debug(remote, "Undecryptable file name: %v", err) fs.Debug(remote, "Undecryptable file name: %v", err)
return remote return remote
@ -287,7 +283,7 @@ func (o *Object) Update(in io.Reader, src fs.ObjectInfo) error {
func (f *Fs) newDir(dir *fs.Dir) *fs.Dir { func (f *Fs) newDir(dir *fs.Dir) *fs.Dir {
new := *dir new := *dir
remote := dir.Name remote := dir.Name
decryptedRemote, err := f.cipher.DecryptName(remote) decryptedRemote, err := f.cipher.DecryptDirName(remote)
if err != nil { if err != nil {
fs.Debug(remote, "Undecryptable dir name: %v", err) fs.Debug(remote, "Undecryptable dir name: %v", err)
} else { } else {
@ -318,7 +314,7 @@ func (o *ObjectInfo) Fs() fs.Info {
// Remote returns the remote path // Remote returns the remote path
func (o *ObjectInfo) Remote() string { func (o *ObjectInfo) Remote() string {
return o.f.cipher.EncryptName(o.ObjectInfo.Remote()) return o.f.cipher.EncryptFileName(o.ObjectInfo.Remote())
} }
// Size returns the size of the file // Size returns the size of the file
@ -358,55 +354,19 @@ func (f *Fs) newListOpts(lo fs.ListOpts, dir string) *ListOpts {
// //
// Each returned item must have less than level `/`s in. // Each returned item must have less than level `/`s in.
func (lo *ListOpts) Level() int { func (lo *ListOpts) Level() int {
// If flattened recurse fully
if lo.f.flatten > 0 {
return fs.MaxLevel
}
return lo.ListOpts.Level() return lo.ListOpts.Level()
} }
// addSyntheticDirs makes up directory objects for the path passed in
func (lo *ListOpts) addSyntheticDirs(path string) {
lo.mu.Lock()
defer lo.mu.Unlock()
for {
i := strings.LastIndexByte(path, '/')
if i < 0 {
break
}
path = path[:i]
if path == "" {
break
}
if _, found := lo.dirs[path]; found {
break
}
slashes := strings.Count(path, "/")
if slashes < lo.ListOpts.Level() {
lo.ListOpts.AddDir(&fs.Dir{Name: path})
}
lo.dirs[path] = struct{}{}
}
}
// Add an object to the output. // Add an object to the output.
// If the function returns true, the operation has been aborted. // If the function returns true, the operation has been aborted.
// Multiple goroutines can safely add objects concurrently. // Multiple goroutines can safely add objects concurrently.
func (lo *ListOpts) Add(obj fs.Object) (abort bool) { func (lo *ListOpts) Add(obj fs.Object) (abort bool) {
remote := obj.Remote() remote := obj.Remote()
decryptedRemote, err := lo.f.cipher.DecryptName(remote) _, err := lo.f.cipher.DecryptFileName(remote)
if err != nil { if err != nil {
fs.Debug(remote, "Skipping undecryptable file name: %v", err) fs.Debug(remote, "Skipping undecryptable file name: %v", err)
return lo.ListOpts.IsFinished() return lo.ListOpts.IsFinished()
} }
// If flattened add synthetic directories
if lo.f.flatten > 0 {
lo.addSyntheticDirs(decryptedRemote)
slashes := strings.Count(decryptedRemote, "/")
if slashes >= lo.ListOpts.Level() {
return lo.ListOpts.IsFinished()
}
}
return lo.ListOpts.Add(lo.f.newObject(obj)) return lo.ListOpts.Add(lo.f.newObject(obj))
} }
@ -414,12 +374,8 @@ func (lo *ListOpts) Add(obj fs.Object) (abort bool) {
// If the function returns true, the operation has been aborted. // If the function returns true, the operation has been aborted.
// Multiple goroutines can safely add objects concurrently. // Multiple goroutines can safely add objects concurrently.
func (lo *ListOpts) AddDir(dir *fs.Dir) (abort bool) { func (lo *ListOpts) AddDir(dir *fs.Dir) (abort bool) {
// If flattened we don't add any directories from the underlying remote
if lo.f.flatten > 0 {
return lo.ListOpts.IsFinished()
}
remote := dir.Name remote := dir.Name
_, err := lo.f.cipher.DecryptName(remote) _, err := lo.f.cipher.DecryptDirName(remote)
if err != nil { if err != nil {
fs.Debug(remote, "Skipping undecryptable dir name: %v", err) fs.Debug(remote, "Skipping undecryptable dir name: %v", err)
return lo.ListOpts.IsFinished() return lo.ListOpts.IsFinished()
@ -430,11 +386,7 @@ func (lo *ListOpts) AddDir(dir *fs.Dir) (abort bool) {
// IncludeDirectory returns whether this directory should be // IncludeDirectory returns whether this directory should be
// included in the listing (and recursed into or not). // included in the listing (and recursed into or not).
func (lo *ListOpts) IncludeDirectory(remote string) bool { func (lo *ListOpts) IncludeDirectory(remote string) bool {
// If flattened we look in all directories decryptedRemote, err := lo.f.cipher.DecryptDirName(remote)
if lo.f.flatten > 0 {
return true
}
decryptedRemote, err := lo.f.cipher.DecryptName(remote)
if err != nil { if err != nil {
fs.Debug(remote, "Not including undecryptable directory name: %v", err) fs.Debug(remote, "Not including undecryptable directory name: %v", err)
return false return false

View file

@ -19,8 +19,8 @@ First check your chosen remote is working - we'll call it
will be encrypted and anything outside won't. This means that if you will be encrypted and anything outside won't. This means that if you
are using a bucket based remote (eg S3, B2, swift) then you should are using a bucket based remote (eg S3, B2, swift) then you should
probably put the bucket in the remote `s3:bucket`. If you just use probably put the bucket in the remote `s3:bucket`. If you just use
`s3:` then rclone will make encrypted bucket names too which may or `s3:` then rclone will make encrypted bucket names too (if using file
may not be what you want. name encryption) which may or may not be what you want.
Now configure `crypt` using `rclone config`. We will call this one Now configure `crypt` using `rclone config`. We will call this one
`secret` to differentiate it from the `remote`. `secret` to differentiate it from the `remote`.
@ -61,32 +61,44 @@ Choose a number from below, or type in your own value
Storage> 5 Storage> 5
Remote to encrypt/decrypt. Remote to encrypt/decrypt.
remote> remote:path remote> remote:path
Flatten the directory structure - more secure, less useful - see docs for tradeoffs. How to encrypt the filenames.
Choose a number from below, or type in your own value Choose a number from below, or type in your own value
1 / Don't flatten files (default) - good for unlimited files, but doesn't hide directory structure. 1 / Don't encrypt the file names. Adds a ".bin" extension only.
\ "0" \ "off"
2 / Spread files over 1 directory good for <10,000 files. 2 / Encrypt the filenames see the docs for the details.
\ "1" \ "standard"
3 / Spread files over 32 directories good for <320,000 files. filename_encryption> 2
\ "2"
4 / Spread files over 1024 directories good for <10,000,000 files.
\ "3"
5 / Spread files over 32,768 directories good for <320,000,000 files.
\ "4"
6 / Spread files over 1,048,576 levels good for <10,000,000,000 files.
\ "5"
flatten> 1
Password or pass phrase for encryption. Password or pass phrase for encryption.
y) Yes type in my own password
g) Generate random password
y/g> y
Enter the password: Enter the password:
password: password:
Confirm the password: Confirm the password:
password: password:
Password or pass phrase for salt. Optional but recommended.
Should be different to the previous password.
y) Yes type in my own password
g) Generate random password
n) No leave this optional password blank
y/g/n> g
Password strength in bits.
64 is just about memorable
128 is secure
1024 is the maximum
Bits> 128
Your password is: JAsJvRcgR-_veXNfy_sGmQ
Use this password?
y) Yes
n) No
y/n> y
Remote config Remote config
-------------------- --------------------
[secret] [secret]
remote = remote:path remote = remote:path
flatten = 0 filename_encryption = standard
password = 0_gtCJ422bzwAWP0UN2lggrjhA-sSg password = CfDxopZIXFG0Oo-ac7dPLWWOHkNJbw
password2 = HYUpfuzHJL8qnX9fOaIYijq0xnVLwyVzp3y4SF3TwYqAU6HLysk
-------------------- --------------------
y) Yes this is OK y) Yes this is OK
e) Edit this remote e) Edit this remote
@ -99,9 +111,9 @@ obscured so it isn't immediately obvious what it is. It is in no way
secure unless you use config file encryption. secure unless you use config file encryption.
A long passphrase is recommended, or you can use a random one. Note A long passphrase is recommended, or you can use a random one. Note
that if you reconfigure rclone with the same password/passphrase that if you reconfigure rclone with the same passwords/passphrases
elsewhere it will be compatible - all the secrets used are derived elsewhere it will be compatible - all the secrets used are derived
from that one password/passphrase. from those two passwords/passphrases.
Note that rclone does not encrypt Note that rclone does not encrypt
* file length - this can be calcuated within 16 bytes * file length - this can be calcuated within 16 bytes
@ -109,7 +121,8 @@ Note that rclone does not encrypt
## Example ## ## Example ##
To test I made a little directory of files To test I made a little directory of files using "standard" file name
encryption.
``` ```
plaintext/ plaintext/
@ -154,39 +167,43 @@ $ rclone -q ls secret:subdir
10 subsubdir/file4.txt 10 subsubdir/file4.txt
``` ```
If you use the flattened flag then the listing will look and that last command will not work. If don't use file name encryption then the remote will look like this
- note the `.bin` extensions added to prevent the cloud provider
attempting to interpret the data.
``` ```
$ rclone -q ls remote:path $ rclone -q ls remote:path
56 t/tsdtcpdu6g9dpamn6poqc248tll9dj5ok78a363etmq8ushr821g 54 file0.txt.bin
57 g/gsrp2g0u85pgsi6kso74bjsrsafe11odpfln8qqpj6n9p20of0a0 57 subdir/file3.txt.bin
55 h/hagjclgavj2mbiqm6u6cnjjqcg 56 subdir/file2.txt.bin
58 4/4jsbao3dhi0jfoubt2oo493pbqmsshn92q01ddu7dg6428rlluhg 58 subdir/subsubdir/file4.txt.bin
54 v/v05749mltvv1tf4onltun46gls 55 file1.txt.bin
``` ```
### Flattened vs non-Flattened ### ### File name encryption modes ###
Pros and cons of each Here are some of the features of the file name encryption modes
Flattened Off
* hides directory structures * doesn't hide file names or directory structure
* identical file names won't have identical encrypted names * allows for longer file names (~246 characters)
* can't use a sub path * can use sub paths and copy single files
* doesn't work: `rclone copy crypt:sub/dir /tmp/recovered`
* use: `rclone copy --include "/sub/dir/**" crypt: /tmp/recovered`
* will always have to recurse through the entire directory structure
* can't copy a single file directly
* doesn't work: `rclone copy crypt:path/to/file /tmp/recovered`
* use: `rclone copy --include "/path/to/file" crypt: /tmp/recovered`
Normal Standard
* file names encrypted
* file names can't be as long (~156 characters)
* can use sub paths and copy single files * can use sub paths and copy single files
* directory structure visibile * directory structure visibile
* identical files names will have identical uploaded names * identical files names will have identical uploaded names
* can use shortcuts to shorten the directory recursion * can use shortcuts to shorten the directory recursion
You can swap between flattened levels without re-uploading your files. Cloud storage systems have various limits on file name length and
total path length which you are more likely to hit using "Standard"
file name encryption. If you keep your file names to below 156
characters in length then you should be OK on all providers.
There may be an even more secure file name encryption mode in the
future which will address the long file name problem.
## File formats ## ## File formats ##
@ -245,25 +262,23 @@ files.
### Name encryption ### ### Name encryption ###
File names are encrypted by crypt. These are either encrypted segment File names are encrypted segment by segment - the path is broken up
by segment - the path is broken up into `/` separated strings and into `/` separated strings and these are encrypted individually.
these are encrypted individually, or if working in flattened mode the
whole path is encrypted `/`s and all.
First file names are padded using using PKCS#7 to a multiple of 16 File segments are padded using using PKCS#7 to a multiple of 16 bytes
bytes before encryption. before encryption.
They are then encrypted with EME using AES with 256 bit key. EME They are then encrypted with EME using AES with 256 bit key. EME
(ECB-Mix-ECB) is a wide-block encryption mode presented in the 2003 (ECB-Mix-ECB) is a wide-block encryption mode presented in the 2003
paper "A Parallelizable Enciphering Mode" by Halevi and Rogaway. paper "A Parallelizable Enciphering Mode" by Halevi and Rogaway.
This makes for determinstic encryption which is what we want - the This makes for determinstic encryption which is what we want - the
same filename must encrypt to the same thing. same filename must encrypt to the same thing otherwise we can't find
it on the cloud storage system.
This means that This means that
* filenames with the same name will encrypt the same * filenames with the same name will encrypt the same
* (though we can use directory flattening to avoid this if required)
* filenames which start the same won't have a common prefix * filenames which start the same won't have a common prefix
This uses a 32 byte key (256 bits) and a 16 byte (128 bits) IV both of This uses a 32 byte key (256 bits) and a 16 byte (128 bits) IV both of
@ -281,8 +296,11 @@ used on case insensitive remotes (eg Windows, Amazon Drive).
### Key derivation ### ### Key derivation ###
Rclone uses `scrypt` with parameters `N=16384, r=8, p=1` with a fixed Rclone uses `scrypt` with parameters `N=16384, r=8, p=1` with a an
salt to derive the 32+32+16 = 80 bytes of key material required. optional user supplied salt (password2) to derive the 32+32+16 = 80
bytes of key material required. If the user doesn't supply a salt
then rclone uses an internal one.
`scrypt` makes it impractical to mount a dictionary attack on rclone `scrypt` makes it impractical to mount a dictionary attack on rclone
encrypted data. encrypted data. For full protection agains this you should always use
a salt.