From 96d587c67619d703b58e1eae64523fe85d0fb2d7 Mon Sep 17 00:00:00 2001 From: zhlhlf Date: Wed, 20 Aug 2025 16:54:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8F=AA=E5=8A=A0=E5=AF=86?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=A4=B9=E5=92=8C=E6=96=87=E4=BB=B6=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E5=B9=B6=E4=B8=94=E5=8A=A0=E5=AF=86=E5=90=8E=E4=BF=9D?= =?UTF-8?q?=E7=95=99=E6=96=87=E4=BB=B6=E5=90=8E=E7=BC=80=E6=96=B9=E4=BE=BF?= =?UTF-8?q?=E4=BA=91=E7=9B=98=E5=8F=AF=E4=BB=A5=E7=94=9F=E6=88=90=E7=BC=A9?= =?UTF-8?q?=E6=94=BE=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drivers/all.go | 1 + drivers/crypt2/driver.go | 454 +++++++++++++++++++++++++++++++++++++++ drivers/crypt2/meta.go | 40 ++++ drivers/crypt2/util.go | 68 ++++++ 4 files changed, 563 insertions(+) create mode 100644 drivers/crypt2/driver.go create mode 100644 drivers/crypt2/meta.go create mode 100644 drivers/crypt2/util.go diff --git a/drivers/all.go b/drivers/all.go index 5b274eab0..f189c97c2 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -23,6 +23,7 @@ import ( _ "github.com/OpenListTeam/OpenList/v4/drivers/cloudreve" _ "github.com/OpenListTeam/OpenList/v4/drivers/cloudreve_v4" _ "github.com/OpenListTeam/OpenList/v4/drivers/crypt" + _ "github.com/OpenListTeam/OpenList/v4/drivers/crypt2" _ "github.com/OpenListTeam/OpenList/v4/drivers/doubao" _ "github.com/OpenListTeam/OpenList/v4/drivers/doubao_share" _ "github.com/OpenListTeam/OpenList/v4/drivers/dropbox" diff --git a/drivers/crypt2/driver.go b/drivers/crypt2/driver.go new file mode 100644 index 000000000..be1bda588 --- /dev/null +++ b/drivers/crypt2/driver.go @@ -0,0 +1,454 @@ +package crypt2 + +import ( + "bytes" + "context" + "fmt" + "io" + "strconv" + stdpath "path" + "strings" + "sync" + "github.com/OpenListTeam/OpenList/v4/internal/driver" + "github.com/OpenListTeam/OpenList/v4/internal/errs" + "github.com/OpenListTeam/OpenList/v4/internal/fs" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/internal/op" + "github.com/OpenListTeam/OpenList/v4/internal/sign" + "github.com/OpenListTeam/OpenList/v4/internal/stream" + "github.com/OpenListTeam/OpenList/v4/pkg/http_range" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" + "github.com/OpenListTeam/OpenList/v4/server/common" + rcCrypt "github.com/rclone/rclone/backend/crypt" + "github.com/rclone/rclone/fs/config/configmap" + "github.com/rclone/rclone/fs/config/obscure" + log "github.com/sirupsen/logrus" +) + +type Crypt struct { + model.Storage + Addition + cipher *rcCrypt.Cipher + remoteStorage driver.Driver +} + +const obfuscatedPrefix = "___Obfuscated___" + +func (d *Crypt) Config() driver.Config { + return config +} + +func (d *Crypt) GetAddition() driver.Additional { + return &d.Addition +} + +func (d *Crypt) Init(ctx context.Context) error { + //obfuscate credentials if it's updated or just created + err := d.updateObfusParm(&d.Password) + if err != nil { + return fmt.Errorf("failed to obfuscate password: %w", err) + } + err = d.updateObfusParm(&d.Salt) + if err != nil { + return fmt.Errorf("failed to obfuscate salt: %w", err) + } + + d.FileNameEncoding = utils.GetNoneEmpty(d.FileNameEncoding, "base64") + + op.MustSaveDriverStorage(d) + + //need remote storage exist + storage, err := fs.GetStorage(d.RemotePath, &fs.GetStoragesArgs{}) + if err != nil { + return fmt.Errorf("can't find remote storage: %w", err) + } + d.remoteStorage = storage + + p, _ := strings.CutPrefix(d.Password, obfuscatedPrefix) + p2, _ := strings.CutPrefix(d.Salt, obfuscatedPrefix) + config := configmap.Simple{ + "password": p, + "password2": p2, + "filename_encryption": d.FileNameEnc, + "directory_name_encryption": strconv.FormatBool(d.DirNameEnc), + "filename_encoding": d.FileNameEncoding, + "pass_bad_blocks": "", + } + c, err := rcCrypt.NewCipher(config) + if err != nil { + return fmt.Errorf("failed to create Cipher: %w", err) + } + d.cipher = c + + return nil +} + +func (d *Crypt) updateObfusParm(str *string) error { + temp := *str + if !strings.HasPrefix(temp, obfuscatedPrefix) { + temp, err := obscure.Obscure(temp) + if err != nil { + return err + } + temp = obfuscatedPrefix + temp + *str = temp + } + return nil +} + +func (d *Crypt) Drop(ctx context.Context) error { + return nil +} + +func (d *Crypt) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { + path := dir.GetPath() + //return d.list(ctx, d.RemotePath, path) + //remoteFull + + objs, err := fs.List(ctx, d.getPathForRemote(path, true), &fs.ListArgs{NoLog: true, Refresh: args.Refresh}) + // the obj must implement the model.SetPath interface + // return objs, err + if err != nil { + return nil, err + } + + var result []model.Obj + for _, obj := range objs { + if obj.IsDir() { + name, err := d.getDecryptedName(obj.GetName()) + if err != nil { + //filter illegal files + continue + } + if !d.ShowHidden && strings.HasPrefix(name, ".") { + continue + } + objRes := model.Object{ + Name: name, + Size: 0, + Modified: obj.ModTime(), + IsFolder: obj.IsDir(), + Ctime: obj.CreateTime(), + // discarding hash as it's encrypted + } + result = append(result, &objRes) + } else { + thumb, ok := model.GetThumb(obj) + size, err := d.cipher.DecryptedSize(obj.GetSize()) + // 如果不进行加密文件 读取的大小应该不进行解密 + if d.NoEncryptedFile { + size = obj.GetSize() + } else { + size, err = d.cipher.DecryptedSize(obj.GetSize()) + if err != nil { + log.Warnf("DecryptedSize failed for %s ,will use original size, err:%s", path, err) + size = obj.GetSize() + } + } + name, err := d.getDecryptedName(obj.GetName()) + if err != nil { + //filter illegal files + continue + } + if !d.ShowHidden && strings.HasPrefix(name, ".") { + continue + } + objRes := model.Object{ + Name: name, + Size: size, + Modified: obj.ModTime(), + IsFolder: obj.IsDir(), + Ctime: obj.CreateTime(), + // discarding hash as it's encrypted + } + if d.Thumbnail && thumb == "" { + thumbPath := stdpath.Join(args.ReqPath, ".thumbnails", name+".webp") + thumb = fmt.Sprintf("%s/d%s?sign=%s", + common.GetApiUrl(ctx), + utils.EncodePath(thumbPath, true), + sign.Sign(thumbPath)) + } + if !ok && !d.Thumbnail { + result = append(result, &objRes) + } else { + objWithThumb := model.ObjThumb{ + Object: objRes, + Thumbnail: model.Thumbnail{ + Thumbnail: thumb, + }, + } + result = append(result, &objWithThumb) + } + } + } + + return result, nil +} + +func (d *Crypt) Get(ctx context.Context, path string) (model.Obj, error) { + if utils.PathEqual(path, "/") { + return &model.Object{ + Name: "Root", + IsFolder: true, + Path: "/", + }, nil + } + remoteFullPath := "" + var remoteObj model.Obj + var err, err2 error + firstTryIsFolder, secondTry := guessPath(path) + remoteFullPath = d.getPathForRemote(path, firstTryIsFolder) + remoteObj, err = fs.Get(ctx, remoteFullPath, &fs.GetArgs{NoLog: true}) + if err != nil { + if errs.IsObjectNotFound(err) && secondTry { + //try the opposite + remoteFullPath = d.getPathForRemote(path, !firstTryIsFolder) + remoteObj, err2 = fs.Get(ctx, remoteFullPath, &fs.GetArgs{NoLog: true}) + if err2 != nil { + return nil, err2 + } + } else { + return nil, err + } + } + var size int64 = 0 + name := "" + if !remoteObj.IsDir() { + // 如果不进行加密文件 读取的大小应该不进行解密 + if d.NoEncryptedFile { + size = remoteObj.GetSize() + } else { + size, err = d.cipher.DecryptedSize(remoteObj.GetSize()) + if err != nil { + log.Warnf("DecryptedSize failed for %s ,will use original size, err:%s", path, err) + size = remoteObj.GetSize() + } + } + + name, err = d.getDecryptedName(remoteObj.GetName()) + + if err != nil { + log.Warnf("DecryptFileName failed for %s ,will use original name, err:%s", path, err) + name = remoteObj.GetName() + } + } else { + name, err = d.getDecryptedName(remoteObj.GetName()) + if err != nil { + log.Warnf("DecryptDirName failed for %s ,will use original name, err:%s", path, err) + name = remoteObj.GetName() + } + } + obj := &model.Object{ + Path: path, + Name: name, + Size: size, + Modified: remoteObj.ModTime(), + IsFolder: remoteObj.IsDir(), + } + return obj, nil + //return nil, errs.ObjectNotFound +} + +// https://github.com/rclone/rclone/blob/v1.67.0/backend/crypt/cipher.go#L37 +const fileHeaderSize = 32 + +func (d *Crypt) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + dstDirActualPath, err := d.getActualPathForRemote(file.GetPath(), false) + if err != nil { + return nil, fmt.Errorf("failed to convert path to remote path: %w", err) + } + remoteLink, remoteFile, err := op.Link(ctx, d.remoteStorage, dstDirActualPath, args) + if err != nil { + return nil, err + } + if(d.NoEncryptedFile) { + return remoteLink, nil + } + + rrf, err := stream.GetRangeReaderFromLink(remoteFile.GetSize(), remoteLink) + if err != nil { + _ = remoteLink.Close() + return nil, fmt.Errorf("the remote storage driver need to be enhanced to support encrytion") + } + + mu := &sync.Mutex{} + var fileHeader []byte + rangeReaderFunc := func(ctx context.Context, offset, limit int64) (io.ReadCloser, error) { + length := limit + if offset == 0 && limit > 0 { + mu.Lock() + if limit <= fileHeaderSize { + defer mu.Unlock() + if fileHeader != nil { + return io.NopCloser(bytes.NewReader(fileHeader[:limit])), nil + } + length = fileHeaderSize + } else if fileHeader == nil { + defer mu.Unlock() + } else { + mu.Unlock() + } + } + + remoteReader, err := rrf.RangeRead(ctx, http_range.Range{Start: offset, Length: length}) + if err != nil { + return nil, err + } + + if offset == 0 && limit > 0 { + fileHeader = make([]byte, fileHeaderSize) + n, _ := io.ReadFull(remoteReader, fileHeader) + if n != fileHeaderSize { + fileHeader = nil + return nil, fmt.Errorf("can't read data, expected=%d, got=%d", fileHeaderSize, n) + } + if limit <= fileHeaderSize { + remoteReader.Close() + return io.NopCloser(bytes.NewReader(fileHeader[:limit])), nil + } else { + remoteReader = utils.ReadCloser{ + Reader: io.MultiReader(bytes.NewReader(fileHeader), remoteReader), + Closer: remoteReader, + } + } + } + return remoteReader, nil + } + return &model.Link{ + RangeReader: stream.RangeReaderFunc(func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) { + readSeeker, err := d.cipher.DecryptDataSeek(ctx, rangeReaderFunc, httpRange.Start, httpRange.Length) + if err != nil { + return nil, err + } + return readSeeker, nil + }), + SyncClosers: utils.NewSyncClosers(remoteLink), + }, nil +} + +func (d *Crypt) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { + dstDirActualPath, err := d.getActualPathForRemote(parentDir.GetPath(), true) + if err != nil { + return fmt.Errorf("failed to convert path to remote path: %w", err) + } + dir, err := d.getEncryptedDirName(dirName) + return op.MakeDir(ctx, d.remoteStorage, stdpath.Join(dstDirActualPath, dir)) +} + +func (d *Crypt) Move(ctx context.Context, srcObj, dstDir model.Obj) error { + srcRemoteActualPath, err := d.getActualPathForRemote(srcObj.GetPath(), srcObj.IsDir()) + if err != nil { + return fmt.Errorf("failed to convert path to remote path: %w", err) + } + dstRemoteActualPath, err := d.getActualPathForRemote(dstDir.GetPath(), dstDir.IsDir()) + if err != nil { + return fmt.Errorf("failed to convert path to remote path: %w", err) + } + return op.Move(ctx, d.remoteStorage, srcRemoteActualPath, dstRemoteActualPath) +} + +func (d *Crypt) Rename(ctx context.Context, srcObj model.Obj, newName string) error { + remoteActualPath, err := d.getActualPathForRemote(srcObj.GetPath(), srcObj.IsDir()) + if err != nil { + return fmt.Errorf("failed to convert path to remote path: %w", err) + } + var newEncryptedName string + if srcObj.IsDir() { + newEncryptedName, err = d.getEncryptedDirName(newName) + } else { + newEncryptedName, err = d.getEncryptedName(newName) + } + if err != nil { + return fmt.Errorf("failed to get encrypted name: %w", err) + } + return op.Rename(ctx, d.remoteStorage, remoteActualPath, newEncryptedName) +} + +func (d *Crypt) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { + srcRemoteActualPath, err := d.getActualPathForRemote(srcObj.GetPath(), srcObj.IsDir()) + if err != nil { + return fmt.Errorf("failed to convert path to remote path: %w", err) + } + dstRemoteActualPath, err := d.getActualPathForRemote(dstDir.GetPath(), dstDir.IsDir()) + if err != nil { + return fmt.Errorf("failed to convert path to remote path: %w", err) + } + return op.Copy(ctx, d.remoteStorage, srcRemoteActualPath, dstRemoteActualPath) + +} + +func (d *Crypt) Remove(ctx context.Context, obj model.Obj) error { + remoteActualPath, err := d.getActualPathForRemote(obj.GetPath(), obj.IsDir()) + if err != nil { + return fmt.Errorf("failed to convert path to remote path: %w", err) + } + return op.Remove(ctx, d.remoteStorage, remoteActualPath) +} + +func (d *Crypt) Put(ctx context.Context, dstDir model.Obj, streamer model.FileStreamer, up driver.UpdateProgress) error { + + dstDirActualPath, err := d.getActualPathForRemote(dstDir.GetPath(), true) + if err != nil { + return fmt.Errorf("failed to convert path to remote path: %w", err) + } + name, err := d.getEncryptedName(streamer.GetName()) + if err != nil { + return fmt.Errorf("failed to get encrypted name: %w", err) + } + if d.NoEncryptedFile { + streamOut := &stream.FileStream{ + Obj: &model.Object{ + ID: streamer.GetID(), + Path: streamer.GetPath(), + Name: name, + Size: streamer.GetSize(), + Modified: streamer.ModTime(), + IsFolder: streamer.IsDir(), + }, + Reader: streamer, + Mimetype: "application/octet-stream", + WebPutAsTask: streamer.NeedStore(), + ForceStreamUpload: true, + Exist: streamer.GetExist(), + } + err = op.Put(ctx, d.remoteStorage, dstDirActualPath, streamOut, up, false) + if err != nil { + return err + } else { + return nil + } + } + // Encrypt the data into wrappedIn + wrappedIn, err := d.cipher.EncryptData(streamer) + if err != nil { + return fmt.Errorf("failed to EncryptData: %w", err) + } + + // doesn't support seekableStream, since rapid-upload is not working for encrypted data + streamOut := &stream.FileStream{ + Obj: &model.Object{ + ID: streamer.GetID(), + Path: streamer.GetPath(), + Name: name, + Size: d.cipher.EncryptedSize(streamer.GetSize()), + Modified: streamer.ModTime(), + IsFolder: streamer.IsDir(), + }, + Reader: wrappedIn, + Mimetype: "application/octet-stream", + WebPutAsTask: streamer.NeedStore(), + ForceStreamUpload: true, + Exist: streamer.GetExist(), + } + err = op.Put(ctx, d.remoteStorage, dstDirActualPath, streamOut, up, false) + if err != nil { + return err + } + return nil +} + +//func (d *Safe) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { +// return nil, errs.NotSupport +//} + +var _ driver.Driver = (*Crypt)(nil) diff --git a/drivers/crypt2/meta.go b/drivers/crypt2/meta.go new file mode 100644 index 000000000..2ab0248fb --- /dev/null +++ b/drivers/crypt2/meta.go @@ -0,0 +1,40 @@ +package crypt2 + +import ( + "github.com/OpenListTeam/OpenList/v4/internal/driver" + "github.com/OpenListTeam/OpenList/v4/internal/op" +) + +type Addition struct { + // Usually one of two + //driver.RootPath + //driver.RootID + // define other + + RemotePath string `json:"remote_path" required:"true" help:"This is where the encrypted data stores"` + + Password string `json:"password" required:"true" confidential:"true" help:"the main password"` + Salt string `json:"salt" confidential:"true" help:"If you don't know what is salt, treat it as a second password. Optional but recommended"` + FileNameEnc string `json:"filename_encryption" type:"select" required:"true" options:"off,standard,obfuscate" default:"off"` + FileNameEncoding string `json:"filename_encoding" type:"select" required:"true" options:"base64,base32,base32768" default:"base64" help:"for advanced user only!"` + + Thumbnail bool `json:"thumbnail" required:"true" default:"false" help:"enable thumbnail which pre-generated under .thumbnails folder"` + ShowHidden bool `json:"show_hidden" default:"true" required:"false" help:"show hidden directories and files"` + DirNameEnc bool `json:"directory_name_encryption" default:"false"` + NoEncryptedFile bool `json:"no_encrypted_file" default:"false"` +} + +var config = driver.Config{ + Name: "Crypt2", + LocalSort: true, + OnlyProxy: false, + NoCache: true, + DefaultRoot: "/", + NoLinkURL: false, +} + +func init() { + op.RegisterDriver(func() driver.Driver { + return &Crypt{} + }) +} diff --git a/drivers/crypt2/util.go b/drivers/crypt2/util.go new file mode 100644 index 000000000..422c89f76 --- /dev/null +++ b/drivers/crypt2/util.go @@ -0,0 +1,68 @@ +package crypt2 + +import ( + stdpath "path" + "path/filepath" + "strings" + "github.com/OpenListTeam/OpenList/v4/internal/op" +) + +// will give the best guessing based on the path +func guessPath(path string) (isFolder, secondTry bool) { + if strings.HasSuffix(path, "/") { + //confirmed a folder + return true, false + } + lastSlash := strings.LastIndex(path, "/") + if strings.Index(path[lastSlash:], ".") < 0 { + //no dot, try folder then try file + return true, true + } + return false, true +} + +func (d *Crypt) getPathForRemote(path string, isFolder bool) (remoteFullPath string) { + if isFolder && !strings.HasSuffix(path, "/") { + path = path + "/" + } + dir, fileName := filepath.Split(path) + + remoteDir, err := d.getEncryptedDirName(dir) + remoteFileName := "" + if len(strings.TrimSpace(fileName)) > 0 { + remoteFileName, err = d.getEncryptedName(fileName) + } + if err != nil { + return stdpath.Join(d.RemotePath, remoteDir, "") + } + return stdpath.Join(d.RemotePath, remoteDir, remoteFileName) + +} + +// actual path is used for internal only. any link for user should come from remoteFullPath +func (d *Crypt) getActualPathForRemote(path string, isFolder bool) (string, error) { + _, remoteActualPath, err := op.GetStorageAndActualPath(d.getPathForRemote(path, isFolder)) + return remoteActualPath, err +} + +// 加密文件名(保留扩展名不变) +func (d *Crypt) getEncryptedName(filename string) (string, error) { + ext := filepath.Ext(filename) + base := filename[:len(filename)-len(ext)] + encrypted := d.cipher.EncryptFileName(base) + return encrypted + ext, nil +} + +// 加密文件夹名 +func (d *Crypt) getEncryptedDirName(dirName string) (string, error) { + encrypted := d.cipher.EncryptDirName(dirName) + return encrypted, nil +} + +// 解密文件名or文件夹名(文件保留扩展名不变) +func (d *Crypt) getDecryptedName(filename string) (string, error) { + ext := filepath.Ext(filename) + base := filename[:len(filename)-len(ext)] + decrypted,err := d.cipher.DecryptFileName(base) + return decrypted + ext, err +} \ No newline at end of file