Add -forcedecode

Force decode of encrypted files even if the integrity check fails, instead of
failing with an IO error. Warning messages are still printed to syslog if corrupted
files are encountered.
It can be useful to recover files from disks with bad sectors or other corrupted
media.

Closes https://github.com/rfjakob/gocryptfs/pull/102 .
This commit is contained in:
danim7 2017-04-08 02:09:28 +02:00 committed by Jakob Unterwurzacher
parent 9777e4bf7e
commit f1945c4daa
16 changed files with 94 additions and 31 deletions

View File

@ -61,6 +61,18 @@ to mount the gocryptfs filesytem without user interaction.
Stay in the foreground instead of forking away. Implies "-nosyslog".
For compatability, "-f" is also accepted, but "-fg" is preferred.
#### -forcedecode
Force decode of encrypted files even if the integrity check fails, instead of
failing with an IO error. Warning messages are still printed to syslog if corrupted
files are encountered.
It can be useful to recover files from disks with bad sectors or other corrupted
media. It shall not be used if the origin of corruption is unknown, specially
if you want to run executable files. For corrupted media, note that you probably want
to use dd_rescue(1) instead.
This option has no effect in reverse mode. It requires gocryptfs to be compiled with openssl
support and implies -openssl true. Because of this, it is not compatible with -aessiv,
that uses built-in Go crpyto.
#### -fsname string
Override the filesystem name (first column in df -T). Can also be
passed as "-o fsname=" and is equivalent to libfuse's option of the

View File

