build/ci: Update Go to v1.24, golangci-lint to v1.64.6 and fix issues (#1804)

* build/ci: Update Go to v1.24, golangci-lint to v1.64.5 and fix issues

* Address shazbert's nitters

* linter/config: Fix new linter issue and use versionSize const

* Address gk's nitters and fix additional linter issue after rebase

* Address glorious nits

* staticcheck: Fix additional linter issues after upgrading to Go 1.24.1 and golangci-lint v1.64.6

Also addresses nits

* Improve testing, assertify usage and use common.ErrParsingWSField

* TestCreateNewStrategy: Replace must > should wording
This commit is contained in:
Adrian Gallagher
2025-03-10 16:33:55 +11:00
committed by GitHub
parent c086e281cf
commit d64d56f77c
114 changed files with 5080 additions and 9355 deletions

View File

@@ -4,7 +4,7 @@ import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"io"
@@ -18,7 +18,9 @@ import (
)
const (
saltRandomLength = 12
saltRandomLength = 12
encryptionVersion = 1
versionSize = 2 // 2 bytes as uint16, allows for 65535 versions (at our current rate of 1 version per decade, should last a few generations)
)
// Public errors
@@ -27,14 +29,16 @@ var (
)
var (
errAESBlockSize = errors.New("config file data is too small for the AES required block size")
errNoPrefix = errors.New("data does not start with Encryption Prefix")
errKeyIsEmpty = errors.New("key is empty")
errUserInput = errors.New("error getting user input")
errAESBlockSize = errors.New("config file data is too small for the AES required block size")
errNoPrefix = errors.New("data does not start with Encryption Prefix")
errKeyIsEmpty = errors.New("key is empty")
errUserInput = errors.New("error getting user input")
errUnsupportedEncryptionVersion = errors.New("unsupported encryption version")
// encryptionPrefix is a prefix to tell us the file is encrypted
encryptionPrefix = []byte("THORS-HAMMER")
saltPrefix = []byte("~GCT~SO~SALTY~")
encryptionPrefix = []byte("THORS-HAMMER")
saltPrefix = []byte("~GCT~SO~SALTY~")
encryptionVersionPrefix = []byte("ENCVER")
)
// promptForConfigEncryption asks for encryption confirmation
@@ -120,18 +124,24 @@ func (c *Config) encryptConfigData(configData []byte) ([]byte, error) {
if err != nil {
return nil, err
}
ciphertext := make([]byte, aes.BlockSize+len(configData))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
aead, err := cipher.NewGCMWithRandomNonce(block)
if err != nil {
return nil, err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], configData)
ciphertext := aead.Seal(nil, nil, configData, nil)
appendedFile := append(bytes.Clone(encryptionPrefix), c.storedSalt...)
appendedFile = append(appendedFile, ciphertext...)
appendedFile := make([]byte, len(encryptionPrefix)+len(c.storedSalt)+len(encryptionVersionPrefix)+versionSize+len(ciphertext))
offset := 0
copy(appendedFile[offset:], encryptionPrefix)
offset += len(encryptionPrefix)
copy(appendedFile[offset:], c.storedSalt)
offset += len(c.storedSalt)
copy(appendedFile[offset:], encryptionVersionPrefix)
offset += len(encryptionVersionPrefix)
binary.BigEndian.PutUint16(appendedFile[offset:offset+versionSize], encryptionVersion)
offset += versionSize
copy(appendedFile[offset:], ciphertext)
return appendedFile, nil
}
@@ -165,23 +175,63 @@ func (c *Config) decryptConfigData(d, key []byte) ([]byte, error) {
d = d[len(salt):]
}
blockDecrypt, err := aes.NewCipher(key)
var ciphertext []byte
if !bytes.HasPrefix(d, encryptionVersionPrefix) {
ciphertext, err = decryptAESCFBCiphertext(d, key)
if err != nil {
return nil, err
}
} else {
d = d[len(encryptionVersionPrefix):]
switch ver := binary.BigEndian.Uint16(d[:versionSize]); ver {
case 1: // TODO: Intertwine this with the existing config versioning system
d = d[versionSize:]
ciphertext, err = decryptAESGCMCiphertext(d, key)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("%w: %d", errUnsupportedEncryptionVersion, ver)
}
}
c.sessionDK, c.storedSalt = sessionDK, storedSalt
return ciphertext, nil
}
// decryptAESGCMCiphertext decrypts the ciphertext using AES-GCM
func decryptAESGCMCiphertext(data, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(d) < aes.BlockSize {
cipherAEAD, err := cipher.NewGCMWithRandomNonce(block)
if err != nil {
return nil, err
}
return cipherAEAD.Open(nil, nil, data, nil)
}
// decryptAESCFBCiphertext decrypts the ciphertext using AES-CFB (legacy mode)
func decryptAESCFBCiphertext(data, key []byte) ([]byte, error) {
if len(data) < aes.BlockSize {
return nil, errAESBlockSize
}
iv, d := d[:aes.BlockSize], d[aes.BlockSize:]
iv := data[:aes.BlockSize]
ciphertext := data[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(blockDecrypt, iv)
stream.XORKeyStream(d, d)
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
c.sessionDK, c.storedSalt = sessionDK, storedSalt
return d, nil
stream := cipher.NewCFBDecrypter(block, iv) //nolint:staticcheck // Deprecated CFB is used for legacy mode
plaintext := make([]byte, len(ciphertext))
stream.XORKeyStream(plaintext, ciphertext)
return plaintext, nil
}
// IsEncrypted returns if the data sequence is encrypted

View File

@@ -3,9 +3,13 @@ package config
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/binary"
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -57,7 +61,7 @@ func TestPromptForConfigKey(t *testing.T) {
})
}
func TestEncryptConfigFile(t *testing.T) {
func TestEncryptConfigData(t *testing.T) {
t.Parallel()
_, err := EncryptConfigData([]byte("test"), nil)
require.ErrorIs(t, err, errKeyIsEmpty)
@@ -83,7 +87,7 @@ func TestEncryptConfigFile(t *testing.T) {
require.NoError(t, err)
}
func TestDecryptConfigFile(t *testing.T) {
func TestDecryptConfigData(t *testing.T) {
t.Parallel()
e, err := EncryptConfigData([]byte(`{"test":1}`), []byte("key"))
require.NoError(t, err)
@@ -100,6 +104,93 @@ func TestDecryptConfigFile(t *testing.T) {
_, err = DecryptConfigData(encryptionPrefix, []byte("AAAAAAAAAAAAAAAA"))
require.ErrorIs(t, err, errAESBlockSize)
sessionDK, salt, err := makeNewSessionDK([]byte("key"))
require.NoError(t, err, "makeNewSessionDK must not error")
encData, err := legacyEncrypt(t, salt, []byte(`{"test":123}`), sessionDK)
require.NoError(t, err)
data, err := DecryptConfigData(encData, []byte("key"))
require.NoError(t, err)
assert.Equal(t, `{"test":123}`, string(data))
badVersion := make([]byte, len(encryptionPrefix)+len(encryptionVersionPrefix)+versionSize)
copy(badVersion, encryptionPrefix)
copy(badVersion[len(encryptionPrefix):], encryptionVersionPrefix)
binary.BigEndian.PutUint16(badVersion[len(encryptionPrefix)+len(encryptionVersionPrefix):], 69)
_, err = DecryptConfigData(badVersion, []byte("key"))
require.ErrorIs(t, err, errUnsupportedEncryptionVersion)
}
func legacyEncrypt(t *testing.T, salt, data, key []byte) ([]byte, error) {
t.Helper()
ciphertext, err := aesCFBEncrypt(t, data, key)
if err != nil {
return nil, err
}
encData := append(bytes.Clone(encryptionPrefix), salt...)
encData = append(encData, ciphertext...)
return encData, nil
}
func aesCFBEncrypt(t *testing.T, data, key []byte) ([]byte, error) {
t.Helper()
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
ciphertext := make([]byte, aes.BlockSize+len(data))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
stream := cipher.NewCFBEncrypter(block, iv) //nolint:staticcheck // For testing purposes only
stream.XORKeyStream(ciphertext[aes.BlockSize:], data)
return ciphertext, nil
}
func TestEncryptAESGCMCiphertext(t *testing.T) {
t.Parallel()
_, err := decryptAESGCMCiphertext(nil, nil)
require.ErrorIs(t, err, aes.KeySizeError(0))
validKey := []byte(strings.Repeat("A", 16))
block, err := aes.NewCipher(validKey)
require.NoError(t, err)
aead, err := cipher.NewGCMWithRandomNonce(block)
require.NoError(t, err)
ciphertext := aead.Seal(nil, nil, []byte("MEOWMEOWMEOWMEOWMEOW"), nil)
data, err := decryptAESGCMCiphertext(ciphertext, validKey)
require.NoError(t, err)
assert.Equal(t, "MEOWMEOWMEOWMEOWMEOW", string(data))
}
func TestDecryptAESCFBCiphertext(t *testing.T) {
t.Parallel()
_, err := decryptAESCFBCiphertext(nil, nil)
require.ErrorIs(t, err, errAESBlockSize)
_, err = decryptAESCFBCiphertext([]byte("WOOFWOOFWOOFWOOFWOOF"), []byte("A"))
require.ErrorIs(t, err, aes.KeySizeError(1))
validKey := []byte(strings.Repeat("A", 16))
data, err := aesCFBEncrypt(t, []byte("WOOFWOOFWOOFWOOFWOOF"), validKey)
require.NoError(t, err)
data, err = decryptAESCFBCiphertext(data, validKey)
require.NoError(t, err)
assert.Equal(t, "WOOFWOOFWOOFWOOFWOOF", string(data))
}
func TestIsEncrypted(t *testing.T) {

View File

@@ -1746,7 +1746,7 @@ func BenchmarkUpdateConfig(b *testing.B) {
}
newCfg := c
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = c.UpdateConfig(TestFile, &newCfg, true)
}
}

View File

@@ -217,7 +217,7 @@ func (m *manager) latest() (uint16, error) {
if len(m.versions) == 0 {
return 0, errNoVersions
}
return uint16(len(m.versions)) - 1, nil
return uint16(len(m.versions)) - 1, nil //nolint:gosec // Ignore this as we hardcode version numbers
}
// checkVersions ensures that registered versions are consistent