diff --git a/cmd/stbak/cmd/archive.go b/cmd/stbak/cmd/archive.go index 84092e2..7b9f92b 100644 --- a/cmd/stbak/cmd/archive.go +++ b/cmd/stbak/cmd/archive.go @@ -30,6 +30,7 @@ import ( "github.com/pojntfx/stfs/internal/controllers" "github.com/pojntfx/stfs/internal/counters" "github.com/pojntfx/stfs/internal/formatting" + "github.com/pojntfx/stfs/internal/keys" "github.com/pojntfx/stfs/internal/noop" "github.com/pojntfx/stfs/internal/pax" "github.com/pojntfx/stfs/internal/persisters" @@ -130,7 +131,7 @@ var archiveCmd = &cobra.Command{ return err } - identity, err := parseSignerIdentity(viper.GetString(signatureFlag), privkey, viper.GetString(passwordFlag)) + identity, err := keys.ParseSignerIdentity(viper.GetString(signatureFlag), privkey, viper.GetString(passwordFlag)) if err != nil { return err } @@ -625,23 +626,6 @@ func encrypt( } } -func parseSignerIdentity( - signatureFormat string, - privkey []byte, - password string, -) (interface{}, error) { - switch signatureFormat { - case signatureFormatMinisignKey: - return minisign.DecryptKey(password, privkey) - case signatureFormatPGPKey: - return parseIdentity(signatureFormat, privkey, password) - case noneKey: - return privkey, nil - default: - return nil, errUnsupportedSignatureFormat - } -} - func sign( src io.Reader, isRegular bool, diff --git a/cmd/stbak/cmd/delete.go b/cmd/stbak/cmd/delete.go index fbd2ffa..5e33ebb 100644 --- a/cmd/stbak/cmd/delete.go +++ b/cmd/stbak/cmd/delete.go @@ -11,6 +11,7 @@ import ( "github.com/pojntfx/stfs/internal/counters" models "github.com/pojntfx/stfs/internal/db/sqlite/models/metadata" "github.com/pojntfx/stfs/internal/formatting" + "github.com/pojntfx/stfs/internal/keys" "github.com/pojntfx/stfs/internal/pax" "github.com/pojntfx/stfs/internal/persisters" "github.com/spf13/cobra" @@ -61,7 +62,7 @@ var deleteCmd = &cobra.Command{ return err } - identity, err := parseSignerIdentity(viper.GetString(signatureFlag), privkey, viper.GetString(passwordFlag)) + identity, err := keys.ParseSignerIdentity(viper.GetString(signatureFlag), privkey, viper.GetString(passwordFlag)) if err != nil { return err } diff --git a/cmd/stbak/cmd/move.go b/cmd/stbak/cmd/move.go index 6b87e9b..770b7f3 100644 --- a/cmd/stbak/cmd/move.go +++ b/cmd/stbak/cmd/move.go @@ -8,6 +8,7 @@ import ( "github.com/pojntfx/stfs/internal/converters" models "github.com/pojntfx/stfs/internal/db/sqlite/models/metadata" "github.com/pojntfx/stfs/internal/formatting" + "github.com/pojntfx/stfs/internal/keys" "github.com/pojntfx/stfs/internal/pax" "github.com/pojntfx/stfs/internal/persisters" "github.com/spf13/cobra" @@ -54,7 +55,7 @@ var moveCmd = &cobra.Command{ return err } - identity, err := parseSignerIdentity(viper.GetString(signatureFlag), privkey, viper.GetString(passwordFlag)) + identity, err := keys.ParseSignerIdentity(viper.GetString(signatureFlag), privkey, viper.GetString(passwordFlag)) if err != nil { return err } diff --git a/cmd/stbak/cmd/recovery_fetch.go b/cmd/stbak/cmd/recovery_fetch.go index 58dcd17..554fff9 100644 --- a/cmd/stbak/cmd/recovery_fetch.go +++ b/cmd/stbak/cmd/recovery_fetch.go @@ -1,32 +1,11 @@ package cmd import ( - "archive/tar" - "bufio" - "bytes" - "compress/gzip" - "context" - "encoding/base64" - "encoding/json" "errors" - "io" - "os" - "path/filepath" - "aead.dev/minisign" - "filippo.io/age" - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/packet" - "github.com/andybalholm/brotli" - "github.com/cosnicolaou/pbzip2" - "github.com/dsnet/compress/bzip2" - "github.com/klauspost/compress/zstd" - "github.com/klauspost/pgzip" - "github.com/pierrec/lz4/v4" - "github.com/pojntfx/stfs/internal/controllers" - "github.com/pojntfx/stfs/internal/formatting" - "github.com/pojntfx/stfs/internal/pax" - "github.com/pojntfx/stfs/internal/tape" + "github.com/pojntfx/stfs/internal/keys" + "github.com/pojntfx/stfs/pkg/config" + "github.com/pojntfx/stfs/pkg/recovery" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/volatiletech/sqlboiler/v4/boil" @@ -77,7 +56,7 @@ var recoveryFetchCmd = &cobra.Command{ return err } - recipient, err := parseSignerRecipient(viper.GetString(signatureFlag), pubkey) + recipient, err := keys.ParseSignerRecipient(viper.GetString(signatureFlag), pubkey) if err != nil { return err } @@ -87,598 +66,38 @@ var recoveryFetchCmd = &cobra.Command{ return err } - identity, err := parseIdentity(viper.GetString(encryptionFlag), privkey, viper.GetString(passwordFlag)) + identity, err := keys.ParseIdentity(viper.GetString(encryptionFlag), privkey, viper.GetString(passwordFlag)) if err != nil { return err } - return restoreFromRecordAndBlock( - viper.GetString(driveFlag), + return recovery.Fetch( + config.StateConfig{ + Drive: viper.GetString(driveFlag), + Metadata: viper.GetString(metadataFlag), + }, + config.PipeConfig{ + Compression: viper.GetString(compressionFlag), + Encryption: viper.GetString(encryptionFlag), + Signature: viper.GetString(signatureFlag), + }, + config.CryptoConfig{ + Recipient: recipient, + Identity: identity, + Password: viper.GetString(passwordFlag), + }, + viper.GetInt(recordSizeFlag), viper.GetInt(recordFlag), viper.GetInt(blockFlag), viper.GetString(toFlag), viper.GetBool(previewFlag), + true, - viper.GetString(compressionFlag), - viper.GetString(encryptionFlag), - identity, - viper.GetString(signatureFlag), - recipient, ) }, } -func restoreFromRecordAndBlock( - drive string, - recordSize int, - record int, - block int, - dst string, - preview bool, - showHeader bool, - compressionFormat string, - encryptionFormat string, - identity interface{}, - signatureFormat string, - recipient interface{}, -) error { - f, isRegular, err := tape.OpenTapeReadOnly(drive) - if err != nil { - return err - } - defer f.Close() - - var tr *tar.Reader - if isRegular { - // Seek to record and block - if _, err := f.Seek(int64((recordSize*controllers.BlockSize*record)+block*controllers.BlockSize), io.SeekStart); err != nil { - return err - } - - tr = tar.NewReader(f) - } else { - // Seek to record - if err := controllers.SeekToRecordOnTape(f, int32(record)); err != nil { - return err - } - - // Seek to block - br := bufio.NewReaderSize(f, controllers.BlockSize*recordSize) - if _, err := br.Read(make([]byte, block*controllers.BlockSize)); err != nil { - return err - } - - tr = tar.NewReader(br) - } - - hdr, err := tr.Next() - if err != nil { - return err - } - - if err := decryptHeader(hdr, encryptionFormat, identity); err != nil { - return err - } - - if err := verifyHeader(hdr, isRegular, signatureFormat, recipient); err != nil { - return err - } - - if showHeader { - if err := formatting.PrintCSV(formatting.TARHeaderCSV); err != nil { - return err - } - - if err := formatting.PrintCSV(formatting.GetTARHeaderAsCSV(int64(record), int64(block), hdr)); err != nil { - return err - } - } - - if !preview { - if dst == "" { - dst = filepath.Base(hdr.Name) - } - - if hdr.Typeflag == tar.TypeDir { - return os.MkdirAll(dst, hdr.FileInfo().Mode()) - } - - dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE, hdr.FileInfo().Mode()) - if err != nil { - return err - } - - if err := dstFile.Truncate(0); err != nil { - return err - } - - // Don't decompress non-regular files - if !hdr.FileInfo().Mode().IsRegular() { - if _, err := io.Copy(dstFile, tr); err != nil { - return err - } - - return nil - } - - decryptor, err := decrypt(tr, encryptionFormat, identity) - if err != nil { - return err - } - - decompressor, err := decompress(decryptor, compressionFormat) - if err != nil { - return err - } - - signature := "" - if hdr.PAXRecords != nil { - if s, ok := hdr.PAXRecords[pax.STFSRecordSignature]; ok { - signature = s - } - } - - verifier, verify, err := verify(decompressor, isRegular, signatureFormat, recipient, signature) - if err != nil { - return err - } - - if _, err := io.Copy(dstFile, verifier); err != nil { - return err - } - - if err := verify(); err != nil { - return err - } - - if err := decryptor.Close(); err != nil { - return err - } - - if err := decompressor.Close(); err != nil { - return err - } - - if err := dstFile.Close(); err != nil { - return err - } - } - - return nil -} - -func decompress( - src io.Reader, - compressionFormat string, -) (io.ReadCloser, error) { - switch compressionFormat { - case compressionFormatGZipKey: - fallthrough - case compressionFormatParallelGZipKey: - if compressionFormat == compressionFormatGZipKey { - return gzip.NewReader(src) - } - - return pgzip.NewReader(src) - case compressionFormatLZ4Key: - lz := lz4.NewReader(src) - if err := lz.Apply(lz4.ConcurrencyOption(-1)); err != nil { - return nil, err - } - - return io.NopCloser(lz), nil - case compressionFormatZStandardKey: - zz, err := zstd.NewReader(src) - if err != nil { - return nil, err - } - - return io.NopCloser(zz), nil - case compressionFormatBrotliKey: - br := brotli.NewReader(src) - - return io.NopCloser(br), nil - case compressionFormatBzip2Key: - return bzip2.NewReader(src, nil) - case compressionFormatBzip2ParallelKey: - bz := pbzip2.NewReader(context.Background(), src) - - return io.NopCloser(bz), nil - case noneKey: - return io.NopCloser(src), nil - default: - return nil, errUnsupportedCompressionFormat - } -} - -func decryptHeader( - hdr *tar.Header, - encryptionFormat string, - identity interface{}, -) error { - if encryptionFormat == noneKey { - return nil - } - - if hdr.PAXRecords == nil { - return errEmbeddedHeaderMissing - } - - encryptedEmbeddedHeader, ok := hdr.PAXRecords[pax.STFSRecordEmbeddedHeader] - if !ok { - return errEmbeddedHeaderMissing - } - - embeddedHeader, err := decryptString(encryptedEmbeddedHeader, encryptionFormat, identity) - if err != nil { - return err - } - - var newHdr tar.Header - if err := json.Unmarshal([]byte(embeddedHeader), &newHdr); err != nil { - return err - } - - *hdr = newHdr - - return nil -} - -func verifyHeader( - hdr *tar.Header, - isRegular bool, - signatureFormat string, - recipient interface{}, -) error { - if signatureFormat == noneKey { - return nil - } - - if hdr.PAXRecords == nil { - return errEmbeddedHeaderMissing - } - - embeddedHeader, ok := hdr.PAXRecords[pax.STFSRecordEmbeddedHeader] - if !ok { - return errEmbeddedHeaderMissing - } - - signature, ok := hdr.PAXRecords[pax.STFSRecordSignature] - if !ok { - return errSignatureMissing - } - - if err := verifyString(embeddedHeader, isRegular, signatureFormat, recipient, signature); err != nil { - return err - } - - var newHdr tar.Header - if err := json.Unmarshal([]byte(embeddedHeader), &newHdr); err != nil { - return err - } - - *hdr = newHdr - - return nil -} - -func parseIdentity( - encryptionFormat string, - privkey []byte, - password string, -) (interface{}, error) { - switch encryptionFormat { - case encryptionFormatAgeKey: - if password != "" { - passwordIdentity, err := age.NewScryptIdentity(password) - if err != nil { - return nil, err - } - - r, err := age.Decrypt(bytes.NewBuffer(privkey), passwordIdentity) - if err != nil { - return nil, err - } - - out := &bytes.Buffer{} - if _, err := io.Copy(out, r); err != nil { - return nil, err - } - - privkey = out.Bytes() - } - - return age.ParseX25519Identity(string(privkey)) - case encryptionFormatPGPKey: - identities, err := openpgp.ReadKeyRing(bytes.NewBuffer(privkey)) - if err != nil { - return nil, err - } - - if password != "" { - for _, identity := range identities { - if identity.PrivateKey == nil { - return nil, errIdentityUnparsable - } - - if err := identity.PrivateKey.Decrypt([]byte(password)); err != nil { - return nil, err - } - - for _, subkey := range identity.Subkeys { - if err := subkey.PrivateKey.Decrypt([]byte(password)); err != nil { - return nil, err - } - } - } - } - - return identities, nil - case noneKey: - return privkey, nil - default: - return nil, errUnsupportedEncryptionFormat - } -} - -func decryptString( - src string, - encryptionFormat string, - identity interface{}, -) (string, error) { - switch encryptionFormat { - case encryptionFormatAgeKey: - identity, ok := identity.(*age.X25519Identity) - if !ok { - return "", errIdentityUnparsable - } - - decoded, err := base64.StdEncoding.DecodeString(src) - if err != nil { - return "", err - } - - r, err := age.Decrypt(bytes.NewBufferString(string(decoded)), identity) - if err != nil { - return "", err - } - - out := &bytes.Buffer{} - if _, err := io.Copy(out, r); err != nil { - return "", err - } - - return out.String(), nil - case encryptionFormatPGPKey: - identity, ok := identity.(openpgp.EntityList) - if !ok { - return "", errIdentityUnparsable - } - - decoded, err := base64.StdEncoding.DecodeString(src) - if err != nil { - return "", err - } - - r, err := openpgp.ReadMessage(bytes.NewBufferString(string(decoded)), identity, nil, nil) - if err != nil { - return "", err - } - - out := &bytes.Buffer{} - if _, err := io.Copy(out, r.UnverifiedBody); err != nil { - return "", err - } - - return out.String(), nil - case noneKey: - return src, nil - default: - return "", errUnsupportedEncryptionFormat - } -} - -func decrypt( - src io.Reader, - encryptionFormat string, - identity interface{}, -) (io.ReadCloser, error) { - switch encryptionFormat { - case encryptionFormatAgeKey: - identity, ok := identity.(*age.X25519Identity) - if !ok { - return nil, errIdentityUnparsable - } - - r, err := age.Decrypt(src, identity) - if err != nil { - return nil, err - } - - return io.NopCloser(r), nil - case encryptionFormatPGPKey: - identity, ok := identity.(openpgp.EntityList) - if !ok { - return nil, errIdentityUnparsable - } - - r, err := openpgp.ReadMessage(src, identity, nil, nil) - if err != nil { - return nil, err - } - - return io.NopCloser(r.UnverifiedBody), nil - case noneKey: - return io.NopCloser(src), nil - default: - return nil, errUnsupportedEncryptionFormat - } -} - -func parseSignerRecipient( - signatureFormat string, - pubkey []byte, -) (interface{}, error) { - switch signatureFormat { - case signatureFormatMinisignKey: - var recipient minisign.PublicKey - if err := recipient.UnmarshalText(pubkey); err != nil { - return nil, err - } - - return recipient, nil - case signatureFormatPGPKey: - return parseRecipient(signatureFormat, pubkey) - case noneKey: - return pubkey, nil - default: - return nil, errUnsupportedSignatureFormat - } -} - -func verify( - src io.Reader, - isRegular bool, - signatureFormat string, - recipient interface{}, - signature string, -) (io.Reader, func() error, error) { - switch signatureFormat { - case signatureFormatMinisignKey: - if !isRegular { - return nil, nil, errSignatureFormatOnlyRegularSupport - } - - recipient, ok := recipient.(minisign.PublicKey) - if !ok { - return nil, nil, errRecipientUnparsable - } - - verifier := minisign.NewReader(src) - - return verifier, func() error { - decodedSignature, err := base64.StdEncoding.DecodeString(signature) - if err != nil { - return err - } - - if verifier.Verify(recipient, decodedSignature) { - return nil - } - - return errInvalidSignature - }, nil - case signatureFormatPGPKey: - recipients, ok := recipient.(openpgp.EntityList) - if !ok { - return nil, nil, errIdentityUnparsable - } - - if len(recipients) < 1 { - return nil, nil, errIdentityUnparsable - } - - decodedSignature, err := base64.StdEncoding.DecodeString(signature) - if err != nil { - return nil, nil, err - } - - reader := packet.NewReader(bytes.NewBuffer(decodedSignature)) - pkt, err := reader.Next() - if err != nil { - return nil, nil, err - } - - sig, ok := pkt.(*packet.Signature) - if !ok { - return nil, nil, errInvalidSignature - } - - hash := sig.Hash.New() - - tee := io.TeeReader(src, hash) - - return tee, func() error { - return recipients[0].PrimaryKey.VerifySignature(hash, sig) - }, nil - case noneKey: - return io.NopCloser(src), func() error { - return nil - }, nil - default: - return nil, nil, errUnsupportedSignatureFormat - } -} - -func verifyString( - src string, - isRegular bool, - signatureFormat string, - recipient interface{}, - signature string, -) error { - switch signatureFormat { - case signatureFormatMinisignKey: - if !isRegular { - return errSignatureFormatOnlyRegularSupport - } - - recipient, ok := recipient.(minisign.PublicKey) - if !ok { - return errRecipientUnparsable - } - - decodedSignature, err := base64.StdEncoding.DecodeString(signature) - if err != nil { - return err - } - - if minisign.Verify(recipient, []byte(src), decodedSignature) { - return nil - } - - return errInvalidSignature - case signatureFormatPGPKey: - recipients, ok := recipient.(openpgp.EntityList) - if !ok { - return nil - } - - if len(recipients) < 1 { - return nil - } - - decodedSignature, err := base64.StdEncoding.DecodeString(signature) - if err != nil { - return nil - } - - reader := packet.NewReader(bytes.NewBuffer(decodedSignature)) - pkt, err := reader.Next() - if err != nil { - return nil - } - - sig, ok := pkt.(*packet.Signature) - if !ok { - return nil - } - - hash := sig.Hash.New() - - if _, err := io.Copy(hash, bytes.NewBufferString(src)); err != nil { - return err - } - - return recipients[0].PrimaryKey.VerifySignature(hash, sig) - case noneKey: - return nil - default: - return errUnsupportedSignatureFormat - } -} - func init() { recoveryFetchCmd.PersistentFlags().IntP(recordSizeFlag, "z", 20, "Amount of 512-bit blocks per record") recoveryFetchCmd.PersistentFlags().IntP(recordFlag, "k", 0, "Record to seek too") diff --git a/cmd/stbak/cmd/recovery_index.go b/cmd/stbak/cmd/recovery_index.go index be7babe..8d22e39 100644 --- a/cmd/stbak/cmd/recovery_index.go +++ b/cmd/stbak/cmd/recovery_index.go @@ -3,6 +3,9 @@ package cmd import ( "archive/tar" + "github.com/pojntfx/stfs/internal/encryption" + "github.com/pojntfx/stfs/internal/keys" + "github.com/pojntfx/stfs/internal/signature" "github.com/pojntfx/stfs/pkg/config" "github.com/pojntfx/stfs/pkg/recovery" "github.com/spf13/cobra" @@ -38,7 +41,7 @@ var recoveryIndexCmd = &cobra.Command{ return err } - recipient, err := parseSignerRecipient(viper.GetString(signatureFlag), pubkey) + recipient, err := keys.ParseSignerRecipient(viper.GetString(signatureFlag), pubkey) if err != nil { return err } @@ -48,7 +51,7 @@ var recoveryIndexCmd = &cobra.Command{ return err } - identity, err := parseIdentity(viper.GetString(encryptionFlag), privkey, viper.GetString(passwordFlag)) + identity, err := keys.ParseIdentity(viper.GetString(encryptionFlag), privkey, viper.GetString(passwordFlag)) if err != nil { return err } @@ -76,10 +79,10 @@ var recoveryIndexCmd = &cobra.Command{ 0, func(hdr *tar.Header, i int) error { - return decryptHeader(hdr, viper.GetString(encryptionFlag), identity) + return encryption.DecryptHeader(hdr, viper.GetString(encryptionFlag), identity) }, func(hdr *tar.Header, isRegular bool) error { - return verifyHeader(hdr, isRegular, viper.GetString(signatureFlag), recipient) + return signature.VerifyHeader(hdr, isRegular, viper.GetString(signatureFlag), recipient) }, ) }, diff --git a/cmd/stbak/cmd/recovery_query.go b/cmd/stbak/cmd/recovery_query.go index a159fff..7a4fdfd 100644 --- a/cmd/stbak/cmd/recovery_query.go +++ b/cmd/stbak/cmd/recovery_query.go @@ -9,7 +9,10 @@ import ( "github.com/pojntfx/stfs/internal/controllers" "github.com/pojntfx/stfs/internal/counters" + "github.com/pojntfx/stfs/internal/encryption" "github.com/pojntfx/stfs/internal/formatting" + "github.com/pojntfx/stfs/internal/keys" + "github.com/pojntfx/stfs/internal/signature" "github.com/pojntfx/stfs/internal/tape" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -44,7 +47,7 @@ var recoveryQueryCmd = &cobra.Command{ return err } - recipient, err := parseSignerRecipient(viper.GetString(signatureFlag), pubkey) + recipient, err := keys.ParseSignerRecipient(viper.GetString(signatureFlag), pubkey) if err != nil { return err } @@ -54,7 +57,7 @@ var recoveryQueryCmd = &cobra.Command{ return err } - identity, err := parseIdentity(viper.GetString(encryptionFlag), privkey, viper.GetString(passwordFlag)) + identity, err := keys.ParseIdentity(viper.GetString(encryptionFlag), privkey, viper.GetString(passwordFlag)) if err != nil { return err } @@ -148,11 +151,11 @@ func query( break } - if err := decryptHeader(hdr, encryptionFormat, identity); err != nil { + if err := encryption.DecryptHeader(hdr, encryptionFormat, identity); err != nil { return err } - if err := verifyHeader(hdr, isRegular, signatureFormat, recipient); err != nil { + if err := signature.VerifyHeader(hdr, isRegular, signatureFormat, recipient); err != nil { return err } @@ -235,11 +238,11 @@ func query( } } - if err := decryptHeader(hdr, encryptionFormat, identity); err != nil { + if err := encryption.DecryptHeader(hdr, encryptionFormat, identity); err != nil { return err } - if err := verifyHeader(hdr, isRegular, signatureFormat, recipient); err != nil { + if err := signature.VerifyHeader(hdr, isRegular, signatureFormat, recipient); err != nil { return err } diff --git a/cmd/stbak/cmd/restore.go b/cmd/stbak/cmd/restore.go index bae5a8d..5fb12ec 100644 --- a/cmd/stbak/cmd/restore.go +++ b/cmd/stbak/cmd/restore.go @@ -11,7 +11,10 @@ import ( "github.com/pojntfx/stfs/internal/converters" models "github.com/pojntfx/stfs/internal/db/sqlite/models/metadata" "github.com/pojntfx/stfs/internal/formatting" + "github.com/pojntfx/stfs/internal/keys" "github.com/pojntfx/stfs/internal/persisters" + "github.com/pojntfx/stfs/pkg/config" + "github.com/pojntfx/stfs/pkg/recovery" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/volatiletech/sqlboiler/v4/boil" @@ -55,7 +58,7 @@ var restoreCmd = &cobra.Command{ return err } - recipient, err := parseSignerRecipient(viper.GetString(signatureFlag), pubkey) + recipient, err := keys.ParseSignerRecipient(viper.GetString(signatureFlag), pubkey) if err != nil { return err } @@ -65,7 +68,7 @@ var restoreCmd = &cobra.Command{ return err } - identity, err := parseIdentity(viper.GetString(encryptionFlag), privkey, viper.GetString(passwordFlag)) + identity, err := keys.ParseIdentity(viper.GetString(encryptionFlag), privkey, viper.GetString(passwordFlag)) if err != nil { return err } @@ -126,19 +129,29 @@ var restoreCmd = &cobra.Command{ } } - if err := restoreFromRecordAndBlock( - viper.GetString(driveFlag), + if err := recovery.Fetch( + config.StateConfig{ + Drive: viper.GetString(driveFlag), + Metadata: viper.GetString(metadataFlag), + }, + config.PipeConfig{ + Compression: viper.GetString(compressionFlag), + Encryption: viper.GetString(encryptionFlag), + Signature: viper.GetString(signatureFlag), + }, + config.CryptoConfig{ + Recipient: recipient, + Identity: identity, + Password: viper.GetString(passwordFlag), + }, + viper.GetInt(recordSizeFlag), int(dbhdr.Record), int(dbhdr.Block), dst, false, + false, - viper.GetString(compressionFlag), - viper.GetString(encryptionFlag), - identity, - viper.GetString(signatureFlag), - recipient, ); err != nil { return err } diff --git a/cmd/stbak/cmd/update.go b/cmd/stbak/cmd/update.go index f3606d1..a3f9814 100644 --- a/cmd/stbak/cmd/update.go +++ b/cmd/stbak/cmd/update.go @@ -14,6 +14,7 @@ import ( "github.com/pojntfx/stfs/internal/controllers" "github.com/pojntfx/stfs/internal/counters" "github.com/pojntfx/stfs/internal/formatting" + "github.com/pojntfx/stfs/internal/keys" "github.com/pojntfx/stfs/internal/pax" "github.com/pojntfx/stfs/internal/persisters" "github.com/pojntfx/stfs/pkg/config" @@ -76,7 +77,7 @@ var updateCmd = &cobra.Command{ return err } - identity, err := parseSignerIdentity(viper.GetString(signatureFlag), privkey, viper.GetString(passwordFlag)) + identity, err := keys.ParseSignerIdentity(viper.GetString(signatureFlag), privkey, viper.GetString(passwordFlag)) if err != nil { return err } diff --git a/internal/compression/decompress.go b/internal/compression/decompress.go new file mode 100644 index 0000000..7c1eb8d --- /dev/null +++ b/internal/compression/decompress.go @@ -0,0 +1,59 @@ +package compression + +import ( + "compress/gzip" + "context" + "io" + + "github.com/andybalholm/brotli" + "github.com/cosnicolaou/pbzip2" + "github.com/dsnet/compress/bzip2" + "github.com/klauspost/compress/zstd" + "github.com/klauspost/pgzip" + "github.com/pierrec/lz4/v4" + "github.com/pojntfx/stfs/pkg/config" +) + +func Decompress( + src io.Reader, + compressionFormat string, +) (io.ReadCloser, error) { + switch compressionFormat { + case config.CompressionFormatGZipKey: + fallthrough + case config.CompressionFormatParallelGZipKey: + if compressionFormat == config.CompressionFormatGZipKey { + return gzip.NewReader(src) + } + + return pgzip.NewReader(src) + case config.CompressionFormatLZ4Key: + lz := lz4.NewReader(src) + if err := lz.Apply(lz4.ConcurrencyOption(-1)); err != nil { + return nil, err + } + + return io.NopCloser(lz), nil + case config.CompressionFormatZStandardKey: + zz, err := zstd.NewReader(src) + if err != nil { + return nil, err + } + + return io.NopCloser(zz), nil + case config.CompressionFormatBrotliKey: + br := brotli.NewReader(src) + + return io.NopCloser(br), nil + case config.CompressionFormatBzip2Key: + return bzip2.NewReader(src, nil) + case config.CompressionFormatBzip2ParallelKey: + bz := pbzip2.NewReader(context.Background(), src) + + return io.NopCloser(bz), nil + case config.NoneKey: + return io.NopCloser(src), nil + default: + return nil, config.ErrUnsupportedCompressionFormat + } +} diff --git a/internal/encryption/decrypt.go b/internal/encryption/decrypt.go new file mode 100644 index 0000000..775b6dd --- /dev/null +++ b/internal/encryption/decrypt.go @@ -0,0 +1,141 @@ +package encryption + +import ( + "archive/tar" + "bytes" + "encoding/base64" + "encoding/json" + "io" + + "filippo.io/age" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/pojntfx/stfs/internal/pax" + "github.com/pojntfx/stfs/pkg/config" +) + +func Decrypt( + src io.Reader, + encryptionFormat string, + identity interface{}, +) (io.ReadCloser, error) { + switch encryptionFormat { + case config.EncryptionFormatAgeKey: + identity, ok := identity.(*age.X25519Identity) + if !ok { + return nil, config.ErrIdentityUnparsable + } + + r, err := age.Decrypt(src, identity) + if err != nil { + return nil, err + } + + return io.NopCloser(r), nil + case config.EncryptionFormatPGPKey: + identity, ok := identity.(openpgp.EntityList) + if !ok { + return nil, config.ErrIdentityUnparsable + } + + r, err := openpgp.ReadMessage(src, identity, nil, nil) + if err != nil { + return nil, err + } + + return io.NopCloser(r.UnverifiedBody), nil + case config.NoneKey: + return io.NopCloser(src), nil + default: + return nil, config.ErrUnsupportedEncryptionFormat + } +} + +func DecryptHeader( + hdr *tar.Header, + encryptionFormat string, + identity interface{}, +) error { + if encryptionFormat == config.NoneKey { + return nil + } + + if hdr.PAXRecords == nil { + return config.ErrEmbeddedHeaderMissing + } + + encryptedEmbeddedHeader, ok := hdr.PAXRecords[pax.STFSRecordEmbeddedHeader] + if !ok { + return config.ErrEmbeddedHeaderMissing + } + + embeddedHeader, err := DecryptString(encryptedEmbeddedHeader, encryptionFormat, identity) + if err != nil { + return err + } + + var newHdr tar.Header + if err := json.Unmarshal([]byte(embeddedHeader), &newHdr); err != nil { + return err + } + + *hdr = newHdr + + return nil +} + +func DecryptString( + src string, + encryptionFormat string, + identity interface{}, +) (string, error) { + switch encryptionFormat { + case config.EncryptionFormatAgeKey: + identity, ok := identity.(*age.X25519Identity) + if !ok { + return "", config.ErrIdentityUnparsable + } + + decoded, err := base64.StdEncoding.DecodeString(src) + if err != nil { + return "", err + } + + r, err := age.Decrypt(bytes.NewBufferString(string(decoded)), identity) + if err != nil { + return "", err + } + + out := &bytes.Buffer{} + if _, err := io.Copy(out, r); err != nil { + return "", err + } + + return out.String(), nil + case config.EncryptionFormatPGPKey: + identity, ok := identity.(openpgp.EntityList) + if !ok { + return "", config.ErrIdentityUnparsable + } + + decoded, err := base64.StdEncoding.DecodeString(src) + if err != nil { + return "", err + } + + r, err := openpgp.ReadMessage(bytes.NewBufferString(string(decoded)), identity, nil, nil) + if err != nil { + return "", err + } + + out := &bytes.Buffer{} + if _, err := io.Copy(out, r.UnverifiedBody); err != nil { + return "", err + } + + return out.String(), nil + case config.NoneKey: + return src, nil + default: + return "", config.ErrUnsupportedEncryptionFormat + } +} diff --git a/internal/keys/identity.go b/internal/keys/identity.go new file mode 100644 index 0000000..83f9dcf --- /dev/null +++ b/internal/keys/identity.go @@ -0,0 +1,87 @@ +package keys + +import ( + "bytes" + "io" + + "aead.dev/minisign" + "filippo.io/age" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/pojntfx/stfs/pkg/config" +) + +func ParseIdentity( + encryptionFormat string, + privkey []byte, + password string, +) (interface{}, error) { + switch encryptionFormat { + case config.EncryptionFormatAgeKey: + if password != "" { + passwordIdentity, err := age.NewScryptIdentity(password) + if err != nil { + return nil, err + } + + r, err := age.Decrypt(bytes.NewBuffer(privkey), passwordIdentity) + if err != nil { + return nil, err + } + + out := &bytes.Buffer{} + if _, err := io.Copy(out, r); err != nil { + return nil, err + } + + privkey = out.Bytes() + } + + return age.ParseX25519Identity(string(privkey)) + case config.EncryptionFormatPGPKey: + identities, err := openpgp.ReadKeyRing(bytes.NewBuffer(privkey)) + if err != nil { + return nil, err + } + + if password != "" { + for _, identity := range identities { + if identity.PrivateKey == nil { + return nil, config.ErrIdentityUnparsable + } + + if err := identity.PrivateKey.Decrypt([]byte(password)); err != nil { + return nil, err + } + + for _, subkey := range identity.Subkeys { + if err := subkey.PrivateKey.Decrypt([]byte(password)); err != nil { + return nil, err + } + } + } + } + + return identities, nil + case config.NoneKey: + return privkey, nil + default: + return nil, config.ErrUnsupportedEncryptionFormat + } +} + +func ParseSignerIdentity( + signatureFormat string, + privkey []byte, + password string, +) (interface{}, error) { + switch signatureFormat { + case config.SignatureFormatMinisignKey: + return minisign.DecryptKey(password, privkey) + case config.SignatureFormatPGPKey: + return ParseIdentity(signatureFormat, privkey, password) + case config.NoneKey: + return privkey, nil + default: + return nil, config.ErrUnsupportedSignatureFormat + } +} diff --git a/internal/keys/recipient.go b/internal/keys/recipient.go new file mode 100644 index 0000000..cec9000 --- /dev/null +++ b/internal/keys/recipient.go @@ -0,0 +1,47 @@ +package keys + +import ( + "bytes" + + "aead.dev/minisign" + "filippo.io/age" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/pojntfx/stfs/pkg/config" +) + +func ParseRecipient( + encryptionFormat string, + pubkey []byte, +) (interface{}, error) { + switch encryptionFormat { + case config.EncryptionFormatAgeKey: + return age.ParseX25519Recipient(string(pubkey)) + case config.EncryptionFormatPGPKey: + return openpgp.ReadKeyRing(bytes.NewBuffer(pubkey)) + case config.NoneKey: + return pubkey, nil + default: + return nil, config.ErrUnsupportedEncryptionFormat + } +} + +func ParseSignerRecipient( + signatureFormat string, + pubkey []byte, +) (interface{}, error) { + switch signatureFormat { + case config.SignatureFormatMinisignKey: + var recipient minisign.PublicKey + if err := recipient.UnmarshalText(pubkey); err != nil { + return nil, err + } + + return recipient, nil + case config.SignatureFormatPGPKey: + return ParseRecipient(signatureFormat, pubkey) + case config.NoneKey: + return pubkey, nil + default: + return nil, config.ErrUnsupportedSignatureFormat + } +} diff --git a/internal/signature/verify.go b/internal/signature/verify.go new file mode 100644 index 0000000..286b4c6 --- /dev/null +++ b/internal/signature/verify.go @@ -0,0 +1,195 @@ +package signature + +import ( + "archive/tar" + "bytes" + "encoding/base64" + "encoding/json" + "io" + + "aead.dev/minisign" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/pojntfx/stfs/internal/pax" + "github.com/pojntfx/stfs/pkg/config" +) + +func Verify( + src io.Reader, + isRegular bool, + signatureFormat string, + recipient interface{}, + signature string, +) (io.Reader, func() error, error) { + switch signatureFormat { + case config.SignatureFormatMinisignKey: + if !isRegular { + return nil, nil, config.ErrSignatureFormatOnlyRegularSupport + } + + recipient, ok := recipient.(minisign.PublicKey) + if !ok { + return nil, nil, config.ErrRecipientUnparsable + } + + verifier := minisign.NewReader(src) + + return verifier, func() error { + decodedSignature, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + return err + } + + if verifier.Verify(recipient, decodedSignature) { + return nil + } + + return config.ErrSignatureInvalid + }, nil + case config.SignatureFormatPGPKey: + recipients, ok := recipient.(openpgp.EntityList) + if !ok { + return nil, nil, config.ErrIdentityUnparsable + } + + if len(recipients) < 1 { + return nil, nil, config.ErrIdentityUnparsable + } + + decodedSignature, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + return nil, nil, err + } + + reader := packet.NewReader(bytes.NewBuffer(decodedSignature)) + pkt, err := reader.Next() + if err != nil { + return nil, nil, err + } + + sig, ok := pkt.(*packet.Signature) + if !ok { + return nil, nil, config.ErrSignatureInvalid + } + + hash := sig.Hash.New() + + tee := io.TeeReader(src, hash) + + return tee, func() error { + return recipients[0].PrimaryKey.VerifySignature(hash, sig) + }, nil + case config.NoneKey: + return io.NopCloser(src), func() error { + return nil + }, nil + default: + return nil, nil, config.ErrUnsupportedSignatureFormat + } +} + +func VerifyHeader( + hdr *tar.Header, + isRegular bool, + signatureFormat string, + recipient interface{}, +) error { + if signatureFormat == config.NoneKey { + return nil + } + + if hdr.PAXRecords == nil { + return config.ErrEmbeddedHeaderMissing + } + + embeddedHeader, ok := hdr.PAXRecords[pax.STFSRecordEmbeddedHeader] + if !ok { + return config.ErrEmbeddedHeaderMissing + } + + signature, ok := hdr.PAXRecords[pax.STFSRecordSignature] + if !ok { + return config.ErrSignatureMissing + } + + if err := VerifyString(embeddedHeader, isRegular, signatureFormat, recipient, signature); err != nil { + return err + } + + var newHdr tar.Header + if err := json.Unmarshal([]byte(embeddedHeader), &newHdr); err != nil { + return err + } + + *hdr = newHdr + + return nil +} + +func VerifyString( + src string, + isRegular bool, + signatureFormat string, + recipient interface{}, + signature string, +) error { + switch signatureFormat { + case config.SignatureFormatMinisignKey: + if !isRegular { + return config.ErrSignatureFormatOnlyRegularSupport + } + + recipient, ok := recipient.(minisign.PublicKey) + if !ok { + return config.ErrRecipientUnparsable + } + + decodedSignature, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + return err + } + + if minisign.Verify(recipient, []byte(src), decodedSignature) { + return nil + } + + return config.ErrSignatureInvalid + case config.SignatureFormatPGPKey: + recipients, ok := recipient.(openpgp.EntityList) + if !ok { + return nil + } + + if len(recipients) < 1 { + return nil + } + + decodedSignature, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + return nil + } + + reader := packet.NewReader(bytes.NewBuffer(decodedSignature)) + pkt, err := reader.Next() + if err != nil { + return nil + } + + sig, ok := pkt.(*packet.Signature) + if !ok { + return nil + } + + hash := sig.Hash.New() + + if _, err := io.Copy(hash, bytes.NewBufferString(src)); err != nil { + return err + } + + return recipients[0].PrimaryKey.VerifySignature(hash, sig) + case config.NoneKey: + return nil + default: + return config.ErrUnsupportedSignatureFormat + } +} diff --git a/pkg/config/error.go b/pkg/config/error.go index 6c20658..f5a9e7d 100644 --- a/pkg/config/error.go +++ b/pkg/config/error.go @@ -3,6 +3,16 @@ package config import "errors" var ( - ErrUnsupportedEncryptionFormat = errors.New("unsupported encryption format") ErrUnsupportedCompressionFormat = errors.New("unsupported compression format") + ErrUnsupportedEncryptionFormat = errors.New("unsupported encryption format") + ErrUnsupportedSignatureFormat = errors.New("unsupported signature format") + + ErrIdentityUnparsable = errors.New("recipient could not be parsed") + ErrRecipientUnparsable = errors.New("recipient could not be parsed") + + ErrEmbeddedHeaderMissing = errors.New("embedded header is missing") + + ErrSignatureFormatOnlyRegularSupport = errors.New("this signature format only supports regular files, not i.e. tape drives") + ErrSignatureInvalid = errors.New("signature invalid") + ErrSignatureMissing = errors.New("signature missing") ) diff --git a/pkg/recovery/fetch.go b/pkg/recovery/fetch.go index cdcabba..57e26ce 100644 --- a/pkg/recovery/fetch.go +++ b/pkg/recovery/fetch.go @@ -1,6 +1,19 @@ package recovery import ( + "archive/tar" + "bufio" + "io" + "os" + "path/filepath" + + "github.com/pojntfx/stfs/internal/compression" + "github.com/pojntfx/stfs/internal/controllers" + "github.com/pojntfx/stfs/internal/encryption" + "github.com/pojntfx/stfs/internal/formatting" + "github.com/pojntfx/stfs/internal/pax" + "github.com/pojntfx/stfs/internal/signature" + "github.com/pojntfx/stfs/internal/tape" "github.com/pojntfx/stfs/pkg/config" ) @@ -13,7 +26,131 @@ func Fetch( record int, block int, to string, - preview string, + preview bool, + + showHeader bool, ) error { + f, isRegular, err := tape.OpenTapeReadOnly(state.Drive) + if err != nil { + return err + } + defer f.Close() + + var tr *tar.Reader + if isRegular { + // Seek to record and block + if _, err := f.Seek(int64((recordSize*controllers.BlockSize*record)+block*controllers.BlockSize), io.SeekStart); err != nil { + return err + } + + tr = tar.NewReader(f) + } else { + // Seek to record + if err := controllers.SeekToRecordOnTape(f, int32(record)); err != nil { + return err + } + + // Seek to block + br := bufio.NewReaderSize(f, controllers.BlockSize*recordSize) + if _, err := br.Read(make([]byte, block*controllers.BlockSize)); err != nil { + return err + } + + tr = tar.NewReader(br) + } + + hdr, err := tr.Next() + if err != nil { + return err + } + + if err := encryption.DecryptHeader(hdr, pipes.Encryption, crypto.Identity); err != nil { + return err + } + + if err := signature.VerifyHeader(hdr, isRegular, pipes.Signature, crypto.Recipient); err != nil { + return err + } + + if showHeader { + if err := formatting.PrintCSV(formatting.TARHeaderCSV); err != nil { + return err + } + + if err := formatting.PrintCSV(formatting.GetTARHeaderAsCSV(int64(record), int64(block), hdr)); err != nil { + return err + } + } + + if !preview { + if to == "" { + to = filepath.Base(hdr.Name) + } + + if hdr.Typeflag == tar.TypeDir { + return os.MkdirAll(to, hdr.FileInfo().Mode()) + } + + dstFile, err := os.OpenFile(to, os.O_WRONLY|os.O_CREATE, hdr.FileInfo().Mode()) + if err != nil { + return err + } + + if err := dstFile.Truncate(0); err != nil { + return err + } + + // Don't decompress non-regular files + if !hdr.FileInfo().Mode().IsRegular() { + if _, err := io.Copy(dstFile, tr); err != nil { + return err + } + + return nil + } + + decryptor, err := encryption.Decrypt(tr, pipes.Encryption, crypto.Identity) + if err != nil { + return err + } + + decompressor, err := compression.Decompress(decryptor, pipes.Compression) + if err != nil { + return err + } + + sig := "" + if hdr.PAXRecords != nil { + if s, ok := hdr.PAXRecords[pax.STFSRecordSignature]; ok { + sig = s + } + } + + verifier, verify, err := signature.Verify(decompressor, isRegular, pipes.Signature, crypto.Recipient, sig) + if err != nil { + return err + } + + if _, err := io.Copy(dstFile, verifier); err != nil { + return err + } + + if err := verify(); err != nil { + return err + } + + if err := decryptor.Close(); err != nil { + return err + } + + if err := decompressor.Close(); err != nil { + return err + } + + if err := dstFile.Close(); err != nil { + return err + } + } + return nil }