diff --git a/README.md b/README.md index 7599469..7a5a1a3 100644 --- a/README.md +++ b/README.md @@ -137,13 +137,9 @@ In addition to the mandatory variables, you can also set the following optional - `ENCRYPTION_KEY` : Password for Encryption. -- `ENCRYPTION_SALT` : Salt for Encryption. - > [!WARNING] -> Keep your Password and Salt safe once generated , teldrive uses same encryption as of rclone internally -so you don't need to enable crypt in rclone.Enabling crypt in rclone makes UI reduntant so encrypting files -in teldrive internally is better way to encrypt files instead of enabling in rclone.To encrypt files see more -about teldrive rclone config. +> Keep your Password safe once generated teldrive uses same encryption as of rclone internally +so you don't need to enable crypt in rclone.**Teldrive generates random salt for each file part and saves in database so its more secure than rclone crypt whereas in rclone same salt value is used for all files by which can be compromised easily**. Enabling crypt in rclone makes UI reduntant so encrypting files in teldrive internally is better way to encrypt files and more secure encryption than rclone.To encrypt files see more about teldrive rclone config. ### For making use of Multi Bots support diff --git a/config/config.go b/config/config.go index 17d592e..3137069 100644 --- a/config/config.go +++ b/config/config.go @@ -34,7 +34,6 @@ type Config struct { UploadRetention int `envconfig:"UPLOAD_RETENTION" default:"15"` DisableStreamBots bool `envconfig:"DISABLE_STREAM_BOTS" default:"false"` EncryptionKey string `envconfig:"ENCRYPTION_KEY"` - EncryptionSalt string `envconfig:"ENCRYPTION_SALT"` ExecDir string } diff --git a/internal/crypt/cipher.go b/internal/crypt/cipher.go index 47e6755..149c08f 100644 --- a/internal/crypt/cipher.go +++ b/internal/crypt/cipher.go @@ -533,7 +533,7 @@ func (c *Cipher) DecryptDataSeek(ctx context.Context, open OpenRangeSeek, offset return out, nil } -func (c *Cipher) EncryptedSize(size int64) int64 { +func EncryptedSize(size int64) int64 { blocks, residue := size/blockDataSize, size%blockDataSize encryptedSize := int64(fileHeaderSize) + blocks*(blockHeaderSize+blockDataSize) if residue != 0 { @@ -542,7 +542,7 @@ func (c *Cipher) EncryptedSize(size int64) int64 { return encryptedSize } -func (c *Cipher) DecryptedSize(size int64) (int64, error) { +func DecryptedSize(size int64) (int64, error) { size -= int64(fileHeaderSize) if size < 0 { return 0, ErrorEncryptedFileTooShort diff --git a/internal/reader/decrypted-reader.go b/internal/reader/decrypted-reader.go index 412855a..55fcfa7 100644 --- a/internal/reader/decrypted-reader.go +++ b/internal/reader/decrypted-reader.go @@ -4,6 +4,7 @@ import ( "context" "io" + "github.com/divyam234/teldrive/config" "github.com/divyam234/teldrive/internal/crypt" "github.com/divyam234/teldrive/pkg/types" "github.com/gotd/td/telegram" @@ -17,14 +18,13 @@ type decrpytedReader struct { reader io.ReadCloser bytesread int64 contentLength int64 - cipher *crypt.Cipher + config *config.Config } func NewDecryptedReader( ctx context.Context, client *telegram.Client, parts []types.Part, - cipher *crypt.Cipher, contentLength int64) (io.ReadCloser, error) { r := &decrpytedReader{ @@ -32,7 +32,7 @@ func NewDecryptedReader( parts: parts, client: client, contentLength: contentLength, - cipher: cipher, + config: config.GetConfig(), } res, err := r.nextPart() @@ -80,7 +80,9 @@ func (r *decrpytedReader) Close() (err error) { func (r *decrpytedReader) nextPart() (io.ReadCloser, error) { - return r.cipher.DecryptDataSeek(r.ctx, + cipher, _ := crypt.NewCipher(r.config.EncryptionKey, r.parts[r.pos].Salt) + + return cipher.DecryptDataSeek(r.ctx, func(ctx context.Context, underlyingOffset, underlyingLimit int64) (io.ReadCloser, error) { diff --git a/pkg/database/migrations/20231208020544_alter.sql b/pkg/database/migrations/20231208020544_alter.sql deleted file mode 100644 index 3c72956..0000000 --- a/pkg/database/migrations/20231208020544_alter.sql +++ /dev/null @@ -1,5 +0,0 @@ --- +goose Up --- +goose StatementBegin -ALTER TABLE teldrive.files ADD COLUMN "encrypted" BOOLEAN NOT NULL DEFAULT FALSE; -ALTER TABLE teldrive.uploads ADD COLUMN "encrypted" BOOLEAN NOT NULL DEFAULT FALSE; --- +goose StatementEnd \ No newline at end of file diff --git a/pkg/database/migrations/20231208114142_alter.sql b/pkg/database/migrations/20231208114142_alter.sql new file mode 100644 index 0000000..f7da1b3 --- /dev/null +++ b/pkg/database/migrations/20231208114142_alter.sql @@ -0,0 +1,6 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE teldrive.files ADD COLUMN IF NOT EXISTS "encrypted" BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE teldrive.uploads ADD COLUMN IF NOT EXISTS "encrypted" BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE teldrive.uploads ADD COLUMN IF NOT EXISTS "salt" TEXT; +-- +goose StatementEnd \ No newline at end of file diff --git a/pkg/mapper/mapper.go b/pkg/mapper/mapper.go index 8149df8..0171575 100644 --- a/pkg/mapper/mapper.go +++ b/pkg/mapper/mapper.go @@ -27,7 +27,8 @@ func ToFileOutFull(file models.File) *schemas.FileOutFull { parts := []schemas.Part{} for _, part := range *file.Parts { parts = append(parts, schemas.Part{ - ID: part.ID, + ID: part.ID, + Salt: part.Salt, }) } @@ -47,6 +48,7 @@ func ToUploadOut(in *models.Upload) *schemas.UploadPartOut { PartNo: in.PartNo, Size: in.Size, Encrypted: in.Encrypted, + Salt: in.Salt, } return out } diff --git a/pkg/models/file.go b/pkg/models/file.go index 306e879..918961a 100644 --- a/pkg/models/file.go +++ b/pkg/models/file.go @@ -27,7 +27,8 @@ type File struct { type Parts []Part type Part struct { - ID int64 `json:"id"` + ID int64 `json:"id"` + Salt string `json:"salt,omitempty"` } func (a Parts) Value() (driver.Value, error) { diff --git a/pkg/models/upload.go b/pkg/models/upload.go index 417cf9a..2928c74 100644 --- a/pkg/models/upload.go +++ b/pkg/models/upload.go @@ -11,6 +11,7 @@ type Upload struct { PartNo int `gorm:"type:integer"` PartId int `gorm:"type:integer"` Encrypted bool `gorm:"default:false"` + Salt string `gorm:"type:text"` ChannelID int64 `gorm:"type:bigint"` Size int64 `gorm:"type:bigint"` CreatedAt time.Time `gorm:"default:timezone('utc'::text, now())"` diff --git a/pkg/schemas/file.go b/pkg/schemas/file.go index 95d1bcd..9805184 100644 --- a/pkg/schemas/file.go +++ b/pkg/schemas/file.go @@ -15,7 +15,8 @@ type SortingQuery struct { } type Part struct { - ID int64 `json:"id"` + ID int64 `json:"id"` + Salt string `json:"salt"` } type FileQuery struct { diff --git a/pkg/schemas/upload.go b/pkg/schemas/upload.go index 4c64050..0e320cc 100644 --- a/pkg/schemas/upload.go +++ b/pkg/schemas/upload.go @@ -14,6 +14,7 @@ type UploadPartOut struct { ChannelID int64 `json:"channelId"` Size int64 `json:"size"` Encrypted bool `json:"encrypted"` + Salt string `json:"salt"` } type UploadOut struct { diff --git a/pkg/services/common.go b/pkg/services/common.go index b362274..dca0fce 100644 --- a/pkg/services/common.go +++ b/pkg/services/common.go @@ -138,7 +138,7 @@ func getTGMessages(ctx context.Context, client *telegram.Client, parts []schemas return messages, nil } -func getParts(ctx context.Context, cipher *crypt.Cipher, client *telegram.Client, file *schemas.FileOutFull, userID string) ([]types.Part, error) { +func getParts(ctx context.Context, client *telegram.Client, file *schemas.FileOutFull, userID string) ([]types.Part, error) { parts := []types.Part{} @@ -156,17 +156,22 @@ func getParts(ctx context.Context, cipher *crypt.Cipher, client *telegram.Client return nil, err } - for _, message := range messages.Messages { + for i, message := range messages.Messages { item := message.(*tg.Message) media := item.Media.(*tg.MessageMediaDocument) document := media.Document.(*tg.Document) location := document.AsInputDocumentFileLocation() end := document.Size - 1 - if cipher != nil { - end, _ = cipher.DecryptedSize(document.Size) + if file.Encrypted { + end, _ = crypt.DecryptedSize(document.Size) end -= 1 } - parts = append(parts, types.Part{Location: location, Start: 0, End: end, Size: document.Size}) + parts = append(parts, types.Part{ + Location: location, + End: end, + Size: document.Size, + Salt: file.Parts[i].Salt, + }) } cache.GetCache().Set(key, &parts, 3600) return parts, nil @@ -189,37 +194,26 @@ func rangedParts(parts []types.Part, startByte, endByte int64) []types.Part { endInLastChunk := endByte % chunkSize if firstChunk == lastChunk { - validParts = append(validParts, types.Part{ - Location: parts[firstChunk].Location, - Start: startInFirstChunk, - End: endInLastChunk, - Size: parts[firstChunk].Size, - }) + part := parts[firstChunk] + part.Start = startInFirstChunk + part.End = endInLastChunk + validParts = append(validParts, part) } else { - validParts = append(validParts, types.Part{ - Location: parts[firstChunk].Location, - Start: startInFirstChunk, - End: parts[firstChunk].End, - Size: parts[firstChunk].Size, - }) - + part := parts[firstChunk] + part.Start = startInFirstChunk + validParts = append(validParts, part) // Add valid parts from any chunks in between. for i := firstChunk + 1; i < lastChunk; i++ { - validParts = append(validParts, types.Part{ - Location: parts[i].Location, - Start: 0, - End: parts[i].End, - Size: parts[i].Size, - }) + part := parts[i] + part.Start = 0 + validParts = append(validParts, part) } // Add valid parts from the last chunk. - validParts = append(validParts, types.Part{ - Location: parts[lastChunk].Location, - Start: 0, - End: endInLastChunk, - Size: parts[lastChunk].Size, - }) + endPart := parts[lastChunk] + endPart.Start = 0 + endPart.End = endInLastChunk + validParts = append(validParts, endPart) } return validParts diff --git a/pkg/services/file.go b/pkg/services/file.go index 0397b33..088170f 100644 --- a/pkg/services/file.go +++ b/pkg/services/file.go @@ -13,7 +13,6 @@ import ( cnf "github.com/divyam234/teldrive/config" "github.com/divyam234/teldrive/internal/cache" - "github.com/divyam234/teldrive/internal/crypt" "github.com/divyam234/teldrive/internal/http_range" "github.com/divyam234/teldrive/internal/md5" "github.com/divyam234/teldrive/internal/reader" @@ -85,7 +84,8 @@ func (fs *FileService) CreateFile(c *gin.Context) (*schemas.FileOut, *types.AppE parts := models.Parts{} for _, part := range fileIn.Parts { parts = append(parts, models.Part{ - ID: part.ID, + ID: part.ID, + Salt: part.Salt, }) } @@ -554,14 +554,9 @@ func (fs *FileService) GetFileStream(c *gin.Context) { var ( token, channelUser string - cipher *crypt.Cipher lr io.ReadCloser ) - if file.Encrypted { - cipher, _ = crypt.NewCipher(config.EncryptionKey, config.EncryptionSalt) - } - if config.LazyStreamBots { tgc.Workers.Set(tokens, file.ChannelID) token = tgc.Workers.Next(file.ChannelID) @@ -569,13 +564,13 @@ func (fs *FileService) GetFileStream(c *gin.Context) { channelUser = strings.Split(token, ":")[0] if r.Method != "HEAD" { tgc.RunWithAuth(c, client, token, func(ctx context.Context) error { - parts, err := getParts(c, cipher, client, file, channelUser) + parts, err := getParts(c, client, file, channelUser) if err != nil { return err } parts = rangedParts(parts, start, end) if file.Encrypted { - lr, _ = reader.NewDecryptedReader(c, client, parts, cipher, contentLength) + lr, _ = reader.NewDecryptedReader(c, client, parts, contentLength) } else { lr, _ = reader.NewLinearReader(c, client, parts, contentLength) } @@ -613,7 +608,7 @@ func (fs *FileService) GetFileStream(c *gin.Context) { } if r.Method != "HEAD" { - parts, err := getParts(c, cipher, client.Tg, file, channelUser) + parts, err := getParts(c, client.Tg, file, channelUser) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -622,7 +617,7 @@ func (fs *FileService) GetFileStream(c *gin.Context) { parts = rangedParts(parts, start, end) if file.Encrypted { - lr, _ = reader.NewDecryptedReader(c, client.Tg, parts, cipher, contentLength) + lr, _ = reader.NewDecryptedReader(c, client.Tg, parts, contentLength) } else { lr, _ = reader.NewLinearReader(c, client.Tg, parts, contentLength) } diff --git a/pkg/services/upload.go b/pkg/services/upload.go index 93167b5..38c9c8c 100644 --- a/pkg/services/upload.go +++ b/pkg/services/upload.go @@ -2,6 +2,9 @@ package services import ( "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" "errors" "net/http" "strconv" @@ -25,6 +28,8 @@ import ( "gorm.io/gorm" ) +const saltLength = 32 + type UploadService struct { Db *gorm.DB } @@ -33,6 +38,20 @@ func NewUploadService(db *gorm.DB) *UploadService { return &UploadService{Db: db} } +func generateRandomSalt() (string, error) { + randomBytes := make([]byte, saltLength) + _, err := rand.Read(randomBytes) + if err != nil { + return "", err + } + + hasher := sha256.New() + hasher.Write(randomBytes) + hashedSalt := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) + + return hashedSalt, nil +} + func (us *UploadService) GetUploadFileById(c *gin.Context) (*schemas.UploadOut, *types.AppError) { uploadId := c.Param("id") parts := []schemas.UploadPartOut{} @@ -147,9 +166,14 @@ func (us *UploadService) UploadFile(c *gin.Context) (*schemas.UploadPartOut, *ty config := cnf.GetConfig() + var salt string + if uploadQuery.Encrypted { - cipher, _ := crypt.NewCipher(config.EncryptionKey, config.EncryptionSalt) - fileSize = cipher.EncryptedSize(fileSize) + + //gen random Salt + salt, _ = generateRandomSalt() + cipher, _ := crypt.NewCipher(config.EncryptionKey, salt) + fileSize = crypt.EncryptedSize(fileSize) fileStream, _ = cipher.EncryptData(fileStream) } @@ -202,6 +226,7 @@ func (us *UploadService) UploadFile(c *gin.Context) (*schemas.UploadPartOut, *ty PartNo: uploadQuery.PartNo, UserId: userId, Encrypted: uploadQuery.Encrypted, + Salt: salt, } if err := us.Db.Create(partUpload).Error; err != nil { diff --git a/pkg/types/types.go b/pkg/types/types.go index f9bd941..1f168b1 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -16,6 +16,7 @@ type Part struct { Start int64 End int64 Size int64 + Salt string } type JWTClaims struct {