From 44fdcf765679334792d2d782896014800e13aa23 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Tue, 25 Feb 2025 15:03:50 +0100 Subject: [PATCH 1/2] Add optional support for AEGIS encryption AEGIS is a new family of authenticated encryption algorithms that offers stronger security, higher usage limits, and better performance than AES-GCM. This pull request adds support for a new `-aegis` command-line flag, allowing AEGIS-128X2 to be used as an alternative to AES-GCM on CPUs with AES acceleration. It also introduces the ability to use ciphers with different key sizes. More information on AEGIS is available here: - https://cfrg.github.io/draft-irtf-cfrg-aegis-aead/draft-irtf-cfrg-aegis-aead.html - https://github.com/cfrg/draft-irtf-cfrg-aegis-aead gocryptfs -speed speed on Apple M1: AES-GCM-256-OpenSSL 3718.79 MB/s AES-GCM-256-Go 5083.43 MB/s (selected in auto mode) AES-SIV-512-Go 625.20 MB/s XChaCha20-Poly1305-OpenSSL 1358.63 MB/s (selected in auto mode) XChaCha20-Poly1305-Go 832.11 MB/s Aegis128X2-Go 11818.73 MB/s gocryptfs -speed speed on AMD Zen 4: AES-GCM-256-OpenSSL 5215.86 MB/s AES-GCM-256-Go 6918.01 MB/s (selected in auto mode) AES-SIV-512-Go 449.61 MB/s XChaCha20-Poly1305-OpenSSL 2643.48 MB/s XChaCha20-Poly1305-Go 3727.46 MB/s (selected in auto mode) Aegis128X2-Go 28109.92 MB/s --- .github/workflows/ci.yml | 2 +- Documentation/MANPAGE.md | 4 ++ Documentation/file-format.md | 12 +++-- Makefile | 2 +- README.md | 2 +- benchmark.bash | 7 ++- build-without-aegis.bash | 10 +++++ build-without-cgo.bash | 10 +++++ build-without-openssl.bash | 10 ----- cli_args.go | 5 ++- go.mod | 1 + go.sum | 2 + gocryptfs-xray/xray_main.go | 4 ++ init_dir.go | 1 + internal/configfile/config_file.go | 16 ++++--- internal/configfile/feature_flags.go | 3 ++ internal/configfile/scrypt.go | 8 ++-- internal/configfile/validate.go | 7 ++- internal/contentenc/content_test.go | 9 ++-- internal/contentenc/offsets_test.go | 3 +- internal/cryptocore/cryptocore.go | 48 +++++++++++++++----- internal/exitcodes/exitcodes.go | 2 + internal/fusefrontend/xattr_unit_test.go | 3 +- internal/nametransform/longnames_test.go | 3 +- internal/speed/speed.go | 9 +++- internal/stupidgcm/Makefile | 8 +++- internal/stupidgcm/aegis.go | 57 ++++++++++++++++++++++++ internal/stupidgcm/aegis_test.go | 16 +++++++ internal/stupidgcm/chacha.go | 4 +- internal/stupidgcm/chacha_test.go | 4 +- internal/stupidgcm/common.go | 4 +- internal/stupidgcm/common_test.go | 2 + internal/stupidgcm/doc.go | 6 ++- internal/stupidgcm/gcm.go | 4 +- internal/stupidgcm/gcm_test.go | 4 +- internal/stupidgcm/locking.go | 4 +- internal/stupidgcm/openssl.go | 4 +- internal/stupidgcm/without_aegis.go | 28 ++++++++++++ internal/stupidgcm/without_openssl.go | 4 +- internal/stupidgcm/xchacha.go | 4 +- internal/stupidgcm/xchacha_test.go | 4 +- masterkey.go | 6 +-- mount.go | 3 ++ package-release-tarballs.bash | 2 +- test.bash | 4 +- tests/matrix/matrix_test.go | 9 ++++ 46 files changed, 288 insertions(+), 76 deletions(-) create mode 100755 build-without-aegis.bash create mode 100755 build-without-cgo.bash delete mode 100755 build-without-openssl.bash create mode 100644 internal/stupidgcm/aegis.go create mode 100644 internal/stupidgcm/aegis_test.go create mode 100644 internal/stupidgcm/without_aegis.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aecbd843..6fd93ae5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: - run: echo user_allow_other | sudo tee -a /etc/fuse.conf # Build & upload static binary - - run: ./build-without-openssl.bash + - run: ./build-without-cgo.bash - uses: actions/upload-artifact@v4 with: name: gocryptfs ${{ github.sha }} static ${{ runner.arch }} binary, Go ${{ matrix.go }} diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index c7a1c03b..fd8f5dba 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -164,6 +164,10 @@ specify `-aessiv`. Use XChaCha20-Poly1305 file content encryption. This should be much faster than AES-GCM on CPUs that lack AES acceleration. +#### -aegis +Use AEGIS file content encryption. This should be much faster +than AES-GCM on CPUs with AES acceleration. + Run `gocryptfs -speed` to find out if and how much faster. MOUNT OPTIONS diff --git a/Documentation/file-format.md b/Documentation/file-format.md index 7cce72c1..ee10524f 100644 --- a/Documentation/file-format.md +++ b/Documentation/file-format.md @@ -24,7 +24,13 @@ Data block, XChaCha20-Poly1305 (enabled via `-init -xchacha`) 1-4096 bytes encrypted data 16 bytes Poly1305 tag -Full block overhead (AES-GCM and AES-SIV mode) = 32/4096 = 1/128 = 0.78125 % +Data block, AEGIS (enabled via `-init -aegis`) + + 16 bytes nonce + 1-4096 bytes encrypted data + 16 bytes tag + +Full block overhead (AEGIS, AES-GCM and AES-SIV mode) = 32/4096 = 1/128 = 0.78125 % Full block overhead (XChaCha20-Poly1305 mode) = 40/4096 = \~1 % @@ -36,8 +42,8 @@ Example: 1-byte file, AES-GCM and AES-SIV mode Total: 51 bytes -Example: 5000-byte file, , AES-GCM and AES-SIV mode ---------------------------------------------------- +Example: 5000-byte file, AEGIS, AES-GCM and AES-SIV mode +-------------------------------------------------------- Header 18 bytes Data block 4128 bytes diff --git a/Makefile b/Makefile index beccc5f7..48e21138 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,7 @@ ci: uname -a ; go version ; openssl version df -Th / /tmp /var/tmp - ./build-without-openssl.bash + ./build-without-cgo.bash ./build.bash ./test.bash make root_test diff --git a/README.md b/README.md index 3776c01e..14020bb7 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Then, download the source code and compile: $ git clone https://github.com/rfjakob/gocryptfs.git $ cd gocryptfs - $ ./build-without-openssl.bash + $ ./build-without-cgo.bash This will compile a static binary that uses the Go stdlib crypto backend. diff --git a/benchmark.bash b/benchmark.bash index fb99c659..db962a70 100755 --- a/benchmark.bash +++ b/benchmark.bash @@ -17,6 +17,7 @@ OPT_OPENSSL="" OPT_DIR="" DD_ONLY="" OPT_XCHACHA="" +OPT_AEGIS="" while [[ $# -gt 0 ]] ; do case $1 in @@ -42,6 +43,9 @@ while [[ $# -gt 0 ]] ; do -xchacha) OPT_XCHACHA="-xchacha" ;; + -aegis) + OPT_AEGIS="-aegis" + ;; -*) echo "Invalid option: $1" usage @@ -82,9 +86,10 @@ elif [[ $OPT_LOOPBACK -eq 1 ]]; then "$HOME/go/src/github.com/hanwen/go-fuse/example/loopback/loopback" "$MNT" "$CRYPT" & sleep 0.5 else - echo -n "Testing gocryptfs $OPT_XCHACHA $OPT_OPENSSL at $CRYPT: " + echo -n "Testing gocryptfs $OPT_XCHACHA $OPT_AEGIS $OPT_OPENSSL at $CRYPT: " gocryptfs -version gocryptfs $OPT_XCHACHA -q -init -extpass="echo test" -scryptn=10 "$CRYPT" + gocryptfs $OPT_AEGIS -q -init -extpass="echo test" -scryptn=10 "$CRYPT" gocryptfs $OPT_OPENSSL -q -extpass="echo test" "$CRYPT" "$MNT" fi diff --git a/build-without-aegis.bash b/build-without-aegis.bash new file mode 100755 index 00000000..c20ca1a2 --- /dev/null +++ b/build-without-aegis.bash @@ -0,0 +1,10 @@ +#!/bin/bash -eu + +cd "$(dirname "$0")" + +CGO_ENABLED=0 source ./build.bash -tags without_aegis + +if ldd gocryptfs 2> /dev/null ; then + echo "build-without-aegis.bash: error: compiled binary is not static" + exit 1 +fi diff --git a/build-without-cgo.bash b/build-without-cgo.bash new file mode 100755 index 00000000..753cc314 --- /dev/null +++ b/build-without-cgo.bash @@ -0,0 +1,10 @@ +#!/bin/bash -eu + +cd "$(dirname "$0")" + +CGO_ENABLED=0 source ./build.bash -tags without_openssl,without_aegis + +if ldd gocryptfs 2> /dev/null ; then + echo "build-without-cgo.bash: error: compiled binary is not static" + exit 1 +fi diff --git a/build-without-openssl.bash b/build-without-openssl.bash deleted file mode 100755 index d5dc2182..00000000 --- a/build-without-openssl.bash +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -eu - -cd "$(dirname "$0")" - -CGO_ENABLED=0 source ./build.bash -tags without_openssl - -if ldd gocryptfs 2> /dev/null ; then - echo "build-without-openssl.bash: error: compiled binary is not static" - exit 1 -fi diff --git a/cli_args.go b/cli_args.go index 2e9e7963..26ebdd1c 100644 --- a/cli_args.go +++ b/cli_args.go @@ -31,13 +31,13 @@ type argContainer struct { longnames, allow_other, reverse, aessiv, nonempty, raw64, noprealloc, speed, hkdf, serialize_reads, hh, info, sharedstorage, fsck, one_file_system, deterministic_names, - xchacha bool + xchacha, aegis bool // Mount options with opposites dev, nodev, suid, nosuid, exec, noexec, rw, ro, kernel_cache, acl bool masterkey, mountpoint, cipherdir, cpuprofile, memprofile, ko, ctlsock, fsname, force_owner, trace string // FIDO2 - fido2 string + fido2 string fido2_assert_options []string // -extpass, -badname, -passfile can be passed multiple times extpass, badname, passfile []string @@ -188,6 +188,7 @@ func parseCliOpts(osArgs []string) (args argContainer) { flagSet.BoolVar(&args.one_file_system, "one-file-system", false, "Don't cross filesystem boundaries") flagSet.BoolVar(&args.deterministic_names, "deterministic-names", false, "Disable diriv file name randomisation") flagSet.BoolVar(&args.xchacha, "xchacha", false, "Use XChaCha20-Poly1305 file content encryption") + flagSet.BoolVar(&args.aegis, "aegis", false, "Use AEGIS file content encryption") // Mount options with opposites flagSet.BoolVar(&args.dev, "dev", false, "Allow device files") diff --git a/go.mod b/go.mod index 0761a161..f3146552 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/rfjakob/gocryptfs/v2 go 1.19 require ( + github.com/aegis-aead/go-libaegis v0.2.10 github.com/aperturerobotics/jacobsa-crypto v1.0.2 github.com/hanwen/go-fuse/v2 v2.5.0 github.com/moby/sys/mountinfo v0.6.2 diff --git a/go.sum b/go.sum index 13baeb90..b2818e56 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/aegis-aead/go-libaegis v0.2.10 h1:5BJd8dgl2Fv3XwJ+W1dSWoS9FBaA61ryLnu2E4H2Vdw= +github.com/aegis-aead/go-libaegis v0.2.10/go.mod h1:WKSG0pcdvpDrwDPBl9NdAIxHxpZOCqlKWH7EOnPLKHc= github.com/aperturerobotics/jacobsa-crypto v1.0.2 h1:tNvVy1rev9FagnOyBmTcI6d23FfNceG9IujZROTRtlc= github.com/aperturerobotics/jacobsa-crypto v1.0.2/go.mod h1:buWU1iY+FjIcfpb1aYfFJZfl07WlS7O30lTyC2iwjv8= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= diff --git a/gocryptfs-xray/xray_main.go b/gocryptfs-xray/xray_main.go index 1c491f17..efba7524 100644 --- a/gocryptfs-xray/xray_main.go +++ b/gocryptfs-xray/xray_main.go @@ -81,6 +81,7 @@ type argContainer struct { encryptPaths *bool aessiv *bool xchacha *bool + aegis *bool sep0 *bool fido2 *string version *bool @@ -94,6 +95,7 @@ func main() { args.sep0 = flag.Bool("0", false, "Use \\0 instead of \\n as separator") args.aessiv = flag.Bool("aessiv", false, "Assume AES-SIV mode instead of AES-GCM") args.xchacha = flag.Bool("xchacha", false, "Assume XChaCha20-Poly1305 mode instead of AES-GCM") + args.aegis = flag.Bool("aegis", false, "Assume AEGIS mode instead of AES-GCM") args.fido2 = flag.String("fido2", "", "Protect the masterkey using a FIDO2 token instead of a password") args.version = flag.Bool("version", false, "Print version information") @@ -176,6 +178,8 @@ func inspectCiphertext(args *argContainer, fd *os.File) { algo = cryptocore.BackendAESSIV } else if *args.xchacha { algo = cryptocore.BackendXChaCha20Poly1305 + } else if *args.aegis { + algo = cryptocore.BackendAegis } headerBytes := make([]byte, contentenc.HeaderLen) n, err := fd.ReadAt(headerBytes, 0) diff --git a/init_dir.go b/init_dir.go index d79a4b7b..f4a871fe 100644 --- a/init_dir.go +++ b/init_dir.go @@ -108,6 +108,7 @@ func initDir(args *argContainer) { Fido2AssertOptions: args.fido2_assert_options, DeterministicNames: args.deterministic_names, XChaCha20Poly1305: args.xchacha, + Aegis: args.aegis, LongNameMax: args.longnamemax, Masterkey: handleArgsMasterkey(args), }) diff --git a/internal/configfile/config_file.go b/internal/configfile/config_file.go index 995a0c8c..5e102289 100644 --- a/internal/configfile/config_file.go +++ b/internal/configfile/config_file.go @@ -32,7 +32,7 @@ type FIDO2Params struct { // FIDO2 credential CredentialID []byte // FIDO2 hmac-secret salt - HMACSalt []byte + HMACSalt []byte AssertOptions []string } @@ -75,6 +75,7 @@ type CreateArgs struct { Fido2AssertOptions []string DeterministicNames bool XChaCha20Poly1305 bool + Aegis bool LongNameMax uint8 Masterkey []byte } @@ -92,6 +93,8 @@ func Create(args *CreateArgs) error { cf.setFeatureFlag(FlagHKDF) if args.XChaCha20Poly1305 { cf.setFeatureFlag(FlagXChaCha20Poly1305) + } else if args.Aegis { + cf.setFeatureFlag(FlagAegis) } else { // 128-bit IVs are mandatory for AES-GCM (default is 96!) and AES-SIV, // XChaCha20Poly1305 uses even an even longer IV of 192 bits. @@ -119,9 +122,9 @@ func Create(args *CreateArgs) error { if len(args.Fido2CredentialID) > 0 { cf.setFeatureFlag(FlagFIDO2) cf.FIDO2 = &FIDO2Params{ - CredentialID: args.Fido2CredentialID, - HMACSalt: args.Fido2HmacSalt, - AssertOptions: args.Fido2AssertOptions, + CredentialID: args.Fido2CredentialID, + HMACSalt: args.Fido2HmacSalt, + AssertOptions: args.Fido2AssertOptions, } } // Catch bugs and invalid cli flag combinations early @@ -133,7 +136,7 @@ func Create(args *CreateArgs) error { key := args.Masterkey if key == nil { // Generate new random master key - key = cryptocore.RandBytes(cryptocore.KeyLen) + key = cryptocore.RandBytes(cryptocore.MaxKeyLen) } tlog.PrintMasterkeyReminder(key) // Encrypt it using the password @@ -327,6 +330,9 @@ func (cf *ConfFile) ContentEncryption() (algo cryptocore.AEADTypeEnum, err error if cf.IsFeatureFlagSet(FlagXChaCha20Poly1305) { return cryptocore.BackendXChaCha20Poly1305, nil } + if cf.IsFeatureFlagSet(FlagAegis) { + return cryptocore.BackendAegis, nil + } if cf.IsFeatureFlagSet(FlagAESSIV) { return cryptocore.BackendAESSIV, nil } diff --git a/internal/configfile/feature_flags.go b/internal/configfile/feature_flags.go index d6627a5a..27228312 100644 --- a/internal/configfile/feature_flags.go +++ b/internal/configfile/feature_flags.go @@ -34,6 +34,8 @@ const ( FlagFIDO2 // FlagXChaCha20Poly1305 means we use XChaCha20-Poly1305 file content encryption FlagXChaCha20Poly1305 + // FlagAegis means we use Aegis file content encryption + FlagAegis ) // knownFlags stores the known feature flags and their string representation @@ -49,6 +51,7 @@ var knownFlags = map[flagIota]string{ FlagHKDF: "HKDF", FlagFIDO2: "FIDO2", FlagXChaCha20Poly1305: "XChaCha20Poly1305", + FlagAegis: "AEGIS", } // isFeatureFlagKnown verifies that we understand a feature flag. diff --git a/internal/configfile/scrypt.go b/internal/configfile/scrypt.go index 0ce87771..b82a4315 100644 --- a/internal/configfile/scrypt.go +++ b/internal/configfile/scrypt.go @@ -49,7 +49,7 @@ type ScryptKDF struct { // NewScryptKDF returns a new instance of ScryptKDF. func NewScryptKDF(logN int) ScryptKDF { var s ScryptKDF - s.Salt = cryptocore.RandBytes(cryptocore.KeyLen) + s.Salt = cryptocore.RandBytes(cryptocore.MaxKeyLen) if logN <= 0 { s.N = 1 << ScryptDefaultLogN } else { @@ -57,7 +57,7 @@ func NewScryptKDF(logN int) ScryptKDF { } s.R = 8 // Always 8 s.P = 1 // Always 1 - s.KeyLen = cryptocore.KeyLen + s.KeyLen = cryptocore.MaxKeyLen return s } @@ -98,8 +98,8 @@ func (s *ScryptKDF) validateParams() error { if len(s.Salt) < scryptMinSaltLen { return fmt.Errorf("Fatal: scrypt salt length below minimum: value=%d, min=%d", len(s.Salt), scryptMinSaltLen) } - if s.KeyLen < cryptocore.KeyLen { - return fmt.Errorf("Fatal: scrypt parameter KeyLen below minimum: value=%d, min=%d", s.KeyLen, cryptocore.KeyLen) + if s.KeyLen < cryptocore.MinKeyLen { + return fmt.Errorf("Fatal: scrypt parameter KeyLen below minimum: value=%d, min=%d", s.KeyLen, cryptocore.MinKeyLen) } return nil } diff --git a/internal/configfile/validate.go b/internal/configfile/validate.go index ab8917de..333eea66 100644 --- a/internal/configfile/validate.go +++ b/internal/configfile/validate.go @@ -38,8 +38,13 @@ func (cf *ConfFile) Validate() error { return fmt.Errorf("XChaCha20Poly1305 requires HKDF feature flag") } } + if cf.IsFeatureFlagSet(FlagAegis) { + if cf.IsFeatureFlagSet(FlagGCMIV128) { + return fmt.Errorf("AEGIS conflicts with GCMIV128 feature flag") + } + } // The absence of other flags means AES-GCM (oldest algorithm) - if !cf.IsFeatureFlagSet(FlagXChaCha20Poly1305) && !cf.IsFeatureFlagSet(FlagAESSIV) { + if !cf.IsFeatureFlagSet(FlagAegis) && !cf.IsFeatureFlagSet(FlagXChaCha20Poly1305) && !cf.IsFeatureFlagSet(FlagAESSIV) { if !cf.IsFeatureFlagSet(FlagGCMIV128) { return fmt.Errorf("AES-GCM requires GCMIV128 feature flag") } diff --git a/internal/contentenc/content_test.go b/internal/contentenc/content_test.go index b20ccb14..2a343072 100644 --- a/internal/contentenc/content_test.go +++ b/internal/contentenc/content_test.go @@ -22,7 +22,8 @@ func TestSplitRange(t *testing.T) { {6654, 8945}, } - key := make([]byte, cryptocore.KeyLen) + keyLen := cryptocore.BackendGoGCM.KeyLen + key := make([]byte, keyLen) cc := cryptocore.New(key, cryptocore.BackendGoGCM, DefaultIVBits, true) f := New(cc, DefaultBS) @@ -50,7 +51,8 @@ func TestCiphertextRange(t *testing.T) { {6654, 8945}, } - key := make([]byte, cryptocore.KeyLen) + keyLen := cryptocore.BackendGoGCM.KeyLen + key := make([]byte, keyLen) cc := cryptocore.New(key, cryptocore.BackendGoGCM, DefaultIVBits, true) f := New(cc, DefaultBS) @@ -73,7 +75,8 @@ func TestCiphertextRange(t *testing.T) { } func TestBlockNo(t *testing.T) { - key := make([]byte, cryptocore.KeyLen) + keyLen := cryptocore.BackendGoGCM.KeyLen + key := make([]byte, keyLen) cc := cryptocore.New(key, cryptocore.BackendGoGCM, DefaultIVBits, true) f := New(cc, DefaultBS) diff --git a/internal/contentenc/offsets_test.go b/internal/contentenc/offsets_test.go index b35964ad..0118c5e2 100644 --- a/internal/contentenc/offsets_test.go +++ b/internal/contentenc/offsets_test.go @@ -9,7 +9,8 @@ import ( // TestSizeToSize tests CipherSizeToPlainSize and PlainSizeToCipherSize func TestSizeToSize(t *testing.T) { - key := make([]byte, cryptocore.KeyLen) + keyLen := cryptocore.BackendGoGCM.KeyLen + key := make([]byte, keyLen) cc := cryptocore.New(key, cryptocore.BackendGoGCM, DefaultIVBits, true) ce := New(cc, DefaultBS) diff --git a/internal/cryptocore/cryptocore.go b/internal/cryptocore/cryptocore.go index 72c9c238..0848096d 100644 --- a/internal/cryptocore/cryptocore.go +++ b/internal/cryptocore/cryptocore.go @@ -11,6 +11,7 @@ import ( "golang.org/x/crypto/chacha20poly1305" + "github.com/aegis-aead/go-libaegis/aegis128x2" "github.com/rfjakob/eme" "github.com/rfjakob/gocryptfs/v2/internal/siv_aead" @@ -19,11 +20,13 @@ import ( ) const ( - // KeyLen is the cipher key length in bytes. All backends use 32 bytes. - KeyLen = 32 // AuthTagLen is the length of a authentication tag in bytes. // All backends use 16 bytes. AuthTagLen = 16 + // Minimum key length + MinKeyLen = 16 + // Maximum key length + MaxKeyLen = 32 ) // AEADTypeEnum indicates the type of AEAD backend in use. @@ -32,6 +35,7 @@ type AEADTypeEnum struct { Algo string // Lib is the library where Algo is implemented. Either "Go" or "OpenSSL". Lib string + KeyLen int NonceSize int } @@ -42,22 +46,24 @@ func (a AEADTypeEnum) String() string { // BackendOpenSSL specifies the OpenSSL AES-256-GCM backend. // "AES-GCM-256-OpenSSL" in gocryptfs -speed. -var BackendOpenSSL = AEADTypeEnum{"AES-GCM-256", "OpenSSL", 16} +var BackendOpenSSL = AEADTypeEnum{"AES-GCM-256", "OpenSSL", 32, 16} // BackendGoGCM specifies the Go based AES-256-GCM backend. // "AES-GCM-256-Go" in gocryptfs -speed. -var BackendGoGCM = AEADTypeEnum{"AES-GCM-256", "Go", 16} +var BackendGoGCM = AEADTypeEnum{"AES-GCM-256", "Go", 32, 16} // BackendAESSIV specifies an AESSIV backend. // "AES-SIV-512-Go" in gocryptfs -speed. -var BackendAESSIV = AEADTypeEnum{"AES-SIV-512", "Go", siv_aead.NonceSize} +var BackendAESSIV = AEADTypeEnum{"AES-SIV-512", "Go", 32, siv_aead.NonceSize} // BackendXChaCha20Poly1305 specifies XChaCha20-Poly1305-Go. // "XChaCha20-Poly1305-Go" in gocryptfs -speed. -var BackendXChaCha20Poly1305 = AEADTypeEnum{"XChaCha20-Poly1305", "Go", chacha20poly1305.NonceSizeX} +var BackendXChaCha20Poly1305 = AEADTypeEnum{"XChaCha20-Poly1305", "Go", 32, chacha20poly1305.NonceSizeX} // BackendXChaCha20Poly1305OpenSSL specifies XChaCha20-Poly1305-OpenSSL. -var BackendXChaCha20Poly1305OpenSSL = AEADTypeEnum{"XChaCha20-Poly1305", "OpenSSL", chacha20poly1305.NonceSizeX} +var BackendXChaCha20Poly1305OpenSSL = AEADTypeEnum{"XChaCha20-Poly1305", "OpenSSL", 32, chacha20poly1305.NonceSizeX} + +var BackendAegis = AEADTypeEnum{"Aegis128X2", "Go", 16, aegis128x2.NonceSize} // CryptoCore is the low level crypto implementation. type CryptoCore struct { @@ -85,9 +91,14 @@ func New(key []byte, aeadType AEADTypeEnum, IVBitLen int, useHKDF bool) *CryptoC tlog.Debug.Printf("cryptocore.New: key=%d bytes, aeadType=%v, IVBitLen=%d, useHKDF=%v", len(key), aeadType, IVBitLen, useHKDF) - if len(key) != KeyLen { + keyLen := aeadType.KeyLen + if keyLen < MinKeyLen || keyLen > MaxKeyLen { + log.Panicf("Unsupported key length of %d bytes", keyLen) + } + if len(key) < keyLen { log.Panicf("Unsupported key length of %d bytes", len(key)) } + key = key[:keyLen] // keys can safely be truncated if IVBitLen != 96 && IVBitLen != 128 && IVBitLen != chacha20poly1305.NonceSizeX*8 { log.Panicf("Unsupported IV length of %d bits", IVBitLen) } @@ -98,7 +109,7 @@ func New(key []byte, aeadType AEADTypeEnum, IVBitLen int, useHKDF bool) *CryptoC { var emeBlockCipher cipher.Block if useHKDF { - emeKey := hkdfDerive(key, hkdfInfoEMENames, KeyLen) + emeKey := hkdfDerive(key, hkdfInfoEMENames, keyLen) emeBlockCipher, err = aes.NewCipher(emeKey) for i := range emeKey { emeKey[i] = 0 @@ -117,7 +128,7 @@ func New(key []byte, aeadType AEADTypeEnum, IVBitLen int, useHKDF bool) *CryptoC if aeadType == BackendOpenSSL || aeadType == BackendGoGCM { var gcmKey []byte if useHKDF { - gcmKey = hkdfDerive(key, hkdfInfoGCMContent, KeyLen) + gcmKey = hkdfDerive(key, hkdfInfoGCMContent, keyLen) } else { // Filesystems created by gocryptfs v0.7 through v1.2 don't use HKDF. // Example: tests/example_filesystems/v0.9 @@ -183,6 +194,23 @@ func New(key []byte, aeadType AEADTypeEnum, IVBitLen int, useHKDF bool) *CryptoC if err != nil { log.Panic(err) } + } else if aeadType == BackendAegis { + if stupidgcm.BuiltWithoutAegis { + log.Panic("AEGIS is not available") + } + if IVBitLen != 128 { + log.Panicf("AEGIS must use 128-bit IVs, you wanted %d", IVBitLen) + } + var aegisKey []byte + if useHKDF { + aegisKey = hkdfDerive(key, hkdfInfoGCMContent, keyLen) + } else { + aegisKey = append([]byte{}, key...) + } + aeadCipher = stupidgcm.NewAegis(aegisKey) + for i := range aegisKey { + aegisKey[i] = 0 + } } else { log.Panicf("unknown cipher backend %q", aeadType) } diff --git a/internal/exitcodes/exitcodes.go b/internal/exitcodes/exitcodes.go index 508ba38b..6bf76723 100644 --- a/internal/exitcodes/exitcodes.go +++ b/internal/exitcodes/exitcodes.go @@ -72,6 +72,8 @@ const ( DevNull = 30 // FIDO2Error - an error was encountered while interacting with a FIDO2 token FIDO2Error = 31 + // Aegis means you tried to enable Aegis, but we were compiled without it. + Aegis = 32 ) // Err wraps an error with an associated numeric exit code diff --git a/internal/fusefrontend/xattr_unit_test.go b/internal/fusefrontend/xattr_unit_test.go index 86c87a76..5f69e004 100644 --- a/internal/fusefrontend/xattr_unit_test.go +++ b/internal/fusefrontend/xattr_unit_test.go @@ -16,7 +16,8 @@ import ( func newTestFS(args Args) *RootNode { // Init crypto backend - key := make([]byte, cryptocore.KeyLen) + keyLen := cryptocore.BackendGoGCM.KeyLen + key := make([]byte, keyLen) cCore := cryptocore.New(key, cryptocore.BackendGoGCM, contentenc.DefaultIVBits, true) cEnc := contentenc.New(cCore, contentenc.DefaultBS) n := nametransform.New(cCore.EMECipher, true, 0, true, nil, false) diff --git a/internal/nametransform/longnames_test.go b/internal/nametransform/longnames_test.go index 7a4e9158..be18f926 100644 --- a/internal/nametransform/longnames_test.go +++ b/internal/nametransform/longnames_test.go @@ -34,7 +34,8 @@ func TestRemoveLongNameSuffix(t *testing.T) { } func newLognamesTestInstance(longNameMax uint8) *NameTransform { - key := make([]byte, cryptocore.KeyLen) + keyLen := cryptocore.BackendGoGCM.KeyLen + key := make([]byte, keyLen) cCore := cryptocore.New(key, cryptocore.BackendGoGCM, contentenc.DefaultIVBits, true) return New(cCore.EMECipher, true, longNameMax, true, nil, false) } diff --git a/internal/speed/speed.go b/internal/speed/speed.go index d6fa12ef..e60e37d4 100644 --- a/internal/speed/speed.go +++ b/internal/speed/speed.go @@ -47,13 +47,14 @@ func Run() { {name: cryptocore.BackendAESSIV.String(), f: bAESSIV, preferred: false}, {name: cryptocore.BackendXChaCha20Poly1305OpenSSL.String(), f: bStupidXchacha, preferred: stupidgcm.PreferOpenSSLXchacha20poly1305()}, {name: cryptocore.BackendXChaCha20Poly1305.String(), f: bXchacha20poly1305, preferred: !stupidgcm.PreferOpenSSLXchacha20poly1305()}, + {name: cryptocore.BackendAegis.String(), f: bAegis, preferred: false}, } testing.Init() for _, b := range bTable { fmt.Printf("%-26s\t", b.name) mbs := mbPerSec(testing.Benchmark(b.f)) if mbs > 0 { - fmt.Printf("%7.2f MB/s", mbs) + fmt.Printf("%8.2f MB/s", mbs) } else { fmt.Printf(" N/A") } @@ -168,3 +169,9 @@ func bStupidXchacha(b *testing.B) { } bEncrypt(b, stupidgcm.NewXchacha20poly1305(randBytes(32))) } + +// bAegis benchmarks Aegis from github.com/aegis-aead/go-libaegis +func bAegis(b *testing.B) { + c := stupidgcm.NewAegis(randBytes(16)) + bEncrypt(b, c) +} diff --git a/internal/stupidgcm/Makefile b/internal/stupidgcm/Makefile index 143819de..d8f7d2ef 100644 --- a/internal/stupidgcm/Makefile +++ b/internal/stupidgcm/Makefile @@ -3,11 +3,15 @@ test: gcc # All three ways of building this must work go build go build -tags without_openssl - CGO_ENABLED=0 go build -tags without_openssl + go build -tags without_aegis + go build -tags without_openssl,without_aegis + CGO_ENABLED=0 go build -tags without_openssl,without_aegis # Likewise, all three ways of testing this must work go test -v go test -v -tags without_openssl - CGO_ENABLED=0 go test -v -tags without_openssl + go test -v -tags without_aegis + go test -v -tags without_openssl,without_aegis + CGO_ENABLED=0 go test -v -tags without_openssl,without_aegis .PHONY: gcc gcc: diff --git a/internal/stupidgcm/aegis.go b/internal/stupidgcm/aegis.go new file mode 100644 index 00000000..89750558 --- /dev/null +++ b/internal/stupidgcm/aegis.go @@ -0,0 +1,57 @@ +//go:build !without_aegis && cgo +// +build !without_aegis,cgo + +package stupidgcm + +import ( + "crypto/cipher" + "log" + + "github.com/aegis-aead/go-libaegis/aegis128x2" + "github.com/aegis-aead/go-libaegis/common" +) + +const ( + // BuiltWithoutAegis indicates if aegis been disabled at compile-time + BuiltWithoutAegis = !common.Available + + // Aegis supports 16 and 32 bit tags + AegisTagLen = 16 +) + +type stupidAegis struct { + aead cipher.AEAD +} + +// Verify that we satisfy the cipher.AEAD interface +var _ cipher.AEAD = &stupidAegis{} + +func (*stupidAegis) NonceSize() int { + return aegis128x2.NonceSize +} + +func (*stupidAegis) Overhead() int { + return AegisTagLen +} + +func NewAegis(key []byte) cipher.AEAD { + aead, err := aegis128x2.New(key, AegisTagLen) + if err != nil { + log.Panic(err) + } + return &stupidAegis{ + aead: aead, + } +} + +func (x *stupidAegis) Seal(dst, nonce, plaintext, additionalData []byte) []byte { + return x.aead.Seal(dst, nonce, plaintext, additionalData) +} + +func (x *stupidAegis) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) { + return x.aead.Open(dst, nonce, ciphertext, additionalData) +} + +func (x *stupidAegis) Wipe() { + x.aead.(*aegis128x2.Aegis128X2).Wipe() +} diff --git a/internal/stupidgcm/aegis_test.go b/internal/stupidgcm/aegis_test.go new file mode 100644 index 00000000..36ef7631 --- /dev/null +++ b/internal/stupidgcm/aegis_test.go @@ -0,0 +1,16 @@ +//go:build !without_aegis && cgo +// +build !without_aegis,cgo + +package stupidgcm + +import "testing" + +func TestStupidAegis(t *testing.T) { + if BuiltWithoutAegis { + t.Skip("Aegis support has been disabled at compile-time") + } + key := randBytes(16) + c := NewAegis(key) + + testCiphers(t, c, c) +} diff --git a/internal/stupidgcm/chacha.go b/internal/stupidgcm/chacha.go index de0c2e80..dcdcb3a9 100644 --- a/internal/stupidgcm/chacha.go +++ b/internal/stupidgcm/chacha.go @@ -1,5 +1,5 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build !without_openssl && cgo +// +build !without_openssl,cgo package stupidgcm diff --git a/internal/stupidgcm/chacha_test.go b/internal/stupidgcm/chacha_test.go index 542ff152..4fa038bf 100644 --- a/internal/stupidgcm/chacha_test.go +++ b/internal/stupidgcm/chacha_test.go @@ -1,5 +1,5 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build !without_openssl && cgo +// +build !without_openssl,cgo package stupidgcm diff --git a/internal/stupidgcm/common.go b/internal/stupidgcm/common.go index d88dc62e..c5dd3380 100644 --- a/internal/stupidgcm/common.go +++ b/internal/stupidgcm/common.go @@ -1,5 +1,5 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build !without_openssl && cgo +// +build !without_openssl,cgo package stupidgcm diff --git a/internal/stupidgcm/common_test.go b/internal/stupidgcm/common_test.go index 7f38e902..47fee7c9 100644 --- a/internal/stupidgcm/common_test.go +++ b/internal/stupidgcm/common_test.go @@ -246,6 +246,8 @@ func testWipe(t *testing.T, c cipher.AEAD) { t.Fatal("c2.key is not zeroed") } } + case *stupidAegis: + c2.Wipe() // AEGIS has its own Wipe method default: t.Fatalf("BUG: unhandled type %T", c2) } diff --git a/internal/stupidgcm/doc.go b/internal/stupidgcm/doc.go index dce82ae7..f5d41778 100644 --- a/internal/stupidgcm/doc.go +++ b/internal/stupidgcm/doc.go @@ -1,5 +1,5 @@ -// Package stupidgcm wraps OpenSSL to provide a cipher.AEAD interface for -// authenticated encryption algorithms. +// Package stupidgcm wraps OpenSSL and libaegis to provide a cipher.AEAD +// interface for authenticated encryption algorithms. // // The supported algorithms are: // @@ -9,6 +9,8 @@ // // (3) XChaCha20-Poly1305 (OpenSSL EVP_chacha20_poly1305 + Go HChaCha20) // +// (4) AEGIS (go-libaegis) +// // The golang.org/x/crypto libraries provides implementations for all algorithms, // and the test suite verifies that the implementation in this package gives // the exact same results. diff --git a/internal/stupidgcm/gcm.go b/internal/stupidgcm/gcm.go index 2e5aac49..738021e7 100644 --- a/internal/stupidgcm/gcm.go +++ b/internal/stupidgcm/gcm.go @@ -1,5 +1,5 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build !without_openssl && cgo +// +build !without_openssl,cgo package stupidgcm diff --git a/internal/stupidgcm/gcm_test.go b/internal/stupidgcm/gcm_test.go index c730a877..6c15287a 100644 --- a/internal/stupidgcm/gcm_test.go +++ b/internal/stupidgcm/gcm_test.go @@ -1,5 +1,5 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build !without_openssl && cgo +// +build !without_openssl,cgo // We compare against Go's built-in GCM implementation. Since stupidgcm only // supports 128-bit IVs and Go only supports that from 1.5 onward, we cannot diff --git a/internal/stupidgcm/locking.go b/internal/stupidgcm/locking.go index 04cf2329..29f83323 100644 --- a/internal/stupidgcm/locking.go +++ b/internal/stupidgcm/locking.go @@ -1,5 +1,5 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build !without_openssl && cgo +// +build !without_openssl,cgo package stupidgcm diff --git a/internal/stupidgcm/openssl.go b/internal/stupidgcm/openssl.go index 8c950f86..03606299 100644 --- a/internal/stupidgcm/openssl.go +++ b/internal/stupidgcm/openssl.go @@ -1,5 +1,5 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build !without_openssl && cgo +// +build !without_openssl,cgo package stupidgcm diff --git a/internal/stupidgcm/without_aegis.go b/internal/stupidgcm/without_aegis.go new file mode 100644 index 00000000..efd665c3 --- /dev/null +++ b/internal/stupidgcm/without_aegis.go @@ -0,0 +1,28 @@ +//go:build without_aegis || !cgo +// +build without_aegis !cgo + +package stupidgcm + +import ( + "fmt" + "os" + + "crypto/cipher" + + "github.com/rfjakob/gocryptfs/v2/internal/exitcodes" +) + +const ( + // BuiltWithoutAegis indicates if openssl been disabled at compile-time + BuiltWithoutAegis = true +) + +type stupidAegis struct { + aead cipher.AEAD +} + +func NewAegis(_ []byte) cipher.AEAD { + fmt.Fprintln(os.Stderr, "I have been compiled without aegis support but you are still trying to use aegis") + os.Exit(exitcodes.Aegis) + return nil +} diff --git a/internal/stupidgcm/without_openssl.go b/internal/stupidgcm/without_openssl.go index fcef7938..1901c787 100644 --- a/internal/stupidgcm/without_openssl.go +++ b/internal/stupidgcm/without_openssl.go @@ -1,5 +1,5 @@ -//go:build without_openssl -// +build without_openssl +//go:build without_openssl || !cgo +// +build without_openssl !cgo package stupidgcm diff --git a/internal/stupidgcm/xchacha.go b/internal/stupidgcm/xchacha.go index 3c121ba9..0187da82 100644 --- a/internal/stupidgcm/xchacha.go +++ b/internal/stupidgcm/xchacha.go @@ -1,5 +1,5 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build !without_openssl && cgo +// +build !without_openssl,cgo // Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style diff --git a/internal/stupidgcm/xchacha_test.go b/internal/stupidgcm/xchacha_test.go index 676a023b..bd62c04c 100644 --- a/internal/stupidgcm/xchacha_test.go +++ b/internal/stupidgcm/xchacha_test.go @@ -1,5 +1,5 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build !without_openssl && cgo +// +build !without_openssl,cgo package stupidgcm diff --git a/masterkey.go b/masterkey.go index d4884418..90f0adf9 100644 --- a/masterkey.go +++ b/masterkey.go @@ -20,8 +20,8 @@ func unhexMasterKey(masterkey string, fromStdin bool) []byte { tlog.Fatal.Printf("Could not parse master key: %v", err) os.Exit(exitcodes.MasterKey) } - if len(key) != cryptocore.KeyLen { - tlog.Fatal.Printf("Master key has length %d but we require length %d", len(key), cryptocore.KeyLen) + if len(key) != cryptocore.MaxKeyLen { + tlog.Fatal.Printf("Master key has length %d but we require length %d", len(key), cryptocore.MaxKeyLen) os.Exit(exitcodes.MasterKey) } tlog.Info.Printf("Using explicit master key.") @@ -56,7 +56,7 @@ func handleArgsMasterkey(args *argContainer) (masterkey []byte) { tlog.Info.Printf(tlog.ColorYellow + "ZEROKEY MODE PROVIDES NO SECURITY AT ALL AND SHOULD ONLY BE USED FOR TESTING." + tlog.ColorReset) - return make([]byte, cryptocore.KeyLen) + return make([]byte, cryptocore.MaxKeyLen) } // No master key source specified on the command line. Caller must parse // the config file. diff --git a/mount.go b/mount.go index 0eaa3dd9..c9fa6395 100644 --- a/mount.go +++ b/mount.go @@ -265,6 +265,9 @@ func initFuseFrontend(args *argContainer) (rootNode fs.InodeEmbedder, wipeKeys f } IVBits = chacha20poly1305.NonceSizeX * 8 } + if args.aegis { + cryptoBackend = cryptocore.BackendAegis + } // forceOwner implies allow_other, as documented. // Set this early, so args.allow_other can be relied on below this point. if args._forceOwner != nil { diff --git a/package-release-tarballs.bash b/package-release-tarballs.bash index fde214ed..66c83706 100755 --- a/package-release-tarballs.bash +++ b/package-release-tarballs.bash @@ -49,7 +49,7 @@ package_source() { package_static_binary() { # Compiles the gocryptfs binary and sets $GITVERSION - source build-without-openssl.bash + source build-without-cgo.bash if ldd gocryptfs > /dev/null ; then echo "error: compiled gocryptfs binary is not static" diff --git a/test.bash b/test.bash index 174236e8..70a6a360 100755 --- a/test.bash +++ b/test.bash @@ -51,8 +51,8 @@ fi # Clean up dangling filesystems and don't exit if we found some unmount_leftovers || true -./build-without-openssl.bash || { - echo "$MYNAME: build-without-openssl.bash failed" +./build-without-cgo.bash || { + echo "$MYNAME: build-without-cgo.bash failed" exit 1 } # Don't build with openssl if we were passed "-tags without_openssl" diff --git a/tests/matrix/matrix_test.go b/tests/matrix/matrix_test.go index 417e126c..625a4231 100644 --- a/tests/matrix/matrix_test.go +++ b/tests/matrix/matrix_test.go @@ -72,8 +72,17 @@ var matrix = []testcaseMatrix{ {false, "false", false, true, []string{"-xchacha"}}, } +var matrixAegisAdditions = []testcaseMatrix{ + {false, "auto", false, false, []string{"-aegis"}}, + {true, "auto", false, false, []string{"-aegis"}}, +} + // This is the entry point for the tests func TestMain(m *testing.M) { + if !stupidgcm.BuiltWithoutAegis { + matrix = append(matrix, matrixAegisAdditions...) + } + // Make "testing.Verbose()" return the correct value flag.Parse() var i int From 42f2c131329b1de19ca879079dd1f6f95253d3a3 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Wed, 26 Feb 2025 17:55:17 +0100 Subject: [PATCH 2/2] Use OPT_XCHACHA and OPT_AEGIS in the same command --- benchmark.bash | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/benchmark.bash b/benchmark.bash index db962a70..21bed73a 100755 --- a/benchmark.bash +++ b/benchmark.bash @@ -88,8 +88,7 @@ elif [[ $OPT_LOOPBACK -eq 1 ]]; then else echo -n "Testing gocryptfs $OPT_XCHACHA $OPT_AEGIS $OPT_OPENSSL at $CRYPT: " gocryptfs -version - gocryptfs $OPT_XCHACHA -q -init -extpass="echo test" -scryptn=10 "$CRYPT" - gocryptfs $OPT_AEGIS -q -init -extpass="echo test" -scryptn=10 "$CRYPT" + gocryptfs $OPT_XCHACHA $OPT_AEGIS -q -init -extpass="echo test" -scryptn=10 "$CRYPT" gocryptfs $OPT_OPENSSL -q -extpass="echo test" "$CRYPT" "$MNT" fi