diff --git a/internal/configfile/config_file.go b/internal/configfile/config_file.go index f58c51c..9152523 100644 --- a/internal/configfile/config_file.go +++ b/internal/configfile/config_file.go @@ -168,7 +168,7 @@ func (cf *ConfFile) EncryptKey(key []byte, password string, logN int) { // Lock master key using password-based key cc := cryptocore.New(scryptHash, cryptocore.BackendGoGCM, 96) ce := contentenc.New(cc, 4096) - cf.EncryptedKey = ce.EncryptBlock(key, 0, nil, contentenc.RandomNonce, nil) + cf.EncryptedKey = ce.EncryptBlock(key, 0, nil) } // WriteFile - write out config in JSON format to file "filename.tmp" diff --git a/internal/contentenc/content.go b/internal/contentenc/content.go index 86be7d5..ac2e8de 100644 --- a/internal/contentenc/content.go +++ b/internal/contentenc/content.go @@ -127,50 +127,34 @@ func (be *ContentEnc) DecryptBlock(ciphertext []byte, blockNo uint64, fileId []b return plaintext, nil } -// EncryptBlocks - Encrypt a number of blocks -// Used for reverse mode -func (be *ContentEnc) EncryptBlocks(plaintext []byte, firstBlockNo uint64, fileId []byte, nMode NonceMode) []byte { - inBuf := bytes.NewBuffer(plaintext) - var outBuf bytes.Buffer - for blockNo := firstBlockNo; inBuf.Len() > 0; blockNo++ { - inBlock := inBuf.Next(int(be.plainBS)) - outBlock := be.EncryptBlock(inBlock, blockNo, fileId, nMode, nil) - outBuf.Write(outBlock) - } - return outBuf.Bytes() +// EncryptBlock - Encrypt plaintext using a random nonce. +// blockNo and fileID are used as associated data. +// The output is nonce + ciphertext + tag. +func (be *ContentEnc) EncryptBlock(plaintext []byte, blockNo uint64, fileID []byte) []byte { + // Get a fresh random nonce + nonce := be.cryptoCore.IVGenerator.Get() + return be.doEncryptBlock(plaintext, blockNo, fileID, nonce) } -// encryptBlock - Encrypt and add IV and MAC -func (be *ContentEnc) EncryptBlock(plaintext []byte, blockNo uint64, fileID []byte, nMode NonceMode, externalNonce []byte) []byte { +// EncryptBlockNonce - Encrypt plaintext using a nonce chosen by the caller. +// blockNo and fileID are used as associated data. +// The output is nonce + ciphertext + tag. +// This function can only be used in SIV mode. +func (be *ContentEnc) EncryptBlockNonce(plaintext []byte, blockNo uint64, fileID []byte, nonce []byte) []byte { + if be.cryptoCore.AEADBackend != cryptocore.BackendAESSIV { + panic("deterministic nonces are only secure in SIV mode") + } + return be.doEncryptBlock(plaintext, blockNo, fileID, nonce) +} + +// doEncryptBlock is the backend for EncryptBlock and EncryptBlockNonce. +// blockNo and fileID are used as associated data. +// The output is nonce + ciphertext + tag. +func (be *ContentEnc) doEncryptBlock(plaintext []byte, blockNo uint64, fileID []byte, nonce []byte) []byte { // Empty block? if len(plaintext) == 0 { return plaintext } - - var nonce []byte - switch nMode { - case ExternalNonce: - if be.cryptoCore.AEADBackend != cryptocore.BackendAESSIV { - panic("MUST NOT use deterministic nonces unless in AESSIV mode!") - } - nonce = externalNonce - case ReverseDeterministicNonce: - if be.cryptoCore.AEADBackend != cryptocore.BackendAESSIV { - panic("MUST NOT use deterministic nonces unless in AESSIV mode!") - } - l := be.cryptoCore.IVLen - nonce = make([]byte, l) - copy(nonce, fileID) - // Add the block number to the last 8 byte. Plus one so the block-zero - // IV is distinct from the fileID. - counter := binary.BigEndian.Uint64(nonce[l-8 : l]) - binary.BigEndian.PutUint64(nonce[l-8:l], counter+blockNo+1) - case RandomNonce: - // Get a fresh random nonce - nonce = be.cryptoCore.IVGenerator.Get() - default: - panic("invalid nonce mode") - } if len(nonce) != be.cryptoCore.IVLen { panic("wrong nonce length") } diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go index aca7d90..a04b6af 100644 --- a/internal/fusefrontend/file.go +++ b/internal/fusefrontend/file.go @@ -256,7 +256,7 @@ func (f *file) doWrite(data []byte, off int64) (uint32, fuse.Status) { // Encrypt blockOffset := b.BlockCipherOff() - blockData = f.contentEnc.EncryptBlock(blockData, b.BlockNo, f.header.Id, contentenc.RandomNonce, nil) + blockData = f.contentEnc.EncryptBlock(blockData, b.BlockNo, f.header.Id) tlog.Debug.Printf("ino%d: Writing %d bytes to block #%d", f.ino, uint64(len(blockData))-f.contentEnc.BlockOverhead(), b.BlockNo) diff --git a/internal/fusefrontend/fs.go b/internal/fusefrontend/fs.go index 295d011..62146a2 100644 --- a/internal/fusefrontend/fs.go +++ b/internal/fusefrontend/fs.go @@ -326,7 +326,7 @@ func (fs *FS) Symlink(target string, linkName string, context *fuse.Context) (co return fuse.ToStatus(err) } // Symlinks are encrypted like file contents (GCM) and base64-encoded - cBinTarget := fs.contentEnc.EncryptBlock([]byte(target), 0, nil, contentenc.RandomNonce, nil) + cBinTarget := fs.contentEnc.EncryptBlock([]byte(target), 0, nil) cTarget := base64.URLEncoding.EncodeToString(cBinTarget) // Handle long file name diff --git a/internal/fusefrontend_reverse/reverse_longnames.go b/internal/fusefrontend_reverse/reverse_longnames.go index 6409d95..b3d21c9 100644 --- a/internal/fusefrontend_reverse/reverse_longnames.go +++ b/internal/fusefrontend_reverse/reverse_longnames.go @@ -84,7 +84,7 @@ func (rfs *reverseFS) newNameFile(relPath string) (nodefs.File, fuse.Status) { if err != nil { return nil, fuse.ToStatus(err) } - dirIV := derivePathIV(cDir) + dirIV := derivePathIV(cDir, ivPurposeDirIV) e, err := rfs.findLongnameParent(pDir, dirIV, longname) if err != nil { return nil, fuse.ToStatus(err) diff --git a/internal/fusefrontend_reverse/rfile.go b/internal/fusefrontend_reverse/rfile.go index 8c5e5f3..2656b7b 100644 --- a/internal/fusefrontend_reverse/rfile.go +++ b/internal/fusefrontend_reverse/rfile.go @@ -2,6 +2,7 @@ package fusefrontend_reverse import ( "bytes" + "encoding/binary" "fmt" "io" "os" @@ -20,6 +21,8 @@ type reverseFile struct { fd *os.File // File header (contains the IV) header contentenc.FileHeader + // IV for block 0 + block0IV []byte // Content encryption helper contentEnc *contentenc.ContentEnc } @@ -33,7 +36,7 @@ func (rfs *reverseFS) NewFile(relPath string, flags uint32) (nodefs.File, fuse.S if err != nil { return nil, fuse.ToStatus(err) } - id := derivePathIV(relPath) + id := derivePathIV(relPath, ivPurposeFileID) header := contentenc.FileHeader{ Version: contentenc.CurrentVersion, Id: id, @@ -42,6 +45,7 @@ func (rfs *reverseFS) NewFile(relPath string, flags uint32) (nodefs.File, fuse.S File: nodefs.NewDefaultFile(), fd: fd, header: header, + block0IV: derivePathIV(relPath, ivPurposeBlock0IV), contentEnc: rfs.contentEnc, }, fuse.OK } @@ -52,6 +56,26 @@ func (rf *reverseFile) GetAttr(*fuse.Attr) fuse.Status { return fuse.ENOSYS } +// encryptBlocks - encrypt "plaintext" into a number of ciphertext blocks. +// "plaintext" must already be block-aligned. +func (rf *reverseFile) encryptBlocks(plaintext []byte, firstBlockNo uint64, fileId []byte, block0IV []byte) []byte { + nonce := make([]byte, len(block0IV)) + copy(nonce, block0IV) + block0IVlow := binary.BigEndian.Uint64(block0IV[8:]) + nonceLow := nonce[8:] + + inBuf := bytes.NewBuffer(plaintext) + var outBuf bytes.Buffer + bs := int(rf.contentEnc.PlainBS()) + for blockNo := firstBlockNo; inBuf.Len() > 0; blockNo++ { + binary.BigEndian.PutUint64(nonceLow, block0IVlow+blockNo) + inBlock := inBuf.Next(bs) + outBlock := rf.contentEnc.EncryptBlockNonce(inBlock, blockNo, fileId, nonce) + outBuf.Write(outBlock) + } + return outBuf.Bytes() +} + // readBackingFile: read from the backing plaintext file, encrypt it, return the // ciphertext. // "off" ... ciphertext offset (must be >= HEADER_LEN) @@ -71,7 +95,7 @@ func (rf *reverseFile) readBackingFile(off uint64, length uint64) (out []byte, e plaintext = plaintext[0:n] // Encrypt blocks - ciphertext := rf.contentEnc.EncryptBlocks(plaintext, blocks[0].BlockNo, rf.header.Id, contentenc.ReverseDeterministicNonce) + ciphertext := rf.encryptBlocks(plaintext, blocks[0].BlockNo, rf.header.Id, rf.block0IV) // Crop down to the relevant part lenHave := len(ciphertext) diff --git a/internal/fusefrontend_reverse/rfs.go b/internal/fusefrontend_reverse/rfs.go index 8411461..1b5e812 100644 --- a/internal/fusefrontend_reverse/rfs.go +++ b/internal/fusefrontend_reverse/rfs.go @@ -254,7 +254,7 @@ func (rfs *reverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse. nVirtual := 1 // Encrypt names - dirIV := derivePathIV(cipherPath) + dirIV := derivePathIV(cipherPath, ivPurposeDirIV) for i := range entries { var cName string // ".gocryptfs.reverse.conf" in the root directory is mapped to "gocryptfs.conf" @@ -296,9 +296,9 @@ func (rfs *reverseFS) Readlink(cipherPath string, context *fuse.Context) (string if rfs.args.PlaintextNames { return plainTarget, fuse.OK } - nonce := derivePathIV(cipherPath) + nonce := derivePathIV(cipherPath, ivPurposeSymlinkIV) // Symlinks are encrypted like file contents and base64-encoded - cBinTarget := rfs.contentEnc.EncryptBlock([]byte(plainTarget), 0, nil, contentenc.ExternalNonce, nonce) + cBinTarget := rfs.contentEnc.EncryptBlockNonce([]byte(plainTarget), 0, nil, nonce) cTarget := base64.URLEncoding.EncodeToString(cBinTarget) return cTarget, fuse.OK } diff --git a/internal/fusefrontend_reverse/rpath.go b/internal/fusefrontend_reverse/rpath.go index 55fb481..ca8c442 100644 --- a/internal/fusefrontend_reverse/rpath.go +++ b/internal/fusefrontend_reverse/rpath.go @@ -19,9 +19,20 @@ func saneDir(path string) string { return d } -// derivePathIV derives an IV from an encrypted path by hashing it -func derivePathIV(path string) []byte { - hash := sha256.Sum256([]byte(path)) +type ivPurposeType string + +const ( + ivPurposeDirIV ivPurposeType = "DIRIV" + ivPurposeFileID ivPurposeType = "FILEID" + ivPurposeSymlinkIV ivPurposeType = "SYMLINKIV" + ivPurposeBlock0IV ivPurposeType = "BLOCK0IV" +) + +// derivePathIV derives an IV from an encrypted path by hashing it with sha256 +func derivePathIV(path string, purpose ivPurposeType) []byte { + // Use null byte as separator as it cannot occour in the path + extended := []byte(path + "\000" + string(purpose)) + hash := sha256.Sum256(extended) return hash[:nametransform.DirIVLen] } @@ -43,7 +54,7 @@ func (rfs *reverseFS) decryptPath(relPath string) (string, error) { // Start at the top and recurse currentDir := filepath.Join(parts[:i]...) nameType := nametransform.NameType(part) - dirIV := derivePathIV(currentDir) + dirIV := derivePathIV(currentDir, ivPurposeDirIV) var transformedPart string if nameType == nametransform.LongNameNone { transformedPart, err = rfs.nameTransform.DecryptName(part, dirIV) diff --git a/internal/fusefrontend_reverse/virtualfile.go b/internal/fusefrontend_reverse/virtualfile.go index 351cbdc..2447e16 100644 --- a/internal/fusefrontend_reverse/virtualfile.go +++ b/internal/fusefrontend_reverse/virtualfile.go @@ -14,7 +14,7 @@ func (rfs *reverseFS) newDirIVFile(cRelPath string) (nodefs.File, fuse.Status) { if err != nil { return nil, fuse.ToStatus(err) } - return rfs.NewVirtualFile(derivePathIV(cDir), absDir) + return rfs.NewVirtualFile(derivePathIV(cDir, ivPurposeDirIV), absDir) } type virtualFile struct {