refactor: Split keys and keyext packages, make compression, signature and encryption packages public
This commit is contained in:
188
pkg/compression/compress.go
Normal file
188
pkg/compression/compress.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package compression
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
"github.com/dsnet/compress/bzip2"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/klauspost/pgzip"
|
||||
"github.com/pierrec/lz4/v4"
|
||||
"github.com/pojntfx/stfs/internal/ioext"
|
||||
"github.com/pojntfx/stfs/internal/mtio"
|
||||
"github.com/pojntfx/stfs/pkg/config"
|
||||
)
|
||||
|
||||
func Compress(
|
||||
dst io.Writer,
|
||||
compressionFormat string,
|
||||
compressionLevel string,
|
||||
isRegular bool,
|
||||
recordSize int,
|
||||
) (ioext.FlusherWriter, error) {
|
||||
switch compressionFormat {
|
||||
case config.CompressionFormatGZipKey:
|
||||
fallthrough
|
||||
case config.CompressionFormatParallelGZipKey:
|
||||
if compressionFormat == config.CompressionFormatGZipKey {
|
||||
if !isRegular {
|
||||
maxSize := getNearestPowerOf2Lower(mtio.BlockSize * recordSize)
|
||||
|
||||
if maxSize < 65535 { // See https://www.daylight.com/meetings/mug00/Sayle/gzip.html#:~:text=Stored%20blocks%20are%20allowed%20to,size%20of%20the%20gzip%20header.
|
||||
return nil, config.ErrCompressionFormatRequiresLargerRecordSize
|
||||
}
|
||||
}
|
||||
|
||||
l := gzip.DefaultCompression
|
||||
switch compressionLevel {
|
||||
case config.CompressionLevelFastest:
|
||||
l = gzip.BestSpeed
|
||||
case config.CompressionLevelBalanced:
|
||||
l = gzip.DefaultCompression
|
||||
case config.CompressionLevelSmallest:
|
||||
l = gzip.BestCompression
|
||||
default:
|
||||
return nil, config.ErrCompressionLevelUnsupported
|
||||
}
|
||||
|
||||
return gzip.NewWriterLevel(dst, l)
|
||||
}
|
||||
|
||||
if !isRegular {
|
||||
return nil, config.ErrCompressionFormatRegularOnly // "device or resource busy"
|
||||
}
|
||||
|
||||
l := pgzip.DefaultCompression
|
||||
switch compressionLevel {
|
||||
case config.CompressionLevelFastest:
|
||||
l = pgzip.BestSpeed
|
||||
case config.CompressionLevelBalanced:
|
||||
l = pgzip.DefaultCompression
|
||||
case config.CompressionLevelSmallest:
|
||||
l = pgzip.BestCompression
|
||||
default:
|
||||
return nil, config.ErrCompressionLevelUnsupported
|
||||
}
|
||||
|
||||
return pgzip.NewWriterLevel(dst, l)
|
||||
case config.CompressionFormatLZ4Key:
|
||||
l := lz4.Level5
|
||||
switch compressionLevel {
|
||||
case config.CompressionLevelFastest:
|
||||
l = lz4.Level1
|
||||
case config.CompressionLevelBalanced:
|
||||
l = lz4.Level5
|
||||
case config.CompressionLevelSmallest:
|
||||
l = lz4.Level9
|
||||
default:
|
||||
return nil, config.ErrCompressionLevelUnsupported
|
||||
}
|
||||
|
||||
opts := []lz4.Option{lz4.CompressionLevelOption(l), lz4.ConcurrencyOption(-1)}
|
||||
if !isRegular {
|
||||
maxSize := getNearestPowerOf2Lower(mtio.BlockSize * recordSize)
|
||||
|
||||
if uint32(maxSize) < uint32(lz4.Block64Kb) {
|
||||
return nil, config.ErrCompressionFormatRequiresLargerRecordSize
|
||||
}
|
||||
|
||||
if uint32(maxSize) < uint32(lz4.Block256Kb) {
|
||||
opts = append(opts, lz4.BlockSizeOption(lz4.Block64Kb))
|
||||
} else if uint32(maxSize) < uint32(lz4.Block1Mb) {
|
||||
opts = append(opts, lz4.BlockSizeOption(lz4.Block256Kb))
|
||||
} else if uint32(maxSize) < uint32(lz4.Block4Mb) {
|
||||
opts = append(opts, lz4.BlockSizeOption(lz4.Block1Mb))
|
||||
} else {
|
||||
opts = append(opts, lz4.BlockSizeOption(lz4.Block4Mb))
|
||||
}
|
||||
}
|
||||
|
||||
lz := lz4.NewWriter(dst)
|
||||
if err := lz.Apply(opts...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ioext.AddFlushNop(lz), nil
|
||||
case config.CompressionFormatZStandardKey:
|
||||
l := zstd.SpeedDefault
|
||||
switch compressionLevel {
|
||||
case config.CompressionLevelFastest:
|
||||
l = zstd.SpeedFastest
|
||||
case config.CompressionLevelBalanced:
|
||||
l = zstd.SpeedDefault
|
||||
case config.CompressionLevelSmallest:
|
||||
l = zstd.SpeedBestCompression
|
||||
default:
|
||||
return nil, config.ErrCompressionLevelUnsupported
|
||||
}
|
||||
|
||||
opts := []zstd.EOption{zstd.WithEncoderLevel(l)}
|
||||
if !isRegular {
|
||||
opts = append(opts, zstd.WithWindowSize(getNearestPowerOf2Lower(mtio.BlockSize*recordSize)))
|
||||
}
|
||||
|
||||
zz, err := zstd.NewWriter(dst, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return zz, nil
|
||||
case config.CompressionFormatBrotliKey:
|
||||
if !isRegular {
|
||||
return nil, config.ErrCompressionFormatRegularOnly // "cannot allocate memory"
|
||||
}
|
||||
|
||||
l := brotli.DefaultCompression
|
||||
switch compressionLevel {
|
||||
case config.CompressionLevelFastest:
|
||||
l = brotli.BestSpeed
|
||||
case config.CompressionLevelBalanced:
|
||||
l = brotli.DefaultCompression
|
||||
case config.CompressionLevelSmallest:
|
||||
l = brotli.BestCompression
|
||||
default:
|
||||
return nil, config.ErrCompressionLevelUnsupported
|
||||
}
|
||||
|
||||
br := brotli.NewWriterLevel(dst, l)
|
||||
|
||||
return br, nil
|
||||
case config.CompressionFormatBzip2Key:
|
||||
fallthrough
|
||||
case config.CompressionFormatBzip2ParallelKey:
|
||||
l := bzip2.DefaultCompression
|
||||
switch compressionLevel {
|
||||
case config.CompressionLevelFastest:
|
||||
l = bzip2.BestSpeed
|
||||
case config.CompressionLevelBalanced:
|
||||
l = bzip2.DefaultCompression
|
||||
case config.CompressionLevelSmallest:
|
||||
l = bzip2.BestCompression
|
||||
default:
|
||||
return nil, config.ErrCompressionLevelUnsupported
|
||||
}
|
||||
|
||||
bz, err := bzip2.NewWriter(dst, &bzip2.WriterConfig{
|
||||
Level: l,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ioext.AddFlushNop(bz), nil
|
||||
case config.NoneKey:
|
||||
return ioext.AddFlushNop(ioext.AddCloseNopToWriter(dst)), nil
|
||||
default:
|
||||
return nil, config.ErrCompressionFormatUnsupported
|
||||
}
|
||||
}
|
||||
|
||||
func getNearestPowerOf2Lower(n int) int {
|
||||
return int(math.Pow(2, float64(getNearestLogOf2Lower(n)))) // Truncation is intentional, see https://www.geeksforgeeks.org/highest-power-2-less-equal-given-number/
|
||||
}
|
||||
|
||||
func getNearestLogOf2Lower(n int) int {
|
||||
return int(math.Log2(float64(n))) // Truncation is intentional, see https://www.geeksforgeeks.org/highest-power-2-less-equal-given-number/
|
||||
}
|
||||
59
pkg/compression/decompress.go
Normal file
59
pkg/compression/decompress.go
Normal file
@@ -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.ErrCompressionFormatUnsupported
|
||||
}
|
||||
}
|
||||
141
pkg/encryption/decrypt.go
Normal file
141
pkg/encryption/decrypt.go
Normal file
@@ -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/records"
|
||||
"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.ErrEncryptionFormatUnsupported
|
||||
}
|
||||
}
|
||||
|
||||
func DecryptHeader(
|
||||
hdr *tar.Header,
|
||||
encryptionFormat string,
|
||||
identity interface{},
|
||||
) error {
|
||||
if encryptionFormat == config.NoneKey {
|
||||
return nil
|
||||
}
|
||||
|
||||
if hdr.PAXRecords == nil {
|
||||
return config.ErrTarHeaderEmbeddedMissing
|
||||
}
|
||||
|
||||
encryptedEmbeddedHeader, ok := hdr.PAXRecords[records.STFSRecordEmbeddedHeader]
|
||||
if !ok {
|
||||
return config.ErrTarHeaderEmbeddedMissing
|
||||
}
|
||||
|
||||
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.ErrEncryptionFormatUnsupported
|
||||
}
|
||||
}
|
||||
127
pkg/encryption/encrypt.go
Normal file
127
pkg/encryption/encrypt.go
Normal file
@@ -0,0 +1,127 @@
|
||||
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/ioext"
|
||||
"github.com/pojntfx/stfs/internal/records"
|
||||
"github.com/pojntfx/stfs/pkg/config"
|
||||
)
|
||||
|
||||
func Encrypt(
|
||||
dst io.Writer,
|
||||
encryptionFormat string,
|
||||
recipient interface{},
|
||||
) (io.WriteCloser, error) {
|
||||
switch encryptionFormat {
|
||||
case config.EncryptionFormatAgeKey:
|
||||
recipient, ok := recipient.(*age.X25519Recipient)
|
||||
if !ok {
|
||||
return nil, config.ErrRecipientUnparsable
|
||||
}
|
||||
|
||||
return age.Encrypt(dst, recipient)
|
||||
case config.EncryptionFormatPGPKey:
|
||||
recipient, ok := recipient.(openpgp.EntityList)
|
||||
if !ok {
|
||||
return nil, config.ErrRecipientUnparsable
|
||||
}
|
||||
|
||||
return openpgp.Encrypt(dst, recipient, nil, nil, nil)
|
||||
case config.NoneKey:
|
||||
return ioext.AddCloseNopToWriter(dst), nil
|
||||
default:
|
||||
return nil, config.ErrEncryptionFormatUnsupported
|
||||
}
|
||||
}
|
||||
|
||||
func EncryptHeader(
|
||||
hdr *tar.Header,
|
||||
encryptionFormat string,
|
||||
recipient interface{},
|
||||
) error {
|
||||
if encryptionFormat == config.NoneKey {
|
||||
return nil
|
||||
}
|
||||
|
||||
newHdr := &tar.Header{
|
||||
Format: tar.FormatPAX,
|
||||
Size: hdr.Size,
|
||||
PAXRecords: map[string]string{},
|
||||
}
|
||||
|
||||
wrappedHeader, err := json.Marshal(hdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newHdr.PAXRecords[records.STFSRecordEmbeddedHeader], err = EncryptString(string(wrappedHeader), encryptionFormat, recipient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*hdr = *newHdr
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func EncryptString(
|
||||
src string,
|
||||
encryptionFormat string,
|
||||
recipient interface{},
|
||||
) (string, error) {
|
||||
switch encryptionFormat {
|
||||
case config.EncryptionFormatAgeKey:
|
||||
recipient, ok := recipient.(*age.X25519Recipient)
|
||||
if !ok {
|
||||
return "", config.ErrRecipientUnparsable
|
||||
}
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
w, err := age.Encrypt(out, recipient)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := io.WriteString(w, src); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(out.Bytes()), nil
|
||||
case config.EncryptionFormatPGPKey:
|
||||
recipient, ok := recipient.(openpgp.EntityList)
|
||||
if !ok {
|
||||
return "", config.ErrRecipientUnparsable
|
||||
}
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
w, err := openpgp.Encrypt(out, recipient, nil, nil, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := io.WriteString(w, src); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(out.Bytes()), nil
|
||||
case config.NoneKey:
|
||||
return src, nil
|
||||
default:
|
||||
return "", config.ErrEncryptionFormatUnsupported
|
||||
}
|
||||
}
|
||||
87
pkg/keys/identity.go
Normal file
87
pkg/keys/identity.go
Normal file
@@ -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.ErrEncryptionFormatUnsupported
|
||||
}
|
||||
}
|
||||
|
||||
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.ErrSignatureFormatUnsupported
|
||||
}
|
||||
}
|
||||
47
pkg/keys/recipient.go
Normal file
47
pkg/keys/recipient.go
Normal file
@@ -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.ErrEncryptionFormatUnsupported
|
||||
}
|
||||
}
|
||||
|
||||
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.ErrSignatureFormatUnsupported
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,17 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pojntfx/stfs/internal/compression"
|
||||
"github.com/pojntfx/stfs/internal/converters"
|
||||
"github.com/pojntfx/stfs/internal/encryption"
|
||||
"github.com/pojntfx/stfs/internal/ioext"
|
||||
"github.com/pojntfx/stfs/internal/mtio"
|
||||
"github.com/pojntfx/stfs/internal/records"
|
||||
"github.com/pojntfx/stfs/internal/signature"
|
||||
"github.com/pojntfx/stfs/internal/suffix"
|
||||
"github.com/pojntfx/stfs/internal/tarext"
|
||||
"github.com/pojntfx/stfs/pkg/compression"
|
||||
"github.com/pojntfx/stfs/pkg/config"
|
||||
"github.com/pojntfx/stfs/pkg/encryption"
|
||||
"github.com/pojntfx/stfs/pkg/recovery"
|
||||
"github.com/pojntfx/stfs/pkg/signature"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -6,12 +6,12 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pojntfx/stfs/internal/converters"
|
||||
"github.com/pojntfx/stfs/internal/encryption"
|
||||
"github.com/pojntfx/stfs/internal/records"
|
||||
"github.com/pojntfx/stfs/internal/signature"
|
||||
"github.com/pojntfx/stfs/internal/tarext"
|
||||
"github.com/pojntfx/stfs/pkg/config"
|
||||
"github.com/pojntfx/stfs/pkg/encryption"
|
||||
"github.com/pojntfx/stfs/pkg/recovery"
|
||||
"github.com/pojntfx/stfs/pkg/signature"
|
||||
)
|
||||
|
||||
func (o *Operations) Delete(name string) error {
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/pojntfx/stfs/internal/converters"
|
||||
"github.com/pojntfx/stfs/internal/encryption"
|
||||
"github.com/pojntfx/stfs/internal/records"
|
||||
"github.com/pojntfx/stfs/internal/signature"
|
||||
"github.com/pojntfx/stfs/internal/tarext"
|
||||
"github.com/pojntfx/stfs/pkg/config"
|
||||
"github.com/pojntfx/stfs/pkg/encryption"
|
||||
"github.com/pojntfx/stfs/pkg/recovery"
|
||||
"github.com/pojntfx/stfs/pkg/signature"
|
||||
)
|
||||
|
||||
func (o *Operations) Move(from string, to string) error {
|
||||
|
||||
@@ -7,17 +7,17 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pojntfx/stfs/internal/compression"
|
||||
"github.com/pojntfx/stfs/internal/converters"
|
||||
"github.com/pojntfx/stfs/internal/encryption"
|
||||
"github.com/pojntfx/stfs/internal/ioext"
|
||||
"github.com/pojntfx/stfs/internal/mtio"
|
||||
"github.com/pojntfx/stfs/internal/records"
|
||||
"github.com/pojntfx/stfs/internal/signature"
|
||||
"github.com/pojntfx/stfs/internal/suffix"
|
||||
"github.com/pojntfx/stfs/internal/tarext"
|
||||
"github.com/pojntfx/stfs/pkg/compression"
|
||||
"github.com/pojntfx/stfs/pkg/config"
|
||||
"github.com/pojntfx/stfs/pkg/encryption"
|
||||
"github.com/pojntfx/stfs/pkg/recovery"
|
||||
"github.com/pojntfx/stfs/pkg/signature"
|
||||
)
|
||||
|
||||
func (o *Operations) Update(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package persisters
|
||||
|
||||
//go:generate sqlboiler sqlite3 -o ../db/sqlite/models/metadata -c ../../configs/sqlboiler/metadata.yaml
|
||||
//go:generate go-bindata -pkg metadata -o ../db/sqlite/migrations/metadata/migrations.go ../../db/sqlite/migrations/metadata
|
||||
//go:generate sqlboiler sqlite3 -o ../../internal/db/sqlite/models/metadata -c ../../configs/sqlboiler/metadata.yaml
|
||||
//go:generate go-bindata -pkg metadata -o ../../internal/db/sqlite/migrations/metadata/migrations.go ../../db/sqlite/migrations/metadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pojntfx/stfs/internal/compression"
|
||||
"github.com/pojntfx/stfs/internal/converters"
|
||||
"github.com/pojntfx/stfs/internal/encryption"
|
||||
"github.com/pojntfx/stfs/internal/mtio"
|
||||
"github.com/pojntfx/stfs/internal/records"
|
||||
"github.com/pojntfx/stfs/internal/signature"
|
||||
"github.com/pojntfx/stfs/pkg/compression"
|
||||
"github.com/pojntfx/stfs/pkg/config"
|
||||
"github.com/pojntfx/stfs/pkg/encryption"
|
||||
"github.com/pojntfx/stfs/pkg/signature"
|
||||
)
|
||||
|
||||
func Fetch(
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"math"
|
||||
|
||||
"github.com/pojntfx/stfs/internal/converters"
|
||||
"github.com/pojntfx/stfs/internal/encryption"
|
||||
"github.com/pojntfx/stfs/internal/ioext"
|
||||
"github.com/pojntfx/stfs/internal/mtio"
|
||||
"github.com/pojntfx/stfs/internal/signature"
|
||||
"github.com/pojntfx/stfs/pkg/config"
|
||||
"github.com/pojntfx/stfs/pkg/encryption"
|
||||
"github.com/pojntfx/stfs/pkg/signature"
|
||||
)
|
||||
|
||||
func Query(
|
||||
|
||||
159
pkg/signature/sign.go
Normal file
159
pkg/signature/sign.go
Normal file
@@ -0,0 +1,159 @@
|
||||
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/records"
|
||||
"github.com/pojntfx/stfs/pkg/config"
|
||||
)
|
||||
|
||||
func Sign(
|
||||
src io.Reader,
|
||||
isRegular bool,
|
||||
signatureFormat string,
|
||||
identity interface{},
|
||||
) (io.Reader, func() (string, error), error) {
|
||||
switch signatureFormat {
|
||||
case config.SignatureFormatMinisignKey:
|
||||
if !isRegular {
|
||||
return nil, nil, config.ErrSignatureFormatRegularOnly
|
||||
}
|
||||
|
||||
identity, ok := identity.(minisign.PrivateKey)
|
||||
if !ok {
|
||||
return nil, nil, config.ErrIdentityUnparsable
|
||||
}
|
||||
|
||||
signer := minisign.NewReader(src)
|
||||
|
||||
return signer, func() (string, error) {
|
||||
return base64.StdEncoding.EncodeToString(signer.Sign(identity)), nil
|
||||
}, nil
|
||||
case config.SignatureFormatPGPKey:
|
||||
identities, ok := identity.(openpgp.EntityList)
|
||||
if !ok {
|
||||
return nil, nil, config.ErrIdentityUnparsable
|
||||
}
|
||||
|
||||
if len(identities) < 1 {
|
||||
return nil, nil, config.ErrIdentityUnparsable
|
||||
}
|
||||
|
||||
// See openpgp.DetachSign
|
||||
var c *packet.Config
|
||||
signingKey, ok := identities[0].SigningKeyById(c.Now(), c.SigningKey())
|
||||
if !ok || signingKey.PrivateKey == nil || signingKey.PublicKey == nil {
|
||||
return nil, nil, config.ErrIdentityUnparsable
|
||||
}
|
||||
|
||||
sig := new(packet.Signature)
|
||||
sig.SigType = packet.SigTypeBinary
|
||||
sig.PubKeyAlgo = signingKey.PrivateKey.PubKeyAlgo
|
||||
sig.Hash = c.Hash()
|
||||
sig.CreationTime = c.Now()
|
||||
sigLifetimeSecs := c.SigLifetime()
|
||||
sig.SigLifetimeSecs = &sigLifetimeSecs
|
||||
sig.IssuerKeyId = &signingKey.PrivateKey.KeyId
|
||||
|
||||
hash := sig.Hash.New()
|
||||
|
||||
return io.TeeReader(src, hash), func() (string, error) {
|
||||
if err := sig.Sign(hash, signingKey.PrivateKey, c); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
if err := sig.Serialize(out); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(out.Bytes()), nil
|
||||
}, nil
|
||||
case config.NoneKey:
|
||||
return src, func() (string, error) {
|
||||
return "", nil
|
||||
}, nil
|
||||
default:
|
||||
return nil, nil, config.ErrSignatureFormatUnsupported
|
||||
}
|
||||
}
|
||||
|
||||
func SignHeader(
|
||||
hdr *tar.Header,
|
||||
isRegular bool,
|
||||
signatureFormat string,
|
||||
identity interface{},
|
||||
) error {
|
||||
if signatureFormat == config.NoneKey {
|
||||
return nil
|
||||
}
|
||||
|
||||
newHdr := &tar.Header{
|
||||
Format: tar.FormatPAX,
|
||||
Size: hdr.Size,
|
||||
PAXRecords: map[string]string{},
|
||||
}
|
||||
|
||||
wrappedHeader, err := json.Marshal(hdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newHdr.PAXRecords[records.STFSRecordEmbeddedHeader] = string(wrappedHeader)
|
||||
newHdr.PAXRecords[records.STFSRecordSignature], err = SignString(newHdr.PAXRecords[records.STFSRecordEmbeddedHeader], isRegular, signatureFormat, identity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*hdr = *newHdr
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SignString(
|
||||
src string,
|
||||
isRegular bool,
|
||||
signatureFormat string,
|
||||
identity interface{},
|
||||
) (string, error) {
|
||||
switch signatureFormat {
|
||||
case config.SignatureFormatMinisignKey:
|
||||
if !isRegular {
|
||||
return "", config.ErrSignatureFormatRegularOnly
|
||||
}
|
||||
|
||||
identity, ok := identity.(minisign.PrivateKey)
|
||||
if !ok {
|
||||
return "", config.ErrIdentityUnparsable
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(minisign.Sign(identity, []byte(src))), nil
|
||||
case config.SignatureFormatPGPKey:
|
||||
identities, ok := identity.(openpgp.EntityList)
|
||||
if !ok {
|
||||
return "", config.ErrIdentityUnparsable
|
||||
}
|
||||
|
||||
if len(identities) < 1 {
|
||||
return "", config.ErrIdentityUnparsable
|
||||
}
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
if err := openpgp.DetachSign(out, identities[0], bytes.NewBufferString(src), nil); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(out.Bytes()), nil
|
||||
case config.NoneKey:
|
||||
return src, nil
|
||||
default:
|
||||
return "", config.ErrSignatureFormatUnsupported
|
||||
}
|
||||
}
|
||||
195
pkg/signature/verify.go
Normal file
195
pkg/signature/verify.go
Normal file
@@ -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/records"
|
||||
"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.ErrSignatureFormatRegularOnly
|
||||
}
|
||||
|
||||
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.ErrSignatureFormatUnsupported
|
||||
}
|
||||
}
|
||||
|
||||
func VerifyHeader(
|
||||
hdr *tar.Header,
|
||||
isRegular bool,
|
||||
signatureFormat string,
|
||||
recipient interface{},
|
||||
) error {
|
||||
if signatureFormat == config.NoneKey {
|
||||
return nil
|
||||
}
|
||||
|
||||
if hdr.PAXRecords == nil {
|
||||
return config.ErrTarHeaderEmbeddedMissing
|
||||
}
|
||||
|
||||
embeddedHeader, ok := hdr.PAXRecords[records.STFSRecordEmbeddedHeader]
|
||||
if !ok {
|
||||
return config.ErrTarHeaderEmbeddedMissing
|
||||
}
|
||||
|
||||
signature, ok := hdr.PAXRecords[records.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.ErrSignatureFormatRegularOnly
|
||||
}
|
||||
|
||||
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.ErrSignatureFormatUnsupported
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user