@ -10,6 +10,7 @@ import (
"github.com/rfjakob/gocryptfs/internal/configfile"
"github.com/rfjakob/gocryptfs/internal/prefer_openssl"
"github.com/rfjakob/gocryptfs/internal/stupidgcm"
"github.com/rfjakob/gocryptfs/internal/tlog"
)
@ -18,7 +19,7 @@ type argContainer struct {
debug, init, zerokey, fusedebug, openssl, passwd, fg, version,
plaintextnames, quiet, nosyslog, wpanic,
longnames, allow_other, ro, reverse, aessiv, nonempty, raw64,
noprealloc, speed, hkdf, serialize_reads bool
noprealloc, speed, hkdf, serialize_reads, forcedecode bool
masterkey, mountpoint, cipherdir, cpuprofile, extpass,
memprofile, ko, passfile, ctlsock, fsname string
// Configuration file name override
@ -113,6 +114,8 @@ func parseCliOpts() (args argContainer) {
flagSet.BoolVar(&args.speed, "speed", false, "Run crypto speed test")
flagSet.BoolVar(&args.hkdf, "hkdf", true, "Use HKDF as an additional key derivation step")
flagSet.BoolVar(&args.serialize_reads, "serialize_reads", false, "Try to serialize read operations")
flagSet.BoolVar(&args.forcedecode, "forcedecode", false, "Force decode of files even if integrity check fails."+
" Requires gocryptfs to be compiled with openssl support and implies -openssl true")
flagSet.StringVar(&args.masterkey, "masterkey", "", "Mount with explicit master key")
flagSet.StringVar(&args.cpuprofile, "cpuprofile", "", "Write cpu profile to specified file")
flagSet.StringVar(&args.memprofile, "memprofile", "", "Write memory profile to specified file")
@ -154,6 +157,26 @@ func parseCliOpts() (args argContainer) {
os.Exit(ErrExitUsage)
}
}
// "-forcedecode" only works with openssl. Check compilation and command line parameters
if args.forcedecode == true {
if stupidgcm.BuiltWithoutOpenssl == true {
tlog.Fatal.Printf("The -forcedecode flag requires openssl support, but gocryptfs was compiled without it!")
os.Exit(ErrExitUsage)
}
if args.aessiv == true {
tlog.Fatal.Printf("The -forcedecode and -aessiv flags are incompatible because they use different crypto libs (openssl vs native Go)")
os.Exit(ErrExitUsage)
}
if args.reverse == true {
tlog.Fatal.Printf("The reverse mode and the -forcedecode option are not compatible")
os.Exit(ErrExitUsage)
}
v, e := strconv.ParseBool(opensslAuto)
if e == nil && v == false {
tlog.Warn.Printf("-openssl set to true, as it is required by -forcedecode flag")
}
args.openssl = true
}
// '-passfile FILE' is a shortcut for -extpass='/bin/cat -- FILE'
if args.passfile != "" {
args.extpass = "/bin/cat -- " + args.passfile

View File

@ -225,7 +225,7 @@ func getKeyEncrypter(scryptHash []byte, useHKDF bool) *contentenc.ContentEnc {
if useHKDF {
IVLen = contentenc.DefaultIVBits
}
cc := cryptocore.New(scryptHash, cryptocore.BackendGoGCM, IVLen, useHKDF)
ce := contentenc.New(cc, 4096)
cc := cryptocore.New(scryptHash, cryptocore.BackendGoGCM, IVLen, useHKDF, false)
ce := contentenc.New(cc, 4096, false)
return ce
}

View File

@ -9,6 +9,7 @@ import (
"log"
"github.com/rfjakob/gocryptfs/internal/cryptocore"
"github.com/rfjakob/gocryptfs/internal/stupidgcm"
"github.com/rfjakob/gocryptfs/internal/tlog"
)
@ -46,10 +47,12 @@ type ContentEnc struct {
allZeroBlock []byte
// All-zero block of size IVBitLen/8, for fast compares
allZeroNonce []byte
// Force decode even if integrity check fails (openSSL only)
forceDecode bool
}
// New returns an initialized ContentEnc instance.
func New(cc *cryptocore.CryptoCore, plainBS uint64) *ContentEnc {
func New(cc *cryptocore.CryptoCore, plainBS uint64, forceDecode bool) *ContentEnc {
cipherBS := plainBS + uint64(cc.IVLen) + cryptocore.AuthTagLen
return &ContentEnc{
@ -58,6 +61,7 @@ func New(cc *cryptocore.CryptoCore, plainBS uint64) *ContentEnc {
cipherBS: cipherBS,
allZeroBlock: make([]byte, cipherBS),
allZeroNonce: make([]byte, cc.IVLen),
forceDecode: forceDecode,
}
}
@ -82,7 +86,9 @@ func (be *ContentEnc) DecryptBlocks(ciphertext []byte, firstBlockNo uint64, file
var pBlock []byte
pBlock, err = be.DecryptBlock(cBlock, firstBlockNo, fileID)
if err != nil {
break
if be.forceDecode == false || (be.forceDecode == true && stupidgcm.AuthError != err) {
break
}
}
pBuf.Write(pBlock)
firstBlockNo++
@ -133,7 +139,11 @@ func (be *ContentEnc) DecryptBlock(ciphertext []byte, blockNo uint64, fileID []b
if err != nil {
tlog.Warn.Printf("DecryptBlock: %s, len=%d", err.Error(), len(ciphertextOrig))
tlog.Debug.Println(hex.Dump(ciphertextOrig))
return nil, err
if be.forceDecode == true {
return plaintext, err
} else {
return nil, err
}
}
return plaintext, nil

View File

@ -23,8 +23,8 @@ func TestSplitRange(t *testing.T) {
testRange{6654, 8945})
key := make([]byte, cryptocore.KeyLen)
cc := cryptocore.New(key, cryptocore.BackendOpenSSL, DefaultIVBits, true)
f := New(cc, DefaultBS)
cc := cryptocore.New(key, cryptocore.BackendOpenSSL, DefaultIVBits, true, false)
f := New(cc, DefaultBS, false)
for _, r := range ranges {
parts := f.ExplodePlainRange(r.offset, r.length)
@ -51,8 +51,8 @@ func TestCiphertextRange(t *testing.T) {
testRange{6654, 8945})
key := make([]byte, cryptocore.KeyLen)
cc := cryptocore.New(key, cryptocore.BackendOpenSSL, DefaultIVBits, true)
f := New(cc, DefaultBS)
cc := cryptocore.New(key, cryptocore.BackendOpenSSL, DefaultIVBits, true, false)
f := New(cc, DefaultBS, false)
for _, r := range ranges {
@ -74,8 +74,8 @@ func TestCiphertextRange(t *testing.T) {
func TestBlockNo(t *testing.T) {
key := make([]byte, cryptocore.KeyLen)
cc := cryptocore.New(key, cryptocore.BackendOpenSSL, DefaultIVBits, true)
f := New(cc, DefaultBS)
cc := cryptocore.New(key, cryptocore.BackendOpenSSL, DefaultIVBits, true, false)
f := New(cc, DefaultBS, false)
b := f.CipherOffToBlockNo(788)
if b != 0 {

View File

@ -51,7 +51,7 @@ type CryptoCore struct {
// Even though the "GCMIV128" feature flag is now mandatory, we must still
// support 96-bit IVs here because they were used for encrypting the master
// key in gocryptfs.conf up to gocryptfs v1.2. v1.3 switched to 128 bits.
func New(key []byte, aeadType AEADTypeEnum, IVBitLen int, useHKDF bool) *CryptoCore {
func New(key []byte, aeadType AEADTypeEnum, IVBitLen int, useHKDF bool, forceDecode bool) *CryptoCore {
if len(key) != KeyLen {
log.Panic(fmt.Sprintf("Unsupported key length %d", len(key)))
}
@ -86,7 +86,7 @@ func New(key []byte, aeadType AEADTypeEnum, IVBitLen int, useHKDF bool) *CryptoC
if IVLen != 16 {
log.Panic("stupidgcm only supports 128-bit IVs")
}
aeadCipher = stupidgcm.New(gcmKey)
aeadCipher = stupidgcm.New(gcmKey, forceDecode)
case BackendGoGCM:
goGcmBlockCipher, err := aes.NewCipher(gcmKey)
if err != nil {

View File

@ -8,15 +8,15 @@ import (
func TestCryptoCoreNew(t *testing.T) {
key := make([]byte, 32)
for _, useHKDF := range []bool{true, false} {
c := New(key, BackendOpenSSL, 128, useHKDF)
c := New(key, BackendOpenSSL, 128, useHKDF, false)
if c.IVLen != 16 {
t.Fail()
}
c = New(key, BackendGoGCM, 96, useHKDF)
c = New(key, BackendGoGCM, 96, useHKDF, false)
if c.IVLen != 12 {
t.Fail()
}
c = New(key, BackendGoGCM, 128, useHKDF)
c = New(key, BackendGoGCM, 128, useHKDF, false)
if c.IVLen != 16 {
t.Fail()
}
@ -32,5 +32,5 @@ func TestNewPanic(t *testing.T) {
}()
key := make([]byte, 16)
New(key, BackendOpenSSL, 128, true)
New(key, BackendOpenSSL, 128, true, false)
}

View File

@ -31,4 +31,6 @@ type Args struct {
HKDF bool
// Try to serialize read operations, "-serialize_reads"
SerializeReads bool
// Force decode even if integrity check fails (openSSL only)
ForceDecode bool
}

View File

@ -202,7 +202,9 @@ func (f *file) doRead(off uint64, length uint64) ([]byte, fuse.Status) {
if err != nil {
curruptBlockNo := firstBlockNo + f.contentEnc.PlainOffToBlockNo(uint64(len(plaintext)))
tlog.Warn.Printf("ino%d: doRead: corrupt block #%d: %v", f.devIno.ino, curruptBlockNo, err)
return nil, fuse.EIO
if (f.fs.args.ForceDecode == false) {
return nil, fuse.EIO
}
}
// Crop down to the relevant part

View File

@ -40,8 +40,8 @@ var _ pathfs.FileSystem = &FS{} // Verify that interface is implemented.
// NewFS returns a new encrypted FUSE overlay filesystem.
func NewFS(args Args) *FS {
cryptoCore := cryptocore.New(args.Masterkey, args.CryptoBackend, contentenc.DefaultIVBits, args.HKDF)
contentEnc := contentenc.New(cryptoCore, contentenc.DefaultBS)
cryptoCore := cryptocore.New(args.Masterkey, args.CryptoBackend, contentenc.DefaultIVBits, args.HKDF, args.ForceDecode)
contentEnc := contentenc.New(cryptoCore, contentenc.DefaultBS, args.ForceDecode)
nameTransform := nametransform.New(cryptoCore.EMECipher, args.LongNames, args.Raw64)
if args.SerializeReads {

View File

@ -44,8 +44,8 @@ func NewFS(args fusefrontend.Args) *ReverseFS {
log.Panic("reverse mode must use AES-SIV, everything else is insecure")
}
initLongnameCache()
cryptoCore := cryptocore.New(args.Masterkey, args.CryptoBackend, contentenc.DefaultIVBits, args.HKDF)
contentEnc := contentenc.New(cryptoCore, contentenc.DefaultBS)
cryptoCore := cryptocore.New(args.Masterkey, args.CryptoBackend, contentenc.DefaultIVBits, args.HKDF, false)
contentEnc := contentenc.New(cryptoCore, contentenc.DefaultBS, false)
nameTransform := nametransform.New(cryptoCore.EMECipher, args.LongNames, args.Raw64)
return &ReverseFS{

View File

@ -72,7 +72,7 @@ func bStupidGCM(b *testing.B) {
in := make([]byte, blockSize)
b.SetBytes(int64(len(in)))
sGCM := stupidgcm.New(key)
sGCM := stupidgcm.New(key, false)
b.ResetTimer()
for i := 0; i < b.N; i++ {

View File

@ -26,17 +26,21 @@ const (
// stupidGCM implements the cipher.AEAD interface
type stupidGCM struct {
key []byte
key []byte
forceDecode bool
}
//authentication error
var AuthError error = fmt.Errorf("stupidgcm: message authentication failed")
var _ cipher.AEAD = &stupidGCM{}
// New returns a new cipher.AEAD implementation..
func New(key []byte) cipher.AEAD {
func New(key []byte, forceDecode bool) cipher.AEAD {
if len(key) != keyLen {
log.Panicf("Only %d-byte keys are supported", keyLen)
}
return stupidGCM{key: key}
return stupidGCM{key: key, forceDecode: forceDecode}
}
func (g stupidGCM) NonceSize() int {
@ -186,7 +190,13 @@ func (g stupidGCM) Open(dst, iv, in, authData []byte) ([]byte, error) {
C.EVP_CIPHER_CTX_free(ctx)
if res != 1 {
return nil, fmt.Errorf("stupidgcm: message authentication failed")
// The error code must always be checked by the calling function, because the decrypted buffer
// may contain corrupted data that we are returning in case the user forced reads
if g.forceDecode == true {
return append(dst, buf...), AuthError
} else {
return nil, AuthError
}
}
return append(dst, buf...), nil

View File

@ -27,7 +27,7 @@ func randBytes(n int) []byte {
// GCM implemenatation and verifies that the results are identical.
func TestEncryptDecrypt(t *testing.T) {
key := randBytes(32)
sGCM := New(key)
sGCM := New(key, false)
authData := randBytes(24)
iv := randBytes(16)
dst := make([]byte, 71) // 71 = random length
@ -77,7 +77,7 @@ func TestEncryptDecrypt(t *testing.T) {
// error
func TestCorruption(t *testing.T) {
key := randBytes(32)
sGCM := New(key)
sGCM := New(key, false)
authData := randBytes(24)
iv := randBytes(16)

View File

@ -14,12 +14,15 @@ const (
BuiltWithoutOpenssl = true
)
//authentication error - needed to compile as same varaible is exported when openssl is enable via stupidgcm.go
var AuthError error = fmt.Errorf("stupidgcm: message authentication failed with openssl disabled!")
func errExit() {
fmt.Fprintln(os.Stderr, "gocryptfs has been compiled without openssl support but you are still trying to use openssl")
os.Exit(2)
}
func New(_ []byte) stupidGCM {
func New(_ []byte, _ bool) stupidGCM {
errExit()
// Never reached
return stupidGCM{}

View File

@ -192,6 +192,7 @@ func initFuseFrontend(key []byte, args *argContainer, confFile *configfile.ConfF
NoPrealloc: args.noprealloc,
HKDF: args.hkdf,
SerializeReads: args.serialize_reads,
ForceDecode: args.forcedecode,
}
// confFile is nil when "-zerokey" or "-masterkey" was used
if confFile != nil {