mirror of
https://github.com/rclone/rclone
synced 2024-12-21 11:45:56 +01:00
crypt: add an "obfuscate" option for filename encryption.
This is a simple "rotate" of the filename, with each file having a rot distance based on the filename. We store the distance at the beginning of the filename. So a file called "go" would become "37.KS". This is not a strong encryption of filenames, but it should stop automated scanning tools from picking up on filename patterns. As such it's an intermediate between "off" and "standard". The advantage is that it allows for longer path segment names. We use the nameKey as an additional input to calculate the obfuscation distance. This should mean that two different passwords will result in two different keys The obfuscation rotation works by splitting the ranges up and handle cases 0-9 A-Za-z 0xA0-0xFF and anything greater in blocks of 256
This commit is contained in:
parent
37e1b20ec1
commit
6e003934fc
194
crypt/cipher.go
194
crypt/cipher.go
@ -8,6 +8,7 @@ import (
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
@ -49,6 +50,7 @@ var (
|
||||
ErrorNotAnEncryptedFile = errors.New("not an encrypted file - no \"" + encryptedSuffix + "\" suffix")
|
||||
ErrorBadSeek = errors.New("Seek beyond end of file")
|
||||
defaultSalt = []byte{0xA8, 0x0D, 0xF4, 0x3A, 0x8F, 0xBD, 0x03, 0x08, 0xA7, 0xCA, 0xB8, 0x3E, 0x58, 0x1F, 0x86, 0xB1}
|
||||
obfuscQuoteRune = '!'
|
||||
)
|
||||
|
||||
// Global variables
|
||||
@ -95,6 +97,7 @@ type NameEncryptionMode int
|
||||
const (
|
||||
NameEncryptionOff NameEncryptionMode = iota
|
||||
NameEncryptionStandard
|
||||
NameEncryptionObfuscated
|
||||
)
|
||||
|
||||
// NewNameEncryptionMode turns a string into a NameEncryptionMode
|
||||
@ -105,6 +108,8 @@ func NewNameEncryptionMode(s string) (mode NameEncryptionMode, err error) {
|
||||
mode = NameEncryptionOff
|
||||
case "standard":
|
||||
mode = NameEncryptionStandard
|
||||
case "obfuscate":
|
||||
mode = NameEncryptionObfuscated
|
||||
default:
|
||||
err = errors.Errorf("Unknown file name encryption mode %q", s)
|
||||
}
|
||||
@ -118,6 +123,8 @@ func (mode NameEncryptionMode) String() (out string) {
|
||||
out = "off"
|
||||
case NameEncryptionStandard:
|
||||
out = "standard"
|
||||
case NameEncryptionObfuscated:
|
||||
out = "obfuscate"
|
||||
default:
|
||||
out = fmt.Sprintf("Unknown mode #%d", mode)
|
||||
}
|
||||
@ -284,11 +291,189 @@ func (c *cipher) decryptSegment(ciphertext string) (string, error) {
|
||||
return string(plaintext), err
|
||||
}
|
||||
|
||||
// Simple obfuscation routines
|
||||
func (c *cipher) obfuscateSegment(plaintext string) string {
|
||||
if plaintext == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// If the string isn't valid UTF8 then don't rotate; just
|
||||
// prepend a 0.
|
||||
if !utf8.ValidString(plaintext) {
|
||||
return "0." + plaintext
|
||||
}
|
||||
|
||||
// Calculate a simple rotation based on the filename and
|
||||
// the nameKey
|
||||
var dir int
|
||||
for _, runeValue := range plaintext {
|
||||
dir += int(runeValue)
|
||||
}
|
||||
dir = dir % 256
|
||||
|
||||
// We'll use this number to store in the result filename...
|
||||
var result bytes.Buffer
|
||||
_, _ = result.WriteString(strconv.Itoa(dir) + ".")
|
||||
|
||||
// but we'll augment it with the nameKey for real calculation
|
||||
for i := 0; i < len(c.nameKey); i++ {
|
||||
dir += int(c.nameKey[i])
|
||||
}
|
||||
|
||||
// Now for each character, depending on the range it is in
|
||||
// we will actually rotate a different amount
|
||||
for _, runeValue := range plaintext {
|
||||
switch {
|
||||
case runeValue == obfuscQuoteRune:
|
||||
// Quote the Quote character
|
||||
_, _ = result.WriteRune(obfuscQuoteRune)
|
||||
_, _ = result.WriteRune(obfuscQuoteRune)
|
||||
|
||||
case runeValue >= '0' && runeValue <= '9':
|
||||
// Number
|
||||
thisdir := (dir % 9) + 1
|
||||
newRune := '0' + (int(runeValue)-'0'+thisdir)%10
|
||||
_, _ = result.WriteRune(rune(newRune))
|
||||
|
||||
case (runeValue >= 'A' && runeValue <= 'Z') ||
|
||||
(runeValue >= 'a' && runeValue <= 'z'):
|
||||
// ASCII letter. Try to avoid trivial A->a mappings
|
||||
thisdir := dir%25 + 1
|
||||
// Calculate the offset of this character in A-Za-z
|
||||
pos := int(runeValue - 'A')
|
||||
if pos >= 26 {
|
||||
pos -= 6 // It's lower case
|
||||
}
|
||||
// Rotate the character to the new location
|
||||
pos = (pos + thisdir) % 52
|
||||
if pos >= 26 {
|
||||
pos += 6 // and handle lower case offset again
|
||||
}
|
||||
_, _ = result.WriteRune(rune('A' + pos))
|
||||
|
||||
case runeValue >= 0xA0 && runeValue <= 0xFF:
|
||||
// Latin 1 supplement
|
||||
thisdir := (dir % 95) + 1
|
||||
newRune := 0xA0 + (int(runeValue)-0xA0+thisdir)%96
|
||||
_, _ = result.WriteRune(rune(newRune))
|
||||
|
||||
case runeValue >= 0x100:
|
||||
// Some random Unicode range; we have no good rules here
|
||||
thisdir := (dir % 127) + 1
|
||||
base := int(runeValue - runeValue%256)
|
||||
newRune := rune(base + (int(runeValue)-base+thisdir)%256)
|
||||
// If the new character isn't a valid UTF8 char
|
||||
// then don't rotate it. Quote it instead
|
||||
if !utf8.ValidRune(newRune) {
|
||||
_, _ = result.WriteRune(obfuscQuoteRune)
|
||||
_, _ = result.WriteRune(runeValue)
|
||||
} else {
|
||||
_, _ = result.WriteRune(newRune)
|
||||
}
|
||||
|
||||
default:
|
||||
// Leave character untouched
|
||||
_, _ = result.WriteRune(runeValue)
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func (c *cipher) deobfuscateSegment(ciphertext string) (string, error) {
|
||||
if ciphertext == "" {
|
||||
return "", nil
|
||||
}
|
||||
pos := strings.Index(ciphertext, ".")
|
||||
if pos == -1 {
|
||||
return "", ErrorNotAnEncryptedFile
|
||||
} // No .
|
||||
num := ciphertext[:pos]
|
||||
if num == "0" {
|
||||
// No rotation; probably original was not valid unicode
|
||||
return ciphertext[pos+1:], nil
|
||||
}
|
||||
dir, err := strconv.Atoi(num)
|
||||
if err != nil {
|
||||
return "", ErrorNotAnEncryptedFile // Not a number
|
||||
}
|
||||
|
||||
// add the nameKey to get the real rotate distance
|
||||
for i := 0; i < len(c.nameKey); i++ {
|
||||
dir += int(c.nameKey[i])
|
||||
}
|
||||
|
||||
var result bytes.Buffer
|
||||
|
||||
inQuote := false
|
||||
for _, runeValue := range ciphertext[pos+1:] {
|
||||
switch {
|
||||
case inQuote:
|
||||
_, _ = result.WriteRune(runeValue)
|
||||
inQuote = false
|
||||
|
||||
case runeValue == obfuscQuoteRune:
|
||||
inQuote = true
|
||||
|
||||
case runeValue >= '0' && runeValue <= '9':
|
||||
// Number
|
||||
thisdir := (dir % 9) + 1
|
||||
newRune := '0' + int(runeValue) - '0' - thisdir
|
||||
if newRune < '0' {
|
||||
newRune += 10
|
||||
}
|
||||
_, _ = result.WriteRune(rune(newRune))
|
||||
|
||||
case (runeValue >= 'A' && runeValue <= 'Z') ||
|
||||
(runeValue >= 'a' && runeValue <= 'z'):
|
||||
thisdir := dir%25 + 1
|
||||
pos := int(runeValue - 'A')
|
||||
if pos >= 26 {
|
||||
pos -= 6
|
||||
}
|
||||
pos = pos - thisdir
|
||||
if pos < 0 {
|
||||
pos += 52
|
||||
}
|
||||
if pos >= 26 {
|
||||
pos += 6
|
||||
}
|
||||
_, _ = result.WriteRune(rune('A' + pos))
|
||||
|
||||
case runeValue >= 0xA0 && runeValue <= 0xFF:
|
||||
thisdir := (dir % 95) + 1
|
||||
newRune := 0xA0 + int(runeValue) - 0xA0 - thisdir
|
||||
if newRune < 0xA0 {
|
||||
newRune += 96
|
||||
}
|
||||
_, _ = result.WriteRune(rune(newRune))
|
||||
|
||||
case runeValue >= 0x100:
|
||||
thisdir := (dir % 127) + 1
|
||||
base := int(runeValue - runeValue%256)
|
||||
newRune := rune(base + (int(runeValue) - base - thisdir))
|
||||
if int(newRune) < base {
|
||||
newRune += 256
|
||||
}
|
||||
_, _ = result.WriteRune(rune(newRune))
|
||||
|
||||
default:
|
||||
_, _ = result.WriteRune(runeValue)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
// encryptFileName encrypts a file path
|
||||
func (c *cipher) encryptFileName(in string) string {
|
||||
segments := strings.Split(in, "/")
|
||||
for i := range segments {
|
||||
segments[i] = c.encryptSegment(segments[i])
|
||||
if c.mode == NameEncryptionStandard {
|
||||
segments[i] = c.encryptSegment(segments[i])
|
||||
} else {
|
||||
segments[i] = c.obfuscateSegment(segments[i])
|
||||
}
|
||||
}
|
||||
return strings.Join(segments, "/")
|
||||
}
|
||||
@ -314,7 +499,12 @@ func (c *cipher) decryptFileName(in string) (string, error) {
|
||||
segments := strings.Split(in, "/")
|
||||
for i := range segments {
|
||||
var err error
|
||||
segments[i], err = c.decryptSegment(segments[i])
|
||||
if c.mode == NameEncryptionStandard {
|
||||
segments[i], err = c.decryptSegment(segments[i])
|
||||
} else {
|
||||
segments[i], err = c.deobfuscateSegment(segments[i])
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ func TestNewNameEncryptionMode(t *testing.T) {
|
||||
}{
|
||||
{"off", NameEncryptionOff, ""},
|
||||
{"standard", NameEncryptionStandard, ""},
|
||||
{"obfuscate", NameEncryptionObfuscated, ""},
|
||||
{"potato", NameEncryptionMode(0), "Unknown file name encryption mode \"potato\""},
|
||||
} {
|
||||
actual, actualErr := NewNameEncryptionMode(test.in)
|
||||
@ -38,7 +39,8 @@ func TestNewNameEncryptionMode(t *testing.T) {
|
||||
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")
|
||||
assert.Equal(t, NameEncryptionObfuscated.String(), "obfuscate")
|
||||
assert.Equal(t, NameEncryptionMode(3).String(), "Unknown mode #3")
|
||||
}
|
||||
|
||||
func TestValidString(t *testing.T) {
|
||||
@ -219,6 +221,11 @@ func TestEncryptFileName(t *testing.T) {
|
||||
// Now off mode
|
||||
c, _ = newCipher(NameEncryptionOff, "", "")
|
||||
assert.Equal(t, "1/12/123.bin", c.EncryptFileName("1/12/123"))
|
||||
// Obfuscation mode
|
||||
c, _ = newCipher(NameEncryptionObfuscated, "", "")
|
||||
assert.Equal(t, "49.6/99.23/150.890/53.!!lipps", c.EncryptFileName("1/12/123/!hello"))
|
||||
assert.Equal(t, "161.\u00e4", c.EncryptFileName("\u00a1"))
|
||||
assert.Equal(t, "160.\u03c2", c.EncryptFileName("\u03a0"))
|
||||
}
|
||||
|
||||
func TestDecryptFileName(t *testing.T) {
|
||||
@ -236,6 +243,10 @@ func TestDecryptFileName(t *testing.T) {
|
||||
{NameEncryptionOff, "1/12/123.bin", "1/12/123", nil},
|
||||
{NameEncryptionOff, "1/12/123.bix", "", ErrorNotAnEncryptedFile},
|
||||
{NameEncryptionOff, ".bin", "", ErrorNotAnEncryptedFile},
|
||||
{NameEncryptionObfuscated, "0.hello", "hello", nil},
|
||||
{NameEncryptionObfuscated, "hello", "", ErrorNotAnEncryptedFile},
|
||||
{NameEncryptionObfuscated, "161.\u00e4", "\u00a1", nil},
|
||||
{NameEncryptionObfuscated, "160.\u03c2", "\u03a0", nil},
|
||||
} {
|
||||
c, _ := newCipher(test.mode, "", "")
|
||||
actual, actualErr := c.DecryptFileName(test.in)
|
||||
@ -245,6 +256,23 @@ func TestDecryptFileName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncDecMatches(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
mode NameEncryptionMode
|
||||
in string
|
||||
}{
|
||||
{NameEncryptionStandard, "1/2/3/4"},
|
||||
{NameEncryptionOff, "1/2/3/4"},
|
||||
{NameEncryptionObfuscated, "1/2/3/4/!hello\u03a0"},
|
||||
} {
|
||||
c, _ := newCipher(test.mode, "", "")
|
||||
out, err := c.DecryptFileName(c.EncryptFileName(test.in))
|
||||
what := fmt.Sprintf("Testing %q (mode=%v)", test.in, test.mode)
|
||||
assert.Equal(t, out, test.in, what)
|
||||
assert.Equal(t, err, nil, what)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDirName(t *testing.T) {
|
||||
// First standard mode
|
||||
c, _ := newCipher(NameEncryptionStandard, "", "")
|
||||
|
@ -37,6 +37,9 @@ func init() {
|
||||
}, {
|
||||
Value: "standard",
|
||||
Help: "Encrypt the filenames see the docs for the details.",
|
||||
}, {
|
||||
Value: "obfuscate",
|
||||
Help: "Very simple filename obfuscation.",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
|
@ -71,6 +71,8 @@ Choose a number from below, or type in your own value
|
||||
\ "off"
|
||||
2 / Encrypt the filenames see the docs for the details.
|
||||
\ "standard"
|
||||
3 / Very simple filename obfuscation.
|
||||
\ "obfuscate"
|
||||
filename_encryption> 2
|
||||
Password or pass phrase for encryption.
|
||||
y) Yes type in my own password
|
||||
@ -225,6 +227,27 @@ Standard
|
||||
* identical files names will have identical uploaded names
|
||||
* can use shortcuts to shorten the directory recursion
|
||||
|
||||
Obfuscation
|
||||
|
||||
This is a simple "rotate" of the filename, with each file having a rot
|
||||
distance based on the filename. We store the distance at the beginning
|
||||
of the filename. So a file called "hello" may become "53.jgnnq"
|
||||
|
||||
This is not a strong encryption of filenames, but it may stop automated
|
||||
scanning tools from picking up on filename patterns. As such it's an
|
||||
intermediate between "off" and "standard". The advantage is that it
|
||||
allows for longer path segment names.
|
||||
|
||||
There is a possibility with some unicode based filenames that the
|
||||
obfuscation is weak and may map lower case characters to upper case
|
||||
equivalents. You can not rely on this for strong protection.
|
||||
|
||||
* file names very lightly obfuscated
|
||||
* file names can be longer than standard encryption
|
||||
* can use sub paths and copy single files
|
||||
* directory structure visibile
|
||||
* identical files names will have identical uploaded names
|
||||
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user