|
package crypt |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"io" |
|
stdpath "path" |
|
"regexp" |
|
"strings" |
|
|
|
"github.com/alist-org/alist/v3/internal/driver" |
|
"github.com/alist-org/alist/v3/internal/errs" |
|
"github.com/alist-org/alist/v3/internal/fs" |
|
"github.com/alist-org/alist/v3/internal/model" |
|
"github.com/alist-org/alist/v3/internal/op" |
|
"github.com/alist-org/alist/v3/internal/sign" |
|
"github.com/alist-org/alist/v3/internal/stream" |
|
"github.com/alist-org/alist/v3/pkg/http_range" |
|
"github.com/alist-org/alist/v3/pkg/utils" |
|
"github.com/alist-org/alist/v3/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 { |
|
|
|
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) |
|
} |
|
|
|
isCryptExt := regexp.MustCompile(`^[.][A-Za-z0-9-_]{2,}$`).MatchString |
|
if !isCryptExt(d.EncryptedSuffix) { |
|
return fmt.Errorf("EncryptedSuffix is Illegal") |
|
} |
|
d.FileNameEncoding = utils.GetNoneEmpty(d.FileNameEncoding, "base64") |
|
d.EncryptedSuffix = utils.GetNoneEmpty(d.EncryptedSuffix, ".bin") |
|
|
|
op.MustSaveDriverStorage(d) |
|
|
|
|
|
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": d.DirNameEnc, |
|
"filename_encoding": d.FileNameEncoding, |
|
"suffix": d.EncryptedSuffix, |
|
"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() |
|
|
|
|
|
|
|
objs, err := fs.List(ctx, d.getPathForRemote(path, true), &fs.ListArgs{NoLog: true}) |
|
|
|
|
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
var result []model.Obj |
|
for _, obj := range objs { |
|
if obj.IsDir() { |
|
name, err := d.cipher.DecryptDirName(obj.GetName()) |
|
if err != nil { |
|
|
|
continue |
|
} |
|
if !d.ShowHidden && strings.HasPrefix(name, ".") { |
|
continue |
|
} |
|
objRes := model.Object{ |
|
Name: name, |
|
Size: 0, |
|
Modified: obj.ModTime(), |
|
IsFolder: obj.IsDir(), |
|
Ctime: obj.CreateTime(), |
|
|
|
} |
|
result = append(result, &objRes) |
|
} else { |
|
thumb, ok := model.GetThumb(obj) |
|
size, err := d.cipher.DecryptedSize(obj.GetSize()) |
|
if err != nil { |
|
|
|
continue |
|
} |
|
name, err := d.cipher.DecryptFileName(obj.GetName()) |
|
if err != nil { |
|
|
|
continue |
|
} |
|
if !d.ShowHidden && strings.HasPrefix(name, ".") { |
|
continue |
|
} |
|
objRes := model.Object{ |
|
Name: name, |
|
Size: size, |
|
Modified: obj.ModTime(), |
|
IsFolder: obj.IsDir(), |
|
Ctime: obj.CreateTime(), |
|
|
|
} |
|
if d.Thumbnail && thumb == "" { |
|
thumbPath := stdpath.Join(args.ReqPath, ".thumbnails", name+".webp") |
|
thumb = fmt.Sprintf("%s/d%s?sign=%s", |
|
common.GetApiUrl(common.GetHttpReq(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 { |
|
|
|
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() { |
|
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.cipher.DecryptFileName(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.cipher.DecryptDirName(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 |
|
|
|
} |
|
|
|
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 remoteLink.RangeReadCloser == nil && remoteLink.MFile == nil && len(remoteLink.URL) == 0 { |
|
return nil, fmt.Errorf("the remote storage driver need to be enhanced to support encrytion") |
|
} |
|
remoteFileSize := remoteFile.GetSize() |
|
remoteClosers := utils.EmptyClosers() |
|
rangeReaderFunc := func(ctx context.Context, underlyingOffset, underlyingLength int64) (io.ReadCloser, error) { |
|
length := underlyingLength |
|
if underlyingLength >= 0 && underlyingOffset+underlyingLength >= remoteFileSize { |
|
length = -1 |
|
} |
|
rrc := remoteLink.RangeReadCloser |
|
if len(remoteLink.URL) > 0 { |
|
|
|
rangedRemoteLink := &model.Link{ |
|
URL: remoteLink.URL, |
|
Header: remoteLink.Header, |
|
} |
|
var converted, err = stream.GetRangeReadCloserFromLink(remoteFileSize, rangedRemoteLink) |
|
if err != nil { |
|
return nil, err |
|
} |
|
rrc = converted |
|
} |
|
if rrc != nil { |
|
|
|
remoteReader, err := rrc.RangeRead(ctx, http_range.Range{Start: underlyingOffset, Length: length}) |
|
remoteClosers.AddClosers(rrc.GetClosers()) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return remoteReader, nil |
|
} |
|
if remoteLink.MFile != nil { |
|
_, err := remoteLink.MFile.Seek(underlyingOffset, io.SeekStart) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
|
|
remoteClosers.Add(remoteLink.MFile) |
|
return io.NopCloser(remoteLink.MFile), nil |
|
} |
|
|
|
return nil, errs.NotSupport |
|
|
|
} |
|
resultRangeReader := 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 |
|
} |
|
|
|
resultRangeReadCloser := &model.RangeReadCloser{RangeReader: resultRangeReader, Closers: remoteClosers} |
|
resultLink := &model.Link{ |
|
Header: remoteLink.Header, |
|
RangeReadCloser: resultRangeReadCloser, |
|
Expiration: remoteLink.Expiration, |
|
} |
|
|
|
return resultLink, 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 := d.cipher.EncryptDirName(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 = d.cipher.EncryptDirName(newName) |
|
} else { |
|
newEncryptedName = d.cipher.EncryptFileName(newName) |
|
} |
|
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) |
|
} |
|
|
|
|
|
wrappedIn, err := d.cipher.EncryptData(streamer) |
|
if err != nil { |
|
return fmt.Errorf("failed to EncryptData: %w", err) |
|
} |
|
|
|
|
|
streamOut := &stream.FileStream{ |
|
Obj: &model.Object{ |
|
ID: streamer.GetID(), |
|
Path: streamer.GetPath(), |
|
Name: d.cipher.EncryptFileName(streamer.GetName()), |
|
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 |
|
} |
|
|
|
|
|
|
|
|
|
|
|
var _ driver.Driver = (*Crypt)(nil) |
|
|