mirror of
https://github.com/X-Cli/large-file-decrypt.git
synced 2024-12-03 16:12:11 +00:00
initial
This commit is contained in:
parent
fafbf8460f
commit
b1f60357fa
8 changed files with 427 additions and 1 deletions
|
@ -1,2 +1,2 @@
|
|||
# large-file-decrypt
|
||||
# Large File Decrypt
|
||||
Example code for hopefully secure large file decryption
|
||||
|
|
100
decrypt/decrypt.go
Normal file
100
decrypt/decrypt.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package decrypt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/X-Cli/large-file-decrypt/utils"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func DecryptFile(inputPath, outputPath string, pubKey, privKey *[32]byte) error {
|
||||
return decryptFile(inputPath, outputPath, pubKey, privKey).ErrorOrNil()
|
||||
}
|
||||
|
||||
func decryptFile(inputPath, outputPath string, pubKey, privKey *[32]byte) (errStack *multierror.Error) {
|
||||
inputTempDir := filepath.Dir(inputPath)
|
||||
privateEncryptedFile, errs := utils.CreatePrivateCopyOf(inputPath, inputTempDir)
|
||||
if errs.ErrorOrNil() != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to create the private copy of the encrypted file: %w", errs))
|
||||
return
|
||||
}
|
||||
privateEncryptedFileClosed := false
|
||||
defer func() {
|
||||
if !privateEncryptedFileClosed {
|
||||
if err := privateEncryptedFile.Close(); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to close private copy of encrypted file: %w", err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
encryptedData, err := utils.GetDataFromFile(privateEncryptedFile)
|
||||
if err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to get encrypted data: %w", err))
|
||||
return
|
||||
}
|
||||
privateEncryptedDataFreed := false
|
||||
defer func() {
|
||||
if !privateEncryptedDataFreed {
|
||||
if err := syscall.Munmap(encryptedData); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to munmap encrypted data: %w", err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
decryptedSize := len(encryptedData) - box.AnonymousOverhead
|
||||
outputTempDir := filepath.Dir(outputPath)
|
||||
privateDecryptedFile, err := utils.CreatePrivateFile(outputTempDir, int64(decryptedSize))
|
||||
if err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to create private decrypted file: %w", err))
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := privateDecryptedFile.Close(); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to close private decrypted file: %w", err))
|
||||
}
|
||||
}()
|
||||
|
||||
decryptedData, err := utils.GetDataFromFile(privateDecryptedFile)
|
||||
if err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to mmap decrypted file: %w", err))
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := syscall.Munmap(decryptedData); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to munmap decrypted data: %w", err))
|
||||
}
|
||||
}()
|
||||
|
||||
if _, ok := box.OpenAnonymous(decryptedData[:0], encryptedData, pubKey, privKey); !ok {
|
||||
errStack = multierror.Append(errStack, errors.New("failed to decrypt file"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := unix.Msync(decryptedData, unix.MS_SYNC); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to flush changes to disk: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Since in the worst case, we can only copy the private decrypted file to the public file, we release the private encrypted file now, so that the algorithm only uses a maximum of 3 times the size of the encrypted file
|
||||
privateEncryptedDataFreed = true
|
||||
if err := syscall.Munmap(encryptedData); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to munmap encrypted data: %w", err))
|
||||
return
|
||||
}
|
||||
privateEncryptedFileClosed = true
|
||||
if err := privateEncryptedFile.Close(); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to close private copy of encrypted file: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := utils.PublishFile(privateDecryptedFile, outputPath).ErrorOrNil(); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to publish decrypted file: %w", err))
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
45
decrypt/decrypt_test.go
Normal file
45
decrypt/decrypt_test.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package decrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
func TestDecrypt1(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
pubKey, privKey, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate keys: %v", err)
|
||||
}
|
||||
|
||||
expectedContent := []byte("Geronimo!")
|
||||
encryptedContent, err := box.SealAnonymous(nil, expectedContent, pubKey, rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encrypt message: %v", err)
|
||||
}
|
||||
|
||||
encryptedFilePath := path.Join(tmpdir, "encryptedFile")
|
||||
if err := ioutil.WriteFile(encryptedFilePath, encryptedContent, 0o600); err != nil {
|
||||
t.Fatalf("failed to initialize encrypted file: %v", err)
|
||||
}
|
||||
|
||||
decryptedFilePath := path.Join(tmpdir, "decryptedFile")
|
||||
if err := DecryptFile(encryptedFilePath, decryptedFilePath, pubKey, privKey); err != nil {
|
||||
t.Fatalf("failed to decrypt file: %v", err)
|
||||
}
|
||||
|
||||
decryptedContent, err := ioutil.ReadFile(decryptedFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read decrypted file: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(expectedContent, decryptedContent) {
|
||||
t.Fatalf("mismatched content: expected %v, found %v", expectedContent, decryptedContent)
|
||||
}
|
||||
}
|
102
encrypt/encrypt.go
Normal file
102
encrypt/encrypt.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package encrypt
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/X-Cli/large-file-decrypt/utils"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func EncryptFile(inputPath, outputPath string, pubKey *[32]byte) error {
|
||||
return encryptFile(inputPath, outputPath, pubKey, rand.Reader).ErrorOrNil()
|
||||
}
|
||||
|
||||
func encryptFile(inputPath, outputPath string, pubKey *[32]byte, cryptoReader io.Reader) (errStack *multierror.Error) {
|
||||
inputTempDir := filepath.Dir(inputPath)
|
||||
|
||||
privateClearFile, errs := utils.CreatePrivateCopyOf(inputPath, inputTempDir)
|
||||
if errs.ErrorOrNil() != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to acquire a private copy of %q: %w", inputPath, errs))
|
||||
return
|
||||
}
|
||||
privateClearFileClosed := false
|
||||
defer func() {
|
||||
if !privateClearFileClosed {
|
||||
if err := privateClearFile.Close(); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to close private copy of %q: %w", inputPath, err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
clearData, err := utils.GetDataFromFile(privateClearFile)
|
||||
if err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to get data from private copy: %w", err))
|
||||
return
|
||||
}
|
||||
clearDataFreed := false
|
||||
defer func() {
|
||||
if !clearDataFreed {
|
||||
if err := syscall.Munmap(clearData); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to munmap clear data: %w", err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
outputTempDir := filepath.Dir(outputPath)
|
||||
encryptedSize := len(clearData) + box.AnonymousOverhead
|
||||
privateEncryptedFile, err := utils.CreatePrivateFile(outputTempDir, int64(encryptedSize))
|
||||
if err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to create private encrypted file: %w", err))
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := privateEncryptedFile.Close(); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to close private encrypted file: %w", err))
|
||||
}
|
||||
}()
|
||||
|
||||
encryptedData, err := utils.GetDataFromFile(privateEncryptedFile)
|
||||
if err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to get data from encrypted file: %w", err))
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := syscall.Munmap(encryptedData); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to release encrypted data: %w", err))
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := box.SealAnonymous(encryptedData[:0], clearData, pubKey, cryptoReader); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to seal data: %w", err))
|
||||
return
|
||||
}
|
||||
if err := unix.Msync(encryptedData, unix.MS_SYNC); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to flush encrypted data on disk: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Releasing private clear text resources since they are no longer needed and they may occupy resources if file clone was not possible
|
||||
clearDataFreed = true
|
||||
if err := syscall.Munmap(clearData); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to get data from private copy: %w", err))
|
||||
return
|
||||
}
|
||||
privateClearFileClosed = true
|
||||
if err := privateClearFile.Close(); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to close private copy of %q: %w", inputPath, err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := utils.PublishFile(privateEncryptedFile, outputPath); err.ErrorOrNil() != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to publish encrypted file: %w", err))
|
||||
return
|
||||
}
|
||||
return
|
||||
|
||||
}
|
44
encrypt/encrypt_test.go
Normal file
44
encrypt/encrypt_test.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package encrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
func TestEncrypt(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
var constantRandom [512]byte
|
||||
randReader := bytes.NewBuffer(constantRandom[:])
|
||||
|
||||
clearFile := path.Join(tmpdir, "clear")
|
||||
encryptedFile := path.Join(tmpdir, "encrypted")
|
||||
|
||||
clearText := []byte("Geronimo!")
|
||||
if err := ioutil.WriteFile(clearFile, clearText, 0o600); err != nil {
|
||||
t.Fatalf("failed to write cleartext file: %v", err)
|
||||
}
|
||||
|
||||
pubKey, _, err := box.GenerateKey(randReader)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate keys: %v", err)
|
||||
}
|
||||
|
||||
if err := encryptFile(clearFile, encryptedFile, pubKey, randReader); err != nil {
|
||||
t.Fatalf("failed to encrypt file: %v", err)
|
||||
}
|
||||
|
||||
cipherText, err := ioutil.ReadFile(encryptedFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read encrypted file: %v", err)
|
||||
}
|
||||
|
||||
expectedCiphertext := []byte{47, 229, 125, 163, 71, 205, 98, 67, 21, 40, 218, 172, 95, 187, 41, 7, 48, 255, 246, 132, 175, 196, 207, 194, 237, 144, 153, 95, 88, 203, 59, 116, 83, 218, 223, 164, 117, 110, 80, 197, 138, 128, 169, 16, 149, 74, 20, 245, 10, 169, 53, 61, 119, 218, 240, 50, 92}
|
||||
if !bytes.Equal(expectedCiphertext, cipherText) {
|
||||
t.Fatalf("unexpected ciphertext: expected %v; found %v", expectedCiphertext, cipherText)
|
||||
}
|
||||
}
|
9
go.mod
Normal file
9
go.mod
Normal file
|
@ -0,0 +1,9 @@
|
|||
module github.com/X-Cli/large-file-decrypt
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
|
||||
)
|
15
go.sum
Normal file
15
go.sum
Normal file
|
@ -0,0 +1,15 @@
|
|||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
111
utils/utils.go
Normal file
111
utils/utils.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func CreatePrivateCopyOf(inputPath, tempDir string) (privateFile *os.File, errStack *multierror.Error) {
|
||||
inputFile, err := os.Open(inputPath)
|
||||
if err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to open file %q: %w", inputPath, err))
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := inputFile.Close(); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to close file %q: %w", inputPath, err))
|
||||
}
|
||||
}()
|
||||
|
||||
privateFileFd, err := unix.Open(tempDir, unix.O_RDWR|unix.O_TMPFILE, 0o600)
|
||||
if err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to open private file: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
privateFile = os.NewFile(uintptr(privateFileFd), "")
|
||||
defer func() {
|
||||
if errStack.ErrorOrNil() != nil {
|
||||
if err := privateFile.Close(); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to close private copy: %w", err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := unix.IoctlFileClone(int(privateFile.Fd()), int(inputFile.Fd())); err != nil {
|
||||
if err != syscall.EOPNOTSUPP && err != syscall.EINVAL {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to clone file: %w", err))
|
||||
return
|
||||
}
|
||||
if _, err := io.Copy(privateFile, inputFile); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to copy file: %w", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetDataFromFile(privateEncryptedFile *os.File) (data []byte, err error) {
|
||||
fs, err := privateEncryptedFile.Stat()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to stat(2) private copy of encrypted file: %w", err)
|
||||
}
|
||||
data, err = syscall.Mmap(int(privateEncryptedFile.Fd()), 0, int(fs.Size()), syscall.PROT_WRITE|syscall.PROT_READ, syscall.MAP_SHARED)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to mmap(2) private copy of encrypted file: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func CreatePrivateFile(tempDir string, size int64) (*os.File, error) {
|
||||
privateFileFd, err := unix.Open(tempDir, unix.O_RDWR|unix.O_TMPFILE, 0o600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create private file: %w", err)
|
||||
}
|
||||
privateFile := os.NewFile(uintptr(privateFileFd), "")
|
||||
|
||||
if err := syscall.Fallocate(int(privateFile.Fd()), 0, 0, size); err != nil {
|
||||
var errStack error = err
|
||||
if err := privateFile.Close(); err != nil {
|
||||
errStack = multierror.Append(errStack, err)
|
||||
}
|
||||
return nil, errStack
|
||||
}
|
||||
return privateFile, nil
|
||||
}
|
||||
|
||||
func PublishFile(privateDecryptedFile *os.File, outputPath string) (errStack *multierror.Error) {
|
||||
if err := unix.Linkat(int(privateDecryptedFile.Fd()), "", 0, outputPath, unix.AT_EMPTY_PATH); err == nil {
|
||||
return
|
||||
} else if err != syscall.ENOENT {
|
||||
// ENOENT is returned if CAP_DAC_READ_SEARCH is not effective
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to call linkat: %w", err))
|
||||
return
|
||||
}
|
||||
outputFile, err := os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE, 0o600)
|
||||
if err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to create output file: %w", err))
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := outputFile.Close(); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to close file %q: %w", outputPath, err))
|
||||
}
|
||||
}()
|
||||
if err := unix.IoctlFileClone(int(outputFile.Fd()), int(privateDecryptedFile.Fd())); err == nil {
|
||||
return
|
||||
} else if err != syscall.EOPNOTSUPP && err != syscall.EINVAL {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to clone file %w", err))
|
||||
return
|
||||
}
|
||||
if _, err := io.Copy(outputFile, privateDecryptedFile); err != nil {
|
||||
errStack = multierror.Append(errStack, fmt.Errorf("failed to copy into public decrypted file: %w", err))
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
Loading…
Reference in a new issue