/***************************************************************************** * Author: Valient Gough * ***************************************************************************** * Copyright (c) 2003-2004, Valient Gough * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "fs/encfs.h" #include #include #include #include #include #include #ifdef linux #include #endif #include #include "base/Error.h" #include "base/Mutex.h" #include "fs/Context.h" #include "fs/DirNode.h" #include "fs/FileUtils.h" #include "fs/fsconfig.pb.h" #include #include using std::list; using std::string; namespace encfs { class DirDeleter { public: void operator () ( DIR *d ) { ::closedir( d ); } }; DirTraverse::DirTraverse(const shared_ptr &_dirPtr, uint64_t _iv, const shared_ptr &_naming) : dir( _dirPtr ) , iv( _iv ) , naming( _naming ) { } DirTraverse::DirTraverse(const DirTraverse &src) : dir( src.dir ) , iv( src.iv ) , naming( src.naming ) { } DirTraverse &DirTraverse::operator = (const DirTraverse &src) { dir = src.dir; iv = src.iv; naming = src.naming; return *this; } DirTraverse::~DirTraverse() { dir.reset(); iv = 0; naming.reset(); } static bool _nextName(struct dirent *&de, const shared_ptr &dir, int *fileType, ino_t *inode) { de = ::readdir( dir.get() ); if(de) { if(fileType) { #if defined(_DIRENT_HAVE_D_TYPE) || defined(__FreeBSD__) *fileType = de->d_type; #else #warning "struct dirent.d_type not supported" *fileType = 0; #endif } if(inode) *inode = de->d_ino; return true; } else { if(fileType) *fileType = 0; return false; } } std::string DirTraverse::nextPlaintextName(int *fileType, ino_t *inode) { struct dirent *de=0; while(_nextName(de, dir, fileType, inode)) { try { uint64_t localIv = iv; return naming->decodePath( de->d_name, &localIv ); } catch ( Error &ex ) { // .. .problem decoding, ignore it and continue on to next name.. VLOG(1) << "error decoding filename " << de->d_name << " : " << ex.what(); } } return string(); } std::string DirTraverse::nextInvalid() { struct dirent *de=0; // find the first name which produces a decoding error... while(_nextName(de, dir, (int*)0, (ino_t*)0)) { try { uint64_t localIv = iv; naming->decodePath( de->d_name, &localIv ); continue; } catch( Error &ex ) { return string( de->d_name ); } } return string(); } struct RenameEl { // ciphertext names string oldCName; string newCName; // intermediate name (not final cname) // plaintext names string oldPName; string newPName; bool isDirectory; }; class RenameOp { private: DirNode *dn; shared_ptr< list > renameList; list::const_iterator last; public: RenameOp( DirNode *_dn, const shared_ptr< list > &_renameList ) : dn(_dn), renameList(_renameList) { last = renameList->begin(); } RenameOp(const RenameOp &src) : dn(src.dn) , renameList(src.renameList) , last(src.last) { } ~RenameOp(); operator bool () const { return renameList; } bool apply(); void undo(); }; RenameOp::~RenameOp() { if(renameList) { // got a bunch of decoded filenames sitting in memory.. do a little // cleanup before leaving.. list::iterator it; for(it = renameList->begin(); it != renameList->end(); ++it) { it->oldPName.assign( it->oldPName.size(), ' ' ); it->newPName.assign( it->newPName.size(), ' ' ); } } } bool RenameOp::apply() { try { while(last != renameList->end()) { // backing store rename. VLOG(2) << "renaming " << last->oldCName << "-> " << last->newCName; struct stat st; bool preserve_mtime = ::stat(last->oldCName.c_str(), &st) == 0; // internal node rename.. dn->renameNode( last->oldPName.c_str(), last->newPName.c_str() ); // rename on disk.. if(::rename( last->oldCName.c_str(), last->newCName.c_str() ) == -1) { LOG(WARNING) << "Error renaming " << last->oldCName << ": " << strerror(errno); dn->renameNode( last->newPName.c_str(), last->oldPName.c_str(), false ); return false; } if(preserve_mtime) { struct utimbuf ut; ut.actime = st.st_atime; ut.modtime = st.st_mtime; ::utime(last->newCName.c_str(), &ut); } ++last; } return true; } catch( Error &err ) { LOG(WARNING) << "caught error in rename application: " << err.what(); return false; } } void RenameOp::undo() { VLOG(1) << "in undoRename"; if(last == renameList->begin()) { VLOG(1) << "nothing to undo"; return; // nothing to undo } // list has to be processed backwards, otherwise we may rename // directories and directory contents in the wrong order! int undoCount = 0; int errorCount = 0; list::const_iterator it = last; while( it != renameList->begin() ) { --it; VLOG(1) << "undo: renaming " << it->newCName << " -> " << it->oldCName; ::rename( it->newCName.c_str(), it->oldCName.c_str() ); try { dn->renameNode( it->newPName.c_str(), it->oldPName.c_str(), false ); } catch( Error &err ) { if (++errorCount == 1) LOG(WARNING) << "error in rename und: " << err.what(); // continue on anyway... } ++undoCount; }; LOG(WARNING) << "Undo rename count: " << undoCount; } DirNode::DirNode(EncFS_Context *_ctx, const string &sourceDir, const FSConfigPtr &_config) { Lock _lock( mutex ); ctx = _ctx; rootDir = sourceDir; fsConfig = _config; // make sure rootDir ends in '/', so that we can form a path by appending // the rest.. if( rootDir[ rootDir.length()-1 ] != '/' ) rootDir.append( 1, '/'); naming = fsConfig->nameCoding; } DirNode::~DirNode() { } bool DirNode::hasDirectoryNameDependency() const { return naming ? naming->getChainedNameIV() : false; } string DirNode::rootDirectory() { // don't update last access here, otherwise 'du' would cause lastAccess to // be reset. // chop off '/' terminator from root dir. return string( rootDir, 0, rootDir.length()-1 ); } string DirNode::cipherPath( const char *plaintextPath ) { return rootDir + naming->encodePath( plaintextPath ); } string DirNode::cipherPathWithoutRoot( const char *plaintextPath ) { return naming->encodePath( plaintextPath ); } string DirNode::plainPath( const char *cipherPath_ ) { try { if( !strncmp( cipherPath_, rootDir.c_str(), rootDir.length() ) ) { return naming->decodePath( cipherPath_ + rootDir.length() ); } else { if ( cipherPath_[0] == '+' ) { // decode as fully qualified path return string("/") + naming->decodeName( cipherPath_+1, strlen(cipherPath_+1) ); } else { return naming->decodePath( cipherPath_ ); } } } catch( Error &err ) { LOG(ERROR) << "decode err: " << err.what(); return string(); } } string DirNode::relativeCipherPath( const char *plaintextPath ) { try { if(plaintextPath[0] == '/') { // mark with '+' to indicate special decoding.. return string("+") + naming->encodeName(plaintextPath+1, strlen(plaintextPath+1)); } else { return naming->encodePath( plaintextPath ); } } catch( Error &err ) { LOG(ERROR) << "encode err: " << err.what(); return string(); } } DirTraverse DirNode::openDir(const char *plaintextPath) { string cyName = rootDir + naming->encodePath( plaintextPath ); //rDebug("openDir on %s", cyName.c_str() ); DIR *dir = ::opendir( cyName.c_str() ); if(dir == NULL) { VLOG(1) << "opendir error " << strerror(errno); return DirTraverse( shared_ptr(), 0, shared_ptr() ); } else { shared_ptr dp( dir, DirDeleter() ); uint64_t iv = 0; // if we're using chained IV mode, then compute the IV at this // directory level.. try { if( naming->getChainedNameIV() ) naming->encodePath( plaintextPath, &iv ); } catch( Error &err ) { LOG(ERROR) << "encode err: " << err.what(); } return DirTraverse( dp, iv, naming ); } } bool DirNode::genRenameList( list &renameList, const char *fromP, const char *toP ) { uint64_t fromIV = 0, toIV = 0; // compute the IV for both paths string fromCPart = naming->encodePath( fromP, &fromIV ); string toCPart = naming->encodePath( toP, &toIV ); // where the files live before the rename.. string sourcePath = rootDir + fromCPart; // ok..... we wish it was so simple.. should almost never happen if(fromIV == toIV) return true; // generate the real destination path, where we expect to find the files.. VLOG(1) << "opendir " << sourcePath; shared_ptr dir = shared_ptr( opendir( sourcePath.c_str() ), DirDeleter() ); if(!dir) return false; struct dirent *de = NULL; while((de = ::readdir( dir.get() )) != NULL) { // decode the name using the oldIV uint64_t localIV = fromIV; string plainName; if((de->d_name[0] == '.') && ((de->d_name[1] == '\0') || ((de->d_name[1] == '.') && (de->d_name[2] == '\0')))) { // skip "." and ".." continue; } try { plainName = naming->decodePath( de->d_name, &localIV ); } catch( Error &ex ) { // if filename can't be decoded, then ignore it.. continue; } // any error in the following will trigger a rename failure. try { // re-encode using the new IV.. localIV = toIV; string newName = naming->encodePath( plainName.c_str(), &localIV ); // store rename information.. string oldFull = sourcePath + '/' + de->d_name; string newFull = sourcePath + '/' + newName; RenameEl ren; ren.oldCName = oldFull; ren.newCName = newFull; ren.oldPName = string(fromP) + '/' + plainName; ren.newPName = string(toP) + '/' + plainName; bool isDir; #if defined(_DIRENT_HAVE_D_TYPE) if(de->d_type != DT_UNKNOWN) { isDir = (de->d_type == DT_DIR); } else #endif { isDir = isDirectory( oldFull.c_str() ); } ren.isDirectory = isDir; if(isDir) { // recurse.. We want to add subdirectory elements before the // parent, as that is the logical rename order.. if(!genRenameList( renameList, ren.oldPName.c_str(), ren.newPName.c_str())) { return false; } } VLOG(1) << "adding file " << oldFull << " to rename list"; renameList.push_back( ren ); } catch( Error &err ) { // We can't convert this name, because we don't have a valid IV for // it (or perhaps a valid key).. It will be inaccessible.. LOG(WARNING) << "Aborting rename: error on file " << fromCPart.append(1, '/').append(de->d_name) << ":" << err.what(); // abort.. Err on the side of safety and disallow rename, rather // then loosing files.. return false; } } return true; } /* A bit of a pain.. If a directory is renamed in a filesystem with directory initialization vector chaining, then we have to recursively rename every descendent of this directory, as all initialization vectors will have changed.. Returns a list of renamed items on success, a null list on failure. */ shared_ptr DirNode::newRenameOp( const char *fromP, const char *toP ) { // Do the rename in two stages to avoid chasing our tail // Undo everything if we encounter an error! shared_ptr< list > renameList(new list); if(!genRenameList( *renameList.get(), fromP, toP )) { LOG(WARNING) << "Error during generation of recursive rename list"; return shared_ptr(); } else return shared_ptr( new RenameOp(this, renameList) ); } int DirNode::mkdir(const char *plaintextPath, mode_t mode, uid_t uid, gid_t gid) { string cyName = rootDir + naming->encodePath( plaintextPath ); rAssert( !cyName.empty() ); VLOG(1) << "mkdir on " << cyName; // if uid or gid are set, then that should be the directory owner int olduid = -1; int oldgid = -1; if(uid != 0) olduid = setfsuid( uid ); if(gid != 0) oldgid = setfsgid( gid ); int res = ::mkdir( cyName.c_str(), mode ); if(olduid >= 0) setfsuid( olduid ); if(oldgid >= 0) setfsgid( oldgid ); if(res == -1) { int eno = errno; LOG(WARNING) << "mkdir error on " << cyName << " mode " << mode << ": " << strerror(eno); res = -eno; } else res = 0; return res; } int DirNode::rename( const char *fromPlaintext, const char *toPlaintext ) { Lock _lock( mutex ); string fromCName = rootDir + naming->encodePath( fromPlaintext ); string toCName = rootDir + naming->encodePath( toPlaintext ); rAssert( !fromCName.empty() ); rAssert( !toCName.empty() ); VLOG(1) << "rename " << fromCName << " -> " << toCName; shared_ptr toNode = findOrCreate( toPlaintext ); shared_ptr renameOp; if( hasDirectoryNameDependency() && isDirectory( fromCName.c_str() )) { VLOG(1) << "recursive rename begin"; renameOp = newRenameOp( fromPlaintext, toPlaintext ); if(!renameOp || !renameOp->apply()) { if(renameOp) renameOp->undo(); LOG(WARNING) << "rename aborted"; return -EACCES; } VLOG(1) << "recursive rename end"; } int res = 0; try { struct stat st; bool preserve_mtime = ::stat(fromCName.c_str(), &st) == 0; renameNode( fromPlaintext, toPlaintext ); res = ::rename( fromCName.c_str(), toCName.c_str() ); if(res == -1) { // undo res = -errno; renameNode( toPlaintext, fromPlaintext, false ); if(renameOp) renameOp->undo(); } else if(preserve_mtime) { struct utimbuf ut; ut.actime = st.st_atime; ut.modtime = st.st_mtime; ::utime(toCName.c_str(), &ut); } } catch( Error &err ) { // exception from renameNode, just show the error and continue.. LOG(ERROR) << "rename err: " << err.what(); res = -EIO; } if(res != 0) { VLOG(1) << "rename failed: " << strerror( errno ); res = -errno; } return res; } int DirNode::link( const char *from, const char *to ) { Lock _lock( mutex ); string fromCName = rootDir + naming->encodePath( from ); string toCName = rootDir + naming->encodePath( to ); rAssert( !fromCName.empty() ); rAssert( !toCName.empty() ); VLOG(1) << "link " << fromCName << " -> " << toCName; int res = -EPERM; if( fsConfig->config->external_iv() ) { VLOG(1) << "hard links not supported with external IV chaining!"; } else { res = ::link( fromCName.c_str(), toCName.c_str() ); if(res == -1) res = -errno; else res = 0; } return res; } /* The node is keyed by filename, so a rename means the internal node names must be changed. */ shared_ptr DirNode::renameNode( const char *from, const char *to ) { return renameNode( from, to, true ); } shared_ptr DirNode::renameNode( const char *from, const char *to, bool forwardMode ) { shared_ptr node = findOrCreate( from ); if(node) { uint64_t newIV = 0; string cname = rootDir + naming->encodePath( to, &newIV ); VLOG(1) << "renaming internal node " << node->cipherName() << " -> " << cname.c_str(); if(node->setName( to, cname.c_str(), newIV, forwardMode )) { if(ctx) ctx->renameNode( from, to ); } else { // rename error! - put it back LOG(ERROR) << "renameNode failed"; throw Error("Internal node name change failed!"); } } return node; } shared_ptr DirNode::findOrCreate( const char *plainName) { shared_ptr node; if(ctx) node = ctx->lookupNode( plainName ); if(!node) { uint64_t iv = 0; string cipherName = naming->encodePath( plainName, &iv ); node.reset( new FileNode( this, fsConfig, plainName, (rootDir + cipherName).c_str())); if(fsConfig->config->external_iv()) node->setName(0, 0, iv); VLOG(1) << "created FileNode for " << node->cipherName(); } return node; } shared_ptr DirNode::lookupNode( const char *plainName, const char * requestor ) { (void)requestor; Lock _lock( mutex ); shared_ptr node = findOrCreate( plainName ); return node; } /* Similar to lookupNode, except that we also call open() and only return a node on sucess.. This is done in one step to avoid any race conditions with the stored state of the file. */ shared_ptr DirNode::openNode( const char *plainName, const char * requestor, int flags, int *result ) { (void)requestor; rAssert( result != NULL ); Lock _lock( mutex ); shared_ptr node = findOrCreate( plainName ); if( node && (*result = node->open( flags )) >= 0 ) return node; else return shared_ptr(); } int DirNode::unlink( const char *plaintextName ) { string cyName = naming->encodePath( plaintextName ); VLOG(1) << "unlink " << cyName; Lock _lock( mutex ); int res = 0; if(ctx && ctx->lookupNode( plaintextName )) { // If FUSE is running with "hard_remove" option where it doesn't // hide open files for us, then we can't allow an unlink of an open // file.. LOG(WARNING) << "Refusing to unlink open file: " << cyName << ", hard_remove option is probably in effect"; res = -EBUSY; } else { string fullName = rootDir + cyName; res = ::unlink( fullName.c_str() ); if(res == -1) { res = -errno; VLOG(1) << "unlink error: " << strerror(errno); } } return res; } } // namespace encfs