From f1945c4daae65074cfca8f0ab5b97ac5a50c24a0 Mon Sep 17 00:00:00 2001 From: danim7 Date: Sat, 8 Apr 2017 02:09:28 +0200 Subject: 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 . --- Documentation/MANPAGE.md | 12 ++++++++++++ cli_args.go | 25 ++++++++++++++++++++++++- internal/configfile/config_file.go | 4 ++-- internal/contentenc/content.go | 16 +++++++++++++--- internal/contentenc/content_test.go | 12 ++++++------ internal/cryptocore/cryptocore.go | 4 ++-- internal/cryptocore/cryptocore_test.go | 8 ++++---- internal/fusefrontend/args.go | 2 ++ internal/fusefrontend/file.go | 4 +++- internal/fusefrontend/fs.go | 4 ++-- internal/fusefrontend_reverse/rfs.go | 4 ++-- internal/speed/speed.go | 2 +- internal/stupidgcm/stupidgcm.go | 18 ++++++++++++++---- internal/stupidgcm/stupidgcm_test.go | 4 ++-- internal/stupidgcm/without_openssl.go | 5 ++++- mount.go | 1 + 16 files changed, 94 insertions(+), 31 deletions(-) diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index 6fb55ca..656a400 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -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 diff --git a/cli_args.go b/cli_args.go index 9414e67..f0bfb48 100644 --- a/cli_args.go +++ b/cli_args.go @@ -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 diff --git a/internal/configfile/config_file.go b/internal/configfile/config_file.go index bf56f8b..5605e1d 100644 --- a/internal/configfile/config_file.go +++ b/internal/configfile/config_file.go @@ -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 } diff --git a/internal/contentenc/content.go b/internal/contentenc/content.go index a2a263c..9998c06 100644 --- a/internal/contentenc/content.go +++ b/internal/contentenc/content.go @@ -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 diff --git a/internal/contentenc/content_test.go b/internal/contentenc/content_test.go index 8ce496d..e4d4a3e 100644 --- a/internal/contentenc/content_test.go +++ b/internal/contentenc/content_test.go @@ -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 { diff --git a/internal/cryptocore/cryptocore.go b/internal/cryptocore/cryptocore.go index 5244104..2c352c2 100644 --- a/internal/cryptocore/cryptocore.go +++ b/internal/cryptocore/cryptocore.go @@ -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 { diff --git a/internal/cryptocore/cryptocore_test.go b/internal/cryptocore/cryptocore_test.go index 25f6572..4c34652 100644 --- a/internal/cryptocore/cryptocore_test.go +++ b/internal/cryptocore/cryptocore_test.go @@ -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) } diff --git a/internal/fusefrontend/args.go b/internal/fusefrontend/args.go index ddfb9dc..5781db8 100644 --- a/internal/fusefrontend/args.go +++ b/internal/fusefrontend/args.go @@ -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 } diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go index 84ce058..4d75d64 100644 --- a/internal/fusefrontend/file.go +++ b/internal/fusefrontend/file.go @@ -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 diff --git a/internal/fusefrontend/fs.go b/internal/fusefrontend/fs.go index 28c43b6..4aa4ffd 100644 --- a/internal/fusefrontend/fs.go +++ b/internal/fusefrontend/fs.go @@ -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 { diff --git a/internal/fusefrontend_reverse/rfs.go b/internal/fusefrontend_reverse/rfs.go index a94f448..3c52244 100644 --- a/internal/fusefrontend_reverse/rfs.go +++ b/internal/fusefrontend_reverse/rfs.go @@ -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{ diff --git a/internal/speed/speed.go b/internal/speed/speed.go index f9bf93c..8732576 100644 --- a/internal/speed/speed.go +++ b/internal/speed/speed.go @@ -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++ { diff --git a/internal/stupidgcm/stupidgcm.go b/internal/stupidgcm/stupidgcm.go index a1a5a14..133ee1a 100644 --- a/internal/stupidgcm/stupidgcm.go +++ b/internal/stupidgcm/stupidgcm.go @@ -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 diff --git a/internal/stupidgcm/stupidgcm_test.go b/internal/stupidgcm/stupidgcm_test.go index 3081085..eb322f2 100644 --- a/internal/stupidgcm/stupidgcm_test.go +++ b/internal/stupidgcm/stupidgcm_test.go @@ -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) diff --git a/internal/stupidgcm/without_openssl.go b/internal/stupidgcm/without_openssl.go index 18c5ddc..52d8fa0 100644 --- a/internal/stupidgcm/without_openssl.go +++ b/internal/stupidgcm/without_openssl.go @@ -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{} diff --git a/mount.go b/mount.go index c4bdc4d..6da6492 100644 --- a/mount.go +++ b/mount.go @@ -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 { -- cgit v1.2.3