From df500c6f1cee0becec47888354cf493d866f4060 Mon Sep 17 00:00:00 2001 From: Jan Tytgat Date: Fri, 25 Oct 2024 17:11:27 +0200 Subject: [PATCH 01/11] Add Encrypt() and Decrypt() functionality --- .github/workflows/codeql.yml | 76 +++++++++++++++++++++++ go.mod | 6 ++ go.sum | 10 +++ pkg/transcrypt/cipherSuite.go | 19 ++++++ pkg/transcrypt/convert.go | 87 ++++++++++++++++++++++++++ pkg/transcrypt/crypto.go | 113 ++++++++++++++++++++++++++++++++++ pkg/transcrypt/decrypt.go | 47 ++++++++++++++ pkg/transcrypt/encrypt.go | 55 +++++++++++++++++ 8 files changed, 413 insertions(+) create mode 100644 .github/workflows/codeql.yml create mode 100644 go.sum create mode 100644 pkg/transcrypt/cipherSuite.go create mode 100644 pkg/transcrypt/convert.go create mode 100644 pkg/transcrypt/crypto.go create mode 100644 pkg/transcrypt/decrypt.go create mode 100644 pkg/transcrypt/encrypt.go diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..f5942dc --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,76 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '17 21 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/go.mod b/go.mod index 3201ab0..3ef5279 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ module github.com/jantytgat/go-transcrypt go 1.23.2 + +require ( + github.com/minio/sio v0.4.1 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/sys v0.26.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..89daac2 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/minio/sio v0.4.1 h1:EMe3YBC1nf+sRQia65Rutxi+Z554XPV0dt8BIBA+a/0= +github.com/minio/sio v0.4.1/go.mod h1:oBSjJeGbBdRMZZwna07sX9EFzZy+ywu5aofRiV1g79I= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/pkg/transcrypt/cipherSuite.go b/pkg/transcrypt/cipherSuite.go new file mode 100644 index 0000000..f619710 --- /dev/null +++ b/pkg/transcrypt/cipherSuite.go @@ -0,0 +1,19 @@ +package transcrypt + +type CipherSuite byte + +const ( + AES_256_GCM CipherSuite = iota + CHACHA20_POLY1305 +) + +func GetCipherSuite(s string) CipherSuite { + switch s { + case "AES_256_GCM": + return AES_256_GCM + case "CHACHA20_POLY1305": + return CHACHA20_POLY1305 + default: + return CHACHA20_POLY1305 + } +} diff --git a/pkg/transcrypt/convert.go b/pkg/transcrypt/convert.go new file mode 100644 index 0000000..aa0af7b --- /dev/null +++ b/pkg/transcrypt/convert.go @@ -0,0 +1,87 @@ +package transcrypt + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "fmt" + "reflect" + "regexp" + "strings" + + "github.com/minio/sio" +) + +var regexEncryptedString = regexp.MustCompile(`\d{2}:[\w\d]{24}:[\w\d]*:[\w\d]*`) + +func convertValueToHexString(v reflect.Value) (string, error) { + var err error + switch v.Kind() { + case reflect.Int: + buf := make([]byte, 0) + bufWriter := bytes.NewBuffer(buf) + err = binary.Write(bufWriter, binary.BigEndian, v.Int()) + if err != nil { + return "", err + } + return hex.EncodeToString(bufWriter.Bytes()), nil + default: + return hex.EncodeToString([]byte(v.String())), nil + } +} + +func convertBytesToValue(d []byte, k reflect.Kind) (reflect.Value, error) { + switch k { + case reflect.Int: + decodedInt := binary.BigEndian.Uint64(d) + return reflect.ValueOf(int(decodedInt)), nil + default: + return reflect.ValueOf(string(d)), nil + } +} + +func decodeHexString(key string, data string) ([]byte, reflect.Kind, sio.Config, error) { + if data == "" { + return nil, reflect.Invalid, sio.Config{}, fmt.Errorf("value is empty") + } + + if !regexEncryptedString.MatchString(data) { + return nil, reflect.Invalid, sio.Config{}, fmt.Errorf("value is not valid") + } + + var split []string + split = strings.Split(data, ":") + + var err error + var cipherSuiteBytes []byte + if cipherSuiteBytes, err = hex.DecodeString(split[0]); err != nil { + return nil, reflect.Invalid, sio.Config{}, fmt.Errorf("cannot decode cipersuite: %w", err) + } + + var nonce []byte + if nonce, err = hex.DecodeString(split[1]); err != nil { + return nil, reflect.Invalid, sio.Config{}, fmt.Errorf("cannot decode nonce: %w", err) + } + + var encryptedBytes []byte + if encryptedBytes, err = hex.DecodeString(split[2]); err != nil { + return nil, reflect.Invalid, sio.Config{}, fmt.Errorf("cannot decode encrypted data: %w", err) + } + + var kindBytes []byte + if kindBytes, err = hex.DecodeString(split[3]); err != nil { + return nil, reflect.Invalid, sio.Config{}, fmt.Errorf("cannot decode kindBytes: %w", err) + } + + var kind reflect.Kind + if kind = getKindForString(string(kindBytes)); kind == reflect.Invalid { + return nil, reflect.Invalid, sio.Config{}, fmt.Errorf("cannot decode kind: %w", err) + } + + var cryptoConfig sio.Config + if cryptoConfig, err = createCryptoConfig(key, cipherSuiteBytes, nonce); err != nil { + return nil, reflect.Invalid, sio.Config{}, fmt.Errorf("cannot create crypto config: %w", err) + } + + return encryptedBytes, kind, cryptoConfig, nil +} diff --git a/pkg/transcrypt/crypto.go b/pkg/transcrypt/crypto.go new file mode 100644 index 0000000..7e50d92 --- /dev/null +++ b/pkg/transcrypt/crypto.go @@ -0,0 +1,113 @@ +package transcrypt + +import ( + "crypto/rand" + "crypto/sha256" + "errors" + "fmt" + "io" + "reflect" + + "github.com/minio/sio" + "golang.org/x/crypto/hkdf" +) + +func createCryptoConfig(key string, cipher []byte, salt []byte) (sio.Config, error) { + if key == "" { + return sio.Config{}, errors.New("key is empty") + } + + if cipher == nil { + return sio.Config{}, errors.New("cipher is empty") + } + + var err error + // If salt is nil, create a new salt that can be used for encryption + if salt == nil { + if salt, err = createSalt(); err != nil { + return sio.Config{}, fmt.Errorf("could not create salt: %w", err) + } + } + + // Create encryption key + kdf := hkdf.New(sha256.New, []byte(key), salt[:12], nil) + var encKey [32]byte + if _, err = io.ReadFull(kdf, encKey[:]); err != nil { + return sio.Config{}, fmt.Errorf("failed to derive encryption encKey: %w", err) + } + + return sio.Config{ + CipherSuites: cipher, + Key: encKey[:], + Nonce: (*[12]byte)(salt[:]), + }, nil +} + +// createSalt creates a random salt for use with the encrypt/decrypt functionality +func createSalt() ([]byte, error) { + var nonce [12]byte + if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { + return nil, fmt.Errorf("failed to read random data for nonce: %w", err) + } + + return nonce[:], nil +} + +func getKindForString(s string) reflect.Kind { + switch s { + case "bool": + return reflect.Bool + case "int": + return reflect.Int + case "int8": + return reflect.Int8 + case "int16": + return reflect.Int16 + case "int32": + return reflect.Int32 + case "int64": + return reflect.Int64 + case "uint": + return reflect.Uint + case "uint8": + return reflect.Uint8 + case "uint16": + return reflect.Uint16 + case "uint32": + return reflect.Uint32 + case "uint64": + return reflect.Uint64 + case "uintptr": + return reflect.Uintptr + case "float32": + return reflect.Float32 + case "float64": + return reflect.Float64 + case "complex64": + return reflect.Complex64 + case "complex128": + return reflect.Complex128 + case "array": + return reflect.Array + case "chan": + return reflect.Chan + case "func": + return reflect.Func + case "interface": + return reflect.Interface + case "map": + return reflect.Map + case "pointer": + return reflect.Pointer + case "slice": + return reflect.Slice + case "string": + return reflect.String + case "struct": + return reflect.Struct + case "unsafepointer": + return reflect.UnsafePointer + default: + return reflect.Invalid + } +} diff --git a/pkg/transcrypt/decrypt.go b/pkg/transcrypt/decrypt.go new file mode 100644 index 0000000..ed51062 --- /dev/null +++ b/pkg/transcrypt/decrypt.go @@ -0,0 +1,47 @@ +package transcrypt + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "reflect" + + "github.com/minio/sio" +) + +func Decrypt(key string, data string) (any, error) { + if key == "" { + return nil, errors.New("key is empty") + } + if data == "" { + return nil, errors.New("data is nil") + } + + var err error + var encryptedData []byte + var kind reflect.Kind + var cryptoConfig sio.Config + + if encryptedData, kind, cryptoConfig, err = decodeHexString(key, data); err != nil { + return nil, err + } + + var decryptedHexData *bytes.Buffer + decryptedHexData = bytes.NewBuffer(make([]byte, 0)) + if _, err = sio.Decrypt(decryptedHexData, bytes.NewBuffer(encryptedData), cryptoConfig); err != nil { + return nil, fmt.Errorf("decrypt failed: %w", err) + } + + var decryptedData []byte + if decryptedData, err = hex.DecodeString(string(decryptedHexData.Bytes())); err != nil { + return nil, fmt.Errorf("decode decrypted hex data failed: %w", err) + } + + var outputValue reflect.Value + if outputValue, err = convertBytesToValue(decryptedData, kind); err != nil { + return nil, err + } + + return outputValue.Interface(), nil +} diff --git a/pkg/transcrypt/encrypt.go b/pkg/transcrypt/encrypt.go new file mode 100644 index 0000000..2b2e0a6 --- /dev/null +++ b/pkg/transcrypt/encrypt.go @@ -0,0 +1,55 @@ +package transcrypt + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "reflect" + "strings" + + "github.com/minio/sio" +) + +func Encrypt(key string, cipherSuite CipherSuite, d any) (string, error) { + if key == "" { + return "", errors.New("key is empty") + } + + if d == nil { + return "", errors.New("data is nil") + } + + var err error + var data string + // Convert input data to reflect.Value before serialization + if data, err = convertValueToHexString(reflect.ValueOf(d)); err != nil { + return "", err + } + + var cryptoConfig sio.Config + if cryptoConfig, err = createCryptoConfig(key, []byte{byte(cipherSuite)}, nil); err != nil { + return "", err + } + + encryptedData := bytes.NewBuffer(make([]byte, 0)) + if _, err = sio.Encrypt(encryptedData, bytes.NewBuffer([]byte(data)), cryptoConfig); err != nil { + return "", err + } + + // Encode all details in hex before joining together + encryptedString := strings.Join( + []string{ + hex.EncodeToString([]byte{byte(cipherSuite)}), + hex.EncodeToString(cryptoConfig.Nonce[:]), + hex.EncodeToString(encryptedData.Bytes()), + hex.EncodeToString([]byte(reflect.TypeOf(d).Kind().String())), + }, ":", + ) + + if !regexEncryptedString.MatchString(encryptedString) { + return "", fmt.Errorf("could not validate encrypted data") + } + + return encryptedString, nil +} From 25b79f8cc64eefb6e8699ac91fa02d571c72dd12 Mon Sep 17 00:00:00 2001 From: Jan Tytgat Date: Sun, 12 Jan 2025 17:29:03 +0100 Subject: [PATCH 02/11] Update dependencies --- go.mod | 9 +++++---- go.sum | 14 ++++++-------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 3ef5279..80726c0 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ module github.com/jantytgat/go-transcrypt -go 1.23.2 +go 1.23 require ( - github.com/minio/sio v0.4.1 // indirect - golang.org/x/crypto v0.28.0 // indirect - golang.org/x/sys v0.26.0 // indirect + github.com/minio/sio v0.4.1 + golang.org/x/crypto v0.32.0 ) + +require golang.org/x/sys v0.29.0 // indirect diff --git a/go.sum b/go.sum index 89daac2..f2b9979 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,8 @@ github.com/minio/sio v0.4.1 h1:EMe3YBC1nf+sRQia65Rutxi+Z554XPV0dt8BIBA+a/0= github.com/minio/sio v0.4.1/go.mod h1:oBSjJeGbBdRMZZwna07sX9EFzZy+ywu5aofRiV1g79I= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= From b8084769003f3025950c86b824f47ec7fdf1e51e Mon Sep 17 00:00:00 2001 From: Jan Tytgat Date: Mon, 13 Jan 2025 14:53:25 +0100 Subject: [PATCH 03/11] Remove .gitkeep --- pkg/transcrypt/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pkg/transcrypt/.gitkeep diff --git a/pkg/transcrypt/.gitkeep b/pkg/transcrypt/.gitkeep deleted file mode 100644 index e69de29..0000000 From cdd19539786fe7e7e716cef7c078c24481a9c895 Mon Sep 17 00:00:00 2001 From: Jan Tytgat Date: Mon, 13 Jan 2025 14:53:49 +0100 Subject: [PATCH 04/11] cipherSuite.go: - Add documentation - Add tests --- pkg/transcrypt/cipherSuite.go | 8 +++++-- pkg/transcrypt/cipherSuite_test.go | 37 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 pkg/transcrypt/cipherSuite_test.go diff --git a/pkg/transcrypt/cipherSuite.go b/pkg/transcrypt/cipherSuite.go index f619710..295e011 100644 --- a/pkg/transcrypt/cipherSuite.go +++ b/pkg/transcrypt/cipherSuite.go @@ -1,12 +1,16 @@ package transcrypt -type CipherSuite byte - const ( AES_256_GCM CipherSuite = iota CHACHA20_POLY1305 ) +// CipherSuite defines which cipher suites can be used for transcryption of data. +// It is based on the types available in github.com/minio/sio . +type CipherSuite byte + +// GetCipherSuite converts a string into its respective CipherSuite. +// It returns CHACHA20_POLY1305 by default if the string cannot be converted. func GetCipherSuite(s string) CipherSuite { switch s { case "AES_256_GCM": diff --git a/pkg/transcrypt/cipherSuite_test.go b/pkg/transcrypt/cipherSuite_test.go new file mode 100644 index 0000000..7a370ff --- /dev/null +++ b/pkg/transcrypt/cipherSuite_test.go @@ -0,0 +1,37 @@ +package transcrypt + +import "testing" + +func TestGetCipherSuite(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want CipherSuite + }{ + { + name: "AES_256_GCM", + args: args{s: "AES_256_GCM"}, + want: AES_256_GCM, + }, + { + name: "CHACHA20_POLY1305", + args: args{s: "CHACHA20_POLY1305"}, + want: CHACHA20_POLY1305, + }, + { + name: "random", + args: args{s: "random"}, + want: CHACHA20_POLY1305, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetCipherSuite(tt.args.s); got != tt.want { + t.Errorf("GetCipherSuite() = %v, want %v", got, tt.want) + } + }) + } +} From 92feadbb704c36c963b7dae60ad621b5ff92ab05 Mon Sep 17 00:00:00 2001 From: Jan Tytgat Date: Mon, 13 Jan 2025 14:54:09 +0100 Subject: [PATCH 05/11] convert.go: - Add documentation - Add tests --- pkg/transcrypt/convert.go | 59 +++++-- pkg/transcrypt/convert_test.go | 291 +++++++++++++++++++++++++++++++++ 2 files changed, 332 insertions(+), 18 deletions(-) create mode 100644 pkg/transcrypt/convert_test.go diff --git a/pkg/transcrypt/convert.go b/pkg/transcrypt/convert.go index aa0af7b..c308300 100644 --- a/pkg/transcrypt/convert.go +++ b/pkg/transcrypt/convert.go @@ -12,35 +12,58 @@ import ( "github.com/minio/sio" ) +// Defines the default layout of a string representing encrypted data. +// The string is divided in sections delimited by a colon. +// 1. Cipher suite +// 2. Salt +// 3. Data +// 4. Original data type var regexEncryptedString = regexp.MustCompile(`\d{2}:[\w\d]{24}:[\w\d]*:[\w\d]*`) +// convertBytesToValue converts a byte-array to a reflect.Value. +// It takes a byte-slice and a reflect.Kind and returns an error if the conversion fails. +func convertBytesToValue(d []byte, k reflect.Kind) (reflect.Value, error) { + switch k { + case reflect.Int: + v := reflect.New(reflect.TypeOf(0)) + v.Elem().SetInt(int64(binary.BigEndian.Uint64(d))) + return reflect.ValueOf(v.Elem().Interface()), nil + case reflect.Uint64: + v := reflect.New(reflect.TypeOf(uint64(0))) + v.Elem().SetUint(binary.BigEndian.Uint64(d)) + return reflect.ValueOf(v.Elem().Interface()), nil + case reflect.String: + return reflect.ValueOf(string(d)), nil + default: + return reflect.Value{}, fmt.Errorf("unknown type %v", k) + } +} + +// convertValueToHexString converts a value to a hex-encoded string. +// It returns an empty string and an error if the value is a kind reflect.Int and cannot be converted. func convertValueToHexString(v reflect.Value) (string, error) { var err error switch v.Kind() { case reflect.Int: - buf := make([]byte, 0) - bufWriter := bytes.NewBuffer(buf) - err = binary.Write(bufWriter, binary.BigEndian, v.Int()) - if err != nil { + bufWriter := bytes.NewBuffer(make([]byte, 0)) + if err = binary.Write(bufWriter, binary.BigEndian, v.Int()); err != nil { return "", err } return hex.EncodeToString(bufWriter.Bytes()), nil - default: + case reflect.String: return hex.EncodeToString([]byte(v.String())), nil - } -} - -func convertBytesToValue(d []byte, k reflect.Kind) (reflect.Value, error) { - switch k { - case reflect.Int: - decodedInt := binary.BigEndian.Uint64(d) - return reflect.ValueOf(int(decodedInt)), nil default: - return reflect.ValueOf(string(d)), nil + return "", fmt.Errorf("unknown type %v", v.Kind()) } } +// decodeHexString decodes data into the pieces that make up the encrypted data. +// It takes an encrypted key and data string and returns the actual encrypted data as a byte-slice, reflect.Kind and the encryption config. +// It returns an error if the data string is empty or invalid, or any of the steps to get the encrypted data fails. func decodeHexString(key string, data string) ([]byte, reflect.Kind, sio.Config, error) { + if key == "" { + return nil, reflect.Invalid, sio.Config{}, fmt.Errorf("key is empty") + } if data == "" { return nil, reflect.Invalid, sio.Config{}, fmt.Errorf("value is empty") } @@ -58,9 +81,9 @@ func decodeHexString(key string, data string) ([]byte, reflect.Kind, sio.Config, return nil, reflect.Invalid, sio.Config{}, fmt.Errorf("cannot decode cipersuite: %w", err) } - var nonce []byte - if nonce, err = hex.DecodeString(split[1]); err != nil { - return nil, reflect.Invalid, sio.Config{}, fmt.Errorf("cannot decode nonce: %w", err) + var salt []byte + if salt, err = hex.DecodeString(split[1]); err != nil { + return nil, reflect.Invalid, sio.Config{}, fmt.Errorf("cannot decode salt: %w", err) } var encryptedBytes []byte @@ -79,7 +102,7 @@ func decodeHexString(key string, data string) ([]byte, reflect.Kind, sio.Config, } var cryptoConfig sio.Config - if cryptoConfig, err = createCryptoConfig(key, cipherSuiteBytes, nonce); err != nil { + if cryptoConfig, err = createCryptoConfig(key, cipherSuiteBytes, salt); err != nil { return nil, reflect.Invalid, sio.Config{}, fmt.Errorf("cannot create crypto config: %w", err) } diff --git a/pkg/transcrypt/convert_test.go b/pkg/transcrypt/convert_test.go new file mode 100644 index 0000000..da21d6b --- /dev/null +++ b/pkg/transcrypt/convert_test.go @@ -0,0 +1,291 @@ +package transcrypt + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "reflect" + "testing" + + "github.com/minio/sio" +) + +func Test_convertBytesToValue_String(t *testing.T) { + type args struct { + d []byte + k reflect.Kind + } + + tests := []struct { + name string + args args + want reflect.Value + wantErr bool + }{ + { + name: "string", + args: args{ + d: []byte("hello world"), + k: reflect.TypeOf("hello world").Kind(), + }, + want: reflect.ValueOf("hello world"), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := convertBytesToValue(tt.args.d, tt.args.k) + if (err != nil) != tt.wantErr { + t.Errorf("convertBytesToValue() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got.String(), tt.want.String()) { + t.Errorf("convertBytesToValue() got = %v, want = %v", got, tt.want) + } + }) + } +} + +func Test_convertBytesToValue_Uint(t *testing.T) { + type args struct { + d []byte + k reflect.Kind + } + var inputUint uint64 = 132130 + bufWriterUint := bytes.NewBuffer(make([]byte, 0)) + if bufErr := binary.Write(bufWriterUint, binary.BigEndian, inputUint); bufErr != nil { + panic(bufErr) + } + + var inputInt = 132130 + bufWriterInt := bytes.NewBuffer(make([]byte, 0)) + if bufErr := binary.Write(bufWriterInt, binary.BigEndian, int64(inputInt)); bufErr != nil { + panic(bufErr) + } + tests := []struct { + name string + args args + want reflect.Value + wantErr bool + }{ + { + name: "uint64", + args: args{ + d: bufWriterUint.Bytes(), + k: reflect.TypeOf(inputUint).Kind(), + }, + want: reflect.ValueOf(inputUint), + wantErr: false, + }, + { + name: "int64", + args: args{ + d: bufWriterInt.Bytes(), + k: reflect.TypeOf(inputInt).Kind(), + }, + want: reflect.ValueOf(inputInt), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := convertBytesToValue(tt.args.d, tt.args.k) + if (err != nil) != tt.wantErr { + t.Errorf("convertBytesToValue() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got == tt.want { + t.Errorf("convertBytesToValue() got = %v, want = %v", got, tt.want) + } + }) + } +} + +func Test_convertValueToHexString(t *testing.T) { + type args struct { + v reflect.Value + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "string", + args: args{ + v: reflect.ValueOf("hello world"), + }, + want: "68656c6c6f20776f726c64", + wantErr: false, + }, + { + name: "int", + args: args{ + v: reflect.ValueOf(132130), + }, + want: "0000000000020422", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := convertValueToHexString(tt.args.v) + if (err != nil) != tt.wantErr { + t.Errorf("convertValueToHexString() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("convertValueToHexString() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_decodeHexString(t *testing.T) { + type args struct { + key string + data string + } + + decodedHexKey, _ := hex.DecodeString("308204a20201000282010100b44756063cdb25f2e5ec868ecc5edc733896d637f5e0ffd8a3f820fb0f2acb2268c69a7774c2d4c4ede1ec47d894bbb0f928e9e700db03767b6152548c49f3170d011dd87aa3d36a1f49d38756bcbc6e122d362ff3986165985d264f11ba8e1d35006b66290c4efa8dfd6b4bcbb5e3fbf9277d31ffaa09b319a976313c3bcd25f3b74be78df293bd7b38e5a6caf48be79684882af47630eb147ecb4426e9e7e6e9554b1d8b53530c5c22cfe44e95bfeeca4bb79f1a3c08e5de93d3bf458aae5e821e9e6c4ec6c3602b9b1f56b31da14775608374ec8653e34e2025901cbef725241b166187972dc1c1913f2c8ba54f1ceb443b4019096fac816cf6334aa2e3f50203010001028201004703b2c75241a17945491ed831794cadb6a4f44da6f5b2d2cc047a396b8817ecbe093ddfc086def9941b62d00a68cc66b23f83a4139a328b019f1ca7617bdfde3ca92bf0929ee630ef924d590ab9de201dd8e17792257c7860c490caa4d930122146c107c533ac08d6d5f4e62ea0bfe60a079c318ddc95658fbe4968aba982edbe0775e71eba35836758b2c486e54e54de4cbb3b7004a9e16b0d0da3b88dbf026413d97e0396aec1f739d504b4eb75a7719fb7bda9886c78a99a79a55ad2b06c36a46ac6df71cd021296020865a2f79b3a0950f9268a1fc773ab407e3e283c10f6fe9412acbae260c2a5c601d75a731db18294a91d20cce7d73d039d7162742102818100efad47d757feb7f570f55f81b7b0fe271de91633234146b5f9fb682b3aafd574481bbd49c85b27c4c118f2634755800b4eb967e46ff6ca4f392bac226410ce31b7c0d22735e61928a940d36033aac6aaa088d21a36720b90904a7ea8b62f998a99938d5ced7b8f2eb2339e34559b25506c43bed054562543a260f426c3136ac702818100c08e75852d7375a09087a5e793d7372e3804257dda911408a1270a14a825879e26d5c7b85796a5ff6f4a290a9185cc097d3eebe0bb2fd8520b50cc9f4bb30a7815a51a23d5ea752f8035c242bce5839ad4f4c6fcb5921e1a8a8f672fc378a7afc57a04c2e882c21414c656b84bf8c3efd4ea0dbbd4269e048e66196d57b99f630281807f592ce0e8ea78c83afac582611df40cc8c1be7ff16d8faac566a5d4c25c0728bfdfad55f4d52a6e4ac37c96efa22864d9b17dd84cfd6e4565f5248329741c7b224d9bdc25b15b10d5cd92027db171d9db6e976442259aab775f7da91b14739ac73b35537903bbf26dd12b7057441631833503c021ef9be131f81e023288b0a50281807cc552bd4320479e0d48f865c0547a3b06ad19261dd45828e75386a2aff9f190b7155b5ec5d2a629881183da87452d5b10bf0ed506361073c94547f20879315572a112f91989dcf93498a111e198ced82b19993ef2e08585293796e34a440a54491fb1aa22436842dedb4e2209885e5e2f96a1e38daaa045cf87b4fe3713de8502818021680ea1a93b048af3deec6fd09d9d110d391e908cb6eeab615e5595556f07238c44f7b8795cffe3508aa3dff21de927576454537473e9b07eaedf0864f4a5a0f6275fbef3f6c9b42f7c692b351451004c25c4918edabbd1f22e5d174aac550db7327e99cb58be7641ec0479425bf00f8b3640685a95e70c93e820c7f39e1a1e") + tests := []struct { + name string + args args + wantData []byte + wantKind reflect.Kind + wantConfig sio.Config + wantErr bool + }{ + { + name: "empty_key", + args: args{ + key: "", + data: "", + }, + wantData: nil, + wantKind: reflect.Invalid, + wantErr: true, + }, + { + name: "empty_data", + args: args{ + key: string(decodedHexKey), + data: "", + }, + wantData: nil, + wantKind: reflect.Invalid, + wantErr: true, + }, + { + name: "invalid_value_ciphersuite", + args: args{ + key: string(decodedHexKey), + data: "__:68e191dfc1f3180904d19a58:20001500e8e191dfc1f3180904d19a589d6c41d057473145672f5e7a90b1fa1d47b21ece952eafbbfa38668f2885b323179721bc10a5:737472696e67", + }, + wantData: nil, + wantKind: reflect.Invalid, + wantErr: true, + }, + { + name: "invalid_value_nonce", + args: args{ + key: string(decodedHexKey), + data: "00:__:20001500e8e191dfc1f3180904d19a589d6c41d057473145672f5e7a90b1fa1d47b21ece952eafbbfa38668f2885b323179721bc10a5:737472696e67", + }, + wantData: nil, + wantKind: reflect.Invalid, + wantErr: true, + }, + { + name: "invalid_value_data", + args: args{ + key: string(decodedHexKey), + data: "00:68e191dfc1f3180904d19a58:__:737472696e67", + }, + wantData: nil, + wantKind: reflect.Invalid, + wantErr: true, + }, + { + name: "invalid_value_kind", + args: args{ + key: string(decodedHexKey), + data: "00:68e191dfc1f3180904d19a58:20001500e8e191dfc1f3180904d19a589d6c41d057473145672f5e7a90b1fa1d47b21ece952eafbbfa38668f2885b323179721bc10a5:__", + }, + wantData: nil, + wantKind: reflect.Invalid, + wantErr: true, + }, + { + name: "invalid_ciphersuite", + args: args{ + key: string(decodedHexKey), + data: "dd:68e191dfc1f3180904d19a58:20001500e8e191dfc1f3180904d19a589d6c41d057473145672f5e7a90b1fa1d47b21ece952eafbbfa38668f2885b323179721bc10a5:737472696e67", + }, + wantData: nil, + wantKind: reflect.String, + wantErr: true, + }, + { + name: "invalid_nonce", + args: args{ + key: string(decodedHexKey), + data: "00:dddddddddddddddddddddddd:20001500e8e191dfc1f3180904d19a589d6c41d057473145672f5e7a90b1fa1d47b21ece952eafbbfa38668f2885b323179721bc10a5:737472696e67", + }, + wantData: nil, + wantKind: reflect.String, + wantErr: true, + }, + { + name: "invalid_data", + args: args{ + key: string(decodedHexKey), + data: "00:68e191dfc1f3180904d19a58:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:737472696e67", + }, + wantData: nil, + wantKind: reflect.String, + wantErr: true, + }, + { + name: "invalid_kind", + args: args{ + key: string(decodedHexKey), + data: "00:68e191dfc1f3180904d19a58:20001500e8e191dfc1f3180904d19a589d6c41d057473145672f5e7a90b1fa1d47b21ece952eafbbfa38668f2885b323179721bc10a5:dddddddddddd", + }, + wantData: nil, + wantKind: reflect.String, + wantErr: true, + }, + { + name: "valid", + args: args{ + key: string(decodedHexKey), + data: "00:68e191dfc1f3180904d19a58:20001500e8e191dfc1f3180904d19a589d6c41d057473145672f5e7a90b1fa1d47b21ece952eafbbfa38668f2885b323179721bc10a5:737472696e67", + }, + wantData: nil, + wantKind: reflect.String, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotData, gotKind, gotConfig, err := decodeHexString(tt.args.key, tt.args.data) + if err != nil { + if (err != nil) != tt.wantErr { + t.Errorf("decodeHexString() error = %v, wantErr %v", err, tt.wantErr) + } + return + } + if string(gotData) == string(tt.wantData) { + t.Errorf("decodeHexString() gotData = %v, wantData %v", gotData, tt.wantData) + } + if gotKind != tt.wantKind { + t.Errorf("decodeHexString() gotKind = %v, wantKind %v", gotKind, tt.wantKind) + } + if string(gotConfig.CipherSuites) == string(tt.wantConfig.CipherSuites) { + t.Errorf("decodeHexString() gotCipherSuites = %v, wantCipherSuites %v", gotConfig, tt.wantConfig) + } + }) + } +} From fabee929b51c70e16eb0a040f9b2376229c53c80 Mon Sep 17 00:00:00 2001 From: Jan Tytgat Date: Mon, 13 Jan 2025 14:56:36 +0100 Subject: [PATCH 06/11] crypto.go: - Add documentation - Add tests --- pkg/transcrypt/crypto.go | 11 +- pkg/transcrypt/crypto_test.go | 237 ++++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 pkg/transcrypt/crypto_test.go diff --git a/pkg/transcrypt/crypto.go b/pkg/transcrypt/crypto.go index 7e50d92..7de4cbd 100644 --- a/pkg/transcrypt/crypto.go +++ b/pkg/transcrypt/crypto.go @@ -12,6 +12,9 @@ import ( "golang.org/x/crypto/hkdf" ) +// createCryptoConfig creates a sio.config from the supplied key, cipher and optional salt. +// It returns an error if either key or cipher is empty. +// It also returns an error if the supplied salt is less than 12 bytes long. func createCryptoConfig(key string, cipher []byte, salt []byte) (sio.Config, error) { if key == "" { return sio.Config{}, errors.New("key is empty") @@ -29,6 +32,10 @@ func createCryptoConfig(key string, cipher []byte, salt []byte) (sio.Config, err } } + if len(salt) < 12 { + return sio.Config{}, fmt.Errorf("salt needs to be at least 12 bytes, got %d", len(salt)) + } + // Create encryption key kdf := hkdf.New(sha256.New, []byte(key), salt[:12], nil) var encKey [32]byte @@ -43,7 +50,7 @@ func createCryptoConfig(key string, cipher []byte, salt []byte) (sio.Config, err }, nil } -// createSalt creates a random salt for use with the encrypt/decrypt functionality +// createSalt creates a random salt for use with the encrypt/decrypt functionality. func createSalt() ([]byte, error) { var nonce [12]byte if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { @@ -53,6 +60,8 @@ func createSalt() ([]byte, error) { return nonce[:], nil } +// getKindFromString converts a string to its representative reflect.Kind. +// It returns a reflect.Invalid by default if the supplied string cannot be found. func getKindForString(s string) reflect.Kind { switch s { case "bool": diff --git a/pkg/transcrypt/crypto_test.go b/pkg/transcrypt/crypto_test.go new file mode 100644 index 0000000..87af8ea --- /dev/null +++ b/pkg/transcrypt/crypto_test.go @@ -0,0 +1,237 @@ +package transcrypt + +import ( + "reflect" + "testing" +) + +func Test_createCryptoConfig(t *testing.T) { + type args struct { + key string + cipher []byte + salt []byte + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty_key", + args: args{ + key: "test", + cipher: nil, + salt: nil, + }, + wantErr: true, + }, + { + name: "empty_cipher", + args: args{ + key: "test", + cipher: nil, + salt: nil, + }, + wantErr: true, + }, + { + name: "invalid_salt", + args: args{ + key: "test", + cipher: []byte("cipher"), + salt: []byte("salt"), + }, + wantErr: true, + }, + { + name: "valid", + args: args{ + key: "test", + cipher: []byte("cipher"), + salt: nil, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := createCryptoConfig(tt.args.key, tt.args.cipher, tt.args.salt) + if (err != nil) != tt.wantErr { + t.Errorf("createCryptoConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func Test_createSalt(t *testing.T) { + tests := []struct { + name string + wantErr bool + }{ + { + name: "succes", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := createSalt() + if (err != nil) != tt.wantErr { + t.Errorf("createSalt() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func Test_getKindForString(t *testing.T) { + tests := []struct { + name string + kind string + want reflect.Kind + }{ + { + name: "bool", + kind: "bool", + want: reflect.Bool, + }, + { + name: "int", + kind: "int", + want: reflect.Int, + }, + { + name: "int8", + kind: "int8", + want: reflect.Int8, + }, + { + name: "int16", + kind: "int16", + want: reflect.Int16, + }, + { + name: "int32", + kind: "int32", + want: reflect.Int32, + }, + { + name: "int64", + kind: "int64", + want: reflect.Int64, + }, + { + name: "uint", + kind: "uint", + want: reflect.Uint, + }, + { + name: "uint8", + kind: "uint8", + want: reflect.Uint8, + }, + { + name: "uint16", + kind: "uint16", + want: reflect.Uint16, + }, + { + name: "uint32", + kind: "uint32", + want: reflect.Uint32, + }, + { + name: "uint64", + kind: "uint64", + want: reflect.Uint64, + }, + { + name: "uintptr", + kind: "uintptr", + want: reflect.Uintptr, + }, + { + name: "float32", + kind: "float32", + want: reflect.Float32, + }, + { + name: "float64", + kind: "float64", + want: reflect.Float64, + }, + { + name: "complex64", + kind: "complex64", + want: reflect.Complex64, + }, + { + name: "complex128", + kind: "complex128", + want: reflect.Complex128, + }, + { + name: "array", + kind: "array", + want: reflect.Array, + }, + { + name: "chan", + kind: "chan", + want: reflect.Chan, + }, + { + name: "func", + kind: "func", + want: reflect.Func, + }, + { + name: "interface", + kind: "interface", + want: reflect.Interface, + }, + { + name: "map", + kind: "map", + want: reflect.Map, + }, + { + name: "pointer", + kind: "pointer", + want: reflect.Pointer, + }, + { + name: "slice", + kind: "slice", + want: reflect.Slice, + }, + { + name: "string", + kind: "string", + want: reflect.String, + }, + { + name: "struct", + kind: "struct", + want: reflect.Struct, + }, + { + name: "unsafepointer", + kind: "unsafepointer", + want: reflect.UnsafePointer, + }, + { + name: "default", + kind: "default", + want: reflect.Invalid, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getKindForString(tt.kind); got != tt.want { + t.Errorf("getKindForString() = %v, want %v", got, tt.want) + } + }) + } +} From 3dcb0ca7add086379b73ba8d7d4fd6a432f0638f Mon Sep 17 00:00:00 2001 From: Jan Tytgat Date: Mon, 13 Jan 2025 15:16:25 +0100 Subject: [PATCH 07/11] Merge encrypt.go and decrypt.go into transcrypt.go - Add documentation for Encrypt and Decrypt functions - Add package documentation Signed-off-by: Jan Tytgat --- pkg/transcrypt/decrypt.go | 47 ---------------- pkg/transcrypt/encrypt.go | 55 ------------------ pkg/transcrypt/transcrypt.go | 104 +++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 102 deletions(-) delete mode 100644 pkg/transcrypt/decrypt.go delete mode 100644 pkg/transcrypt/encrypt.go create mode 100644 pkg/transcrypt/transcrypt.go diff --git a/pkg/transcrypt/decrypt.go b/pkg/transcrypt/decrypt.go deleted file mode 100644 index ed51062..0000000 --- a/pkg/transcrypt/decrypt.go +++ /dev/null @@ -1,47 +0,0 @@ -package transcrypt - -import ( - "bytes" - "encoding/hex" - "errors" - "fmt" - "reflect" - - "github.com/minio/sio" -) - -func Decrypt(key string, data string) (any, error) { - if key == "" { - return nil, errors.New("key is empty") - } - if data == "" { - return nil, errors.New("data is nil") - } - - var err error - var encryptedData []byte - var kind reflect.Kind - var cryptoConfig sio.Config - - if encryptedData, kind, cryptoConfig, err = decodeHexString(key, data); err != nil { - return nil, err - } - - var decryptedHexData *bytes.Buffer - decryptedHexData = bytes.NewBuffer(make([]byte, 0)) - if _, err = sio.Decrypt(decryptedHexData, bytes.NewBuffer(encryptedData), cryptoConfig); err != nil { - return nil, fmt.Errorf("decrypt failed: %w", err) - } - - var decryptedData []byte - if decryptedData, err = hex.DecodeString(string(decryptedHexData.Bytes())); err != nil { - return nil, fmt.Errorf("decode decrypted hex data failed: %w", err) - } - - var outputValue reflect.Value - if outputValue, err = convertBytesToValue(decryptedData, kind); err != nil { - return nil, err - } - - return outputValue.Interface(), nil -} diff --git a/pkg/transcrypt/encrypt.go b/pkg/transcrypt/encrypt.go deleted file mode 100644 index 2b2e0a6..0000000 --- a/pkg/transcrypt/encrypt.go +++ /dev/null @@ -1,55 +0,0 @@ -package transcrypt - -import ( - "bytes" - "encoding/hex" - "errors" - "fmt" - "reflect" - "strings" - - "github.com/minio/sio" -) - -func Encrypt(key string, cipherSuite CipherSuite, d any) (string, error) { - if key == "" { - return "", errors.New("key is empty") - } - - if d == nil { - return "", errors.New("data is nil") - } - - var err error - var data string - // Convert input data to reflect.Value before serialization - if data, err = convertValueToHexString(reflect.ValueOf(d)); err != nil { - return "", err - } - - var cryptoConfig sio.Config - if cryptoConfig, err = createCryptoConfig(key, []byte{byte(cipherSuite)}, nil); err != nil { - return "", err - } - - encryptedData := bytes.NewBuffer(make([]byte, 0)) - if _, err = sio.Encrypt(encryptedData, bytes.NewBuffer([]byte(data)), cryptoConfig); err != nil { - return "", err - } - - // Encode all details in hex before joining together - encryptedString := strings.Join( - []string{ - hex.EncodeToString([]byte{byte(cipherSuite)}), - hex.EncodeToString(cryptoConfig.Nonce[:]), - hex.EncodeToString(encryptedData.Bytes()), - hex.EncodeToString([]byte(reflect.TypeOf(d).Kind().String())), - }, ":", - ) - - if !regexEncryptedString.MatchString(encryptedString) { - return "", fmt.Errorf("could not validate encrypted data") - } - - return encryptedString, nil -} diff --git a/pkg/transcrypt/transcrypt.go b/pkg/transcrypt/transcrypt.go new file mode 100644 index 0000000..0e06ea0 --- /dev/null +++ b/pkg/transcrypt/transcrypt.go @@ -0,0 +1,104 @@ +// Package transcrypt provides functionality to encrypt arbitrary data into a hex encoded string for safe on-disk storage, and decrypt said string. +package transcrypt + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "reflect" + "strings" + + "github.com/minio/sio" +) + +// Encrypt encrypts the supplied data using the supplied secret key and cipher suite. +// It will return an error if either the key is empty or the data is nil. +// Additionally, if the necessary cryptographic configuration cannot be created using the supplied cipherSuite, it will return an error. +// If a salt is provided, it must be at least 12 bytes. +// If salt is nil, the function will automatically create one on-the-fly. +func Encrypt(key string, salt []byte, cipherSuite CipherSuite, d any) (string, error) { + if key == "" { + return "", errors.New("key is empty") + } + + if d == nil { + return "", errors.New("data is nil") + } + + if salt != nil && len(salt) < 12 { + return "", fmt.Errorf("salt needs to be at least 12 bytes, got %d", len(salt)) + } + + var err error + var data string + // Convert input data to reflect.Value before serialization + if data, err = convertValueToHexString(reflect.ValueOf(d)); err != nil { + return "", err + } + + var cryptoConfig sio.Config + if cryptoConfig, err = createCryptoConfig(key, []byte{byte(cipherSuite)}, salt); err != nil { + return "", err + } + + encryptedData := bytes.NewBuffer(make([]byte, 0)) + if _, err = sio.Encrypt(encryptedData, bytes.NewBuffer([]byte(data)), cryptoConfig); err != nil { + return "", err + } + + // Encode all details in hex before joining together + encryptedString := strings.Join( + []string{ + hex.EncodeToString([]byte{byte(cipherSuite)}), + hex.EncodeToString(cryptoConfig.Nonce[:]), + hex.EncodeToString(encryptedData.Bytes()), + hex.EncodeToString([]byte(reflect.TypeOf(d).Kind().String())), + }, ":", + ) + + if !regexEncryptedString.MatchString(encryptedString) { + return "", fmt.Errorf("could not validate encrypted data") + } + + return encryptedString, nil +} + +// Decrypt decrypts a supplied hex-encoded data string using the supplied secret key. +// It will return an error if either the key or the data is empty. +// If the hex-encoded string data cannot be converted into proper encrypted data, decryption will also fail with an error. +func Decrypt(key string, data string) (any, error) { + if key == "" { + return nil, errors.New("key is empty") + } + if data == "" { + return nil, errors.New("data is empty") + } + + var err error + var encryptedData []byte + var kind reflect.Kind + var cryptoConfig sio.Config + + if encryptedData, kind, cryptoConfig, err = decodeHexString(key, data); err != nil { + return nil, err + } + + var decryptedHexData *bytes.Buffer + decryptedHexData = bytes.NewBuffer(make([]byte, 0)) + if _, err = sio.Decrypt(decryptedHexData, bytes.NewBuffer(encryptedData), cryptoConfig); err != nil { + return nil, fmt.Errorf("decrypt failed: %w", err) + } + + var decryptedData []byte + if decryptedData, err = hex.DecodeString(string(decryptedHexData.Bytes())); err != nil { + return nil, fmt.Errorf("decode decrypted hex data failed: %w", err) + } + + var outputValue reflect.Value + if outputValue, err = convertBytesToValue(decryptedData, kind); err != nil { + return nil, err + } + + return outputValue.Interface(), nil +} From e5f0823dbaf4f751384d1b4f841e7af5de580068 Mon Sep 17 00:00:00 2001 From: Jan Tytgat Date: Mon, 13 Jan 2025 16:22:48 +0100 Subject: [PATCH 08/11] Update crypto.go - Make CreateSalt() public - Add CreateHexKey() to create a random hex-encoded key Signed-off-by: Jan Tytgat --- pkg/transcrypt/crypto.go | 48 +++++++++++++----- pkg/transcrypt/crypto_test.go | 93 +++++++++++++++++++++++++++-------- 2 files changed, 109 insertions(+), 32 deletions(-) diff --git a/pkg/transcrypt/crypto.go b/pkg/transcrypt/crypto.go index 7de4cbd..bde6b69 100644 --- a/pkg/transcrypt/crypto.go +++ b/pkg/transcrypt/crypto.go @@ -2,7 +2,11 @@ package transcrypt import ( "crypto/rand" + "crypto/rsa" "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/pem" "errors" "fmt" "io" @@ -12,6 +16,38 @@ import ( "golang.org/x/crypto/hkdf" ) +// CreateHexKey generates a random key which can be used for encryption. +// It generates a RSA Private Key with the supplied bitSize, and converts it to a hex-encoded PEM Block. +func CreateHexKey(bitSize int) (string, error) { + if bitSize < 12 { + return "", errors.New("bit size must be at least 12") + } + var err error + var privKey *rsa.PrivateKey + + var reader = rand.Reader + + if privKey, err = rsa.GenerateKey(reader, bitSize); err != nil { + return "", err + } + + return hex.EncodeToString(pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privKey), + })), nil +} + +// CreateSalt creates a random 12-byte salt for use with the encrypt/decrypt functionality. +func CreateSalt() ([]byte, error) { + var nonce [12]byte + if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { + return nil, fmt.Errorf("failed to read random data for nonce: %w", err) + } + + return nonce[:], nil +} + // createCryptoConfig creates a sio.config from the supplied key, cipher and optional salt. // It returns an error if either key or cipher is empty. // It also returns an error if the supplied salt is less than 12 bytes long. @@ -27,7 +63,7 @@ func createCryptoConfig(key string, cipher []byte, salt []byte) (sio.Config, err var err error // If salt is nil, create a new salt that can be used for encryption if salt == nil { - if salt, err = createSalt(); err != nil { + if salt, err = CreateSalt(); err != nil { return sio.Config{}, fmt.Errorf("could not create salt: %w", err) } } @@ -50,16 +86,6 @@ func createCryptoConfig(key string, cipher []byte, salt []byte) (sio.Config, err }, nil } -// createSalt creates a random salt for use with the encrypt/decrypt functionality. -func createSalt() ([]byte, error) { - var nonce [12]byte - if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { - return nil, fmt.Errorf("failed to read random data for nonce: %w", err) - } - - return nonce[:], nil -} - // getKindFromString converts a string to its representative reflect.Kind. // It returns a reflect.Invalid by default if the supplied string cannot be found. func getKindForString(s string) reflect.Kind { diff --git a/pkg/transcrypt/crypto_test.go b/pkg/transcrypt/crypto_test.go index 87af8ea..d6fbb2c 100644 --- a/pkg/transcrypt/crypto_test.go +++ b/pkg/transcrypt/crypto_test.go @@ -5,6 +5,78 @@ import ( "testing" ) +func Test_CreateHexKey(t *testing.T) { + type args struct { + bitSize int + } + tests := []struct { + name string + bitSize int + wantErr bool + }{ + { + name: "invalid_size_0", + bitSize: 0, + wantErr: true, + }, + { + name: "invalid_size_11", + bitSize: 11, + wantErr: true, + }, + { + name: "valid_size_12", + bitSize: 12, + wantErr: false, + }, + { + name: "valid_size_256", + bitSize: 256, + wantErr: false, + }, + { + name: "valid_size_1024", + bitSize: 1024, + wantErr: false, + }, + { + name: "valid_size_2048", + bitSize: 2048, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := CreateHexKey(tt.bitSize) + if (err != nil) != tt.wantErr { + t.Errorf("CreateHexKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func Test_CreateSalt(t *testing.T) { + tests := []struct { + name string + wantErr bool + }{ + { + name: "success", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := CreateSalt() + if (err != nil) != tt.wantErr { + t.Errorf("CreateSalt() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + func Test_createCryptoConfig(t *testing.T) { type args struct { key string @@ -64,27 +136,6 @@ func Test_createCryptoConfig(t *testing.T) { } } -func Test_createSalt(t *testing.T) { - tests := []struct { - name string - wantErr bool - }{ - { - name: "succes", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := createSalt() - if (err != nil) != tt.wantErr { - t.Errorf("createSalt() error = %v, wantErr %v", err, tt.wantErr) - return - } - }) - } -} - func Test_getKindForString(t *testing.T) { tests := []struct { name string From 229400421ab49a505326640be9a0a54533604e6c Mon Sep 17 00:00:00 2001 From: Jan Tytgat Date: Mon, 13 Jan 2025 16:24:30 +0100 Subject: [PATCH 09/11] Add example Signed-off-by: Jan Tytgat --- examples/encrypt/main.go | 53 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 examples/encrypt/main.go diff --git a/examples/encrypt/main.go b/examples/encrypt/main.go new file mode 100644 index 0000000..10b9266 --- /dev/null +++ b/examples/encrypt/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + + "github.com/jantytgat/go-transcrypt/pkg/transcrypt" +) + +func main() { + var err error + var key string + if key, err = transcrypt.CreateHexKey(1024); err != nil { + panic(err) + } + + var salt []byte + // Uncomment the following lines if you want to use a pre-defined salt + // if salt, err = transcrypt.CreateSalt(); err != nil { + // panic(err) + // } + + fmt.Println("Key: ", key) + fmt.Println("###############") + + var inputString = "hello world" + fmt.Println("input:", inputString) + var encryptedString string + if encryptedString, err = transcrypt.Encrypt(key, salt, transcrypt.AES_256_GCM, inputString); err != nil { + panic(err) + } + fmt.Println("Encrypted:", encryptedString) + + var decryptedString any + if decryptedString, err = transcrypt.Decrypt(key, encryptedString); err != nil { + panic(err) + } + fmt.Println("Decrypted:", decryptedString) + fmt.Println("###############") + + var inputInt = 123456 + fmt.Println("input:", inputInt) + var encryptedInt string + if encryptedInt, err = transcrypt.Encrypt(key, salt, transcrypt.AES_256_GCM, inputInt); err != nil { + panic(err) + } + fmt.Println("Encrypted:", encryptedInt) + + var decryptedInt any + if decryptedInt, err = transcrypt.Decrypt(key, encryptedInt); err != nil { + panic(err) + } + fmt.Println("Decrypted:", decryptedInt) +} From 125953366b49dce52da3fe439970df494e3d85e4 Mon Sep 17 00:00:00 2001 From: Jan Tytgat Date: Mon, 13 Jan 2025 16:48:08 +0100 Subject: [PATCH 10/11] transcrypt.go - Add documentation - Add tests Signed-off-by: Jan Tytgat --- pkg/transcrypt/transcrypt.go | 78 +++++++------- pkg/transcrypt/transcrypt_test.go | 166 ++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 39 deletions(-) create mode 100644 pkg/transcrypt/transcrypt_test.go diff --git a/pkg/transcrypt/transcrypt.go b/pkg/transcrypt/transcrypt.go index 0e06ea0..79b1c48 100644 --- a/pkg/transcrypt/transcrypt.go +++ b/pkg/transcrypt/transcrypt.go @@ -12,6 +12,45 @@ import ( "github.com/minio/sio" ) +// Decrypt decrypts a supplied hex-encoded data string using the supplied secret key. +// It will return an error if either the key or the data is empty. +// If the hex-encoded string data cannot be converted into proper encrypted data, decryption will also fail with an error. +func Decrypt(key string, data string) (any, error) { + if key == "" { + return nil, errors.New("key is empty") + } + if data == "" { + return nil, errors.New("data is empty") + } + + var err error + var encryptedData []byte + var kind reflect.Kind + var cryptoConfig sio.Config + + if encryptedData, kind, cryptoConfig, err = decodeHexString(key, data); err != nil { + return nil, err + } + + var decryptedHexData *bytes.Buffer + decryptedHexData = bytes.NewBuffer(make([]byte, 0)) + if _, err = sio.Decrypt(decryptedHexData, bytes.NewBuffer(encryptedData), cryptoConfig); err != nil { + return nil, fmt.Errorf("decrypt failed: %w", err) + } + + var decryptedData []byte + if decryptedData, err = hex.DecodeString(string(decryptedHexData.Bytes())); err != nil { + return nil, fmt.Errorf("decode decrypted hex data failed: %w", err) + } + + var outputValue reflect.Value + if outputValue, err = convertBytesToValue(decryptedData, kind); err != nil { + return nil, err + } + + return outputValue.Interface(), nil +} + // Encrypt encrypts the supplied data using the supplied secret key and cipher suite. // It will return an error if either the key is empty or the data is nil. // Additionally, if the necessary cryptographic configuration cannot be created using the supplied cipherSuite, it will return an error. @@ -63,42 +102,3 @@ func Encrypt(key string, salt []byte, cipherSuite CipherSuite, d any) (string, e return encryptedString, nil } - -// Decrypt decrypts a supplied hex-encoded data string using the supplied secret key. -// It will return an error if either the key or the data is empty. -// If the hex-encoded string data cannot be converted into proper encrypted data, decryption will also fail with an error. -func Decrypt(key string, data string) (any, error) { - if key == "" { - return nil, errors.New("key is empty") - } - if data == "" { - return nil, errors.New("data is empty") - } - - var err error - var encryptedData []byte - var kind reflect.Kind - var cryptoConfig sio.Config - - if encryptedData, kind, cryptoConfig, err = decodeHexString(key, data); err != nil { - return nil, err - } - - var decryptedHexData *bytes.Buffer - decryptedHexData = bytes.NewBuffer(make([]byte, 0)) - if _, err = sio.Decrypt(decryptedHexData, bytes.NewBuffer(encryptedData), cryptoConfig); err != nil { - return nil, fmt.Errorf("decrypt failed: %w", err) - } - - var decryptedData []byte - if decryptedData, err = hex.DecodeString(string(decryptedHexData.Bytes())); err != nil { - return nil, fmt.Errorf("decode decrypted hex data failed: %w", err) - } - - var outputValue reflect.Value - if outputValue, err = convertBytesToValue(decryptedData, kind); err != nil { - return nil, err - } - - return outputValue.Interface(), nil -} diff --git a/pkg/transcrypt/transcrypt_test.go b/pkg/transcrypt/transcrypt_test.go new file mode 100644 index 0000000..2ac06a2 --- /dev/null +++ b/pkg/transcrypt/transcrypt_test.go @@ -0,0 +1,166 @@ +package transcrypt + +import ( + "fmt" + "reflect" + "testing" +) + +func TestDecrypt(t *testing.T) { + type args struct { + key string + data string + } + tests := []struct { + name string + args args + want any + wantErr bool + }{ + { + name: "empty_key", + args: args{ + key: "", + data: "", + }, + want: nil, + wantErr: true, + }, + { + name: "empty_data", + args: args{ + key: "key", + data: "", + }, + want: nil, + wantErr: true, + }, + { + name: "invalid_data", + args: args{ + key: "key", + data: "invalid_data", + }, + want: "hello world", + wantErr: true, + }, + { + name: "invalid_key", + args: args{ + key: "key", + data: "00:5a412cac418ecf54f86c0da4:20001500da412cac418ecf54f86c0da472bb69380c4abb66a0f8542e4b147d01fa503589bb4e3a37c2e2f979d4721da17397089d1477:737472696e67", + }, + want: "hello world", + wantErr: true, + }, + { + name: "valid_string", + args: args{ + key: "2d2d2d2d2d424547494e205253412050524956415445204b45592d2d2d2d2d0a4d423843415141434167773341674d42414145434167635a41674537416745314167455441674578416745780a2d2d2d2d2d454e44205253412050524956415445204b45592d2d2d2d2d0a", + data: "00:5a412cac418ecf54f86c0da4:20001500da412cac418ecf54f86c0da472bb69380c4abb66a0f8542e4b147d01fa503589bb4e3a37c2e2f979d4721da17397089d1477:737472696e67", + }, + want: "hello world", + wantErr: false, + }, + { + name: "valid_int", + args: args{ + key: "2d2d2d2d2d424547494e205253412050524956415445204b45592d2d2d2d2d0a4d423843415141434167773341674d42414145434167635a41674537416745314167455441674578416745780a2d2d2d2d2d454e44205253412050524956415445204b45592d2d2d2d2d0a", + data: "00:41ce7b530435c9189a203937:20000f00c1ce7b530435c9189a20393717bf895ce6d904a75640a6de8d2e33ab3c2fb3751e2825e9f6f2b23f5bf4df12:696e74", + }, + want: 123456, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Decrypt(tt.args.key, tt.args.data) + if err != nil { + if (err != nil) != tt.wantErr { + t.Errorf("Decrypt() error = %v, wantErr %v", err, tt.wantErr) + return + } + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Decrypt() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEncrypt(t *testing.T) { + type args struct { + key string + salt []byte + cipherSuite CipherSuite + d any + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "empty_key", + args: args{ + key: "", + salt: nil, + cipherSuite: AES_256_GCM, + d: nil, + }, + want: "", + wantErr: true, + }, + { + name: "empty_data", + args: args{ + key: "key", + salt: nil, + cipherSuite: AES_256_GCM, + d: nil, + }, + want: "", + wantErr: true, + }, + { + name: "invalid_salt", + args: args{ + key: "key", + salt: []byte("invalid"), + cipherSuite: AES_256_GCM, + d: "data", + }, + want: "", + wantErr: true, + }, + { + name: "valid", + args: args{ + key: "2d2d2d2d2d424547494e205253412050524956415445204b45592d2d2d2d2d0a4d423843415141434167773341674d42414145434167635a41674537416745314167455441674578416745780a2d2d2d2d2d454e44205253412050524956415445204b45592d2d2d2d2d0a", + salt: []byte("saltsaltsalt"), + cipherSuite: AES_256_GCM, + d: "hello world", + }, + want: "00:73616c7473616c7473616c74:20001500f3616c7473616c7473616c74da182aeef9d1060ec5564b974689147f32bf626db98a13a0f4f6adf6df675dd07fa1463e3d1e:737472696e67", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Encrypt(tt.args.key, tt.args.salt, tt.args.cipherSuite, tt.args.d) + if err != nil { + if (err != nil) != tt.wantErr { + t.Errorf("Encrypt() error = %v, wantErr %v", err, tt.wantErr) + return + } + fmt.Println(err) + return + } + if got != tt.want { + t.Errorf("Encrypt() got = %v, want %v", got, tt.want) + } + }) + } +} From 15a8520f6c22c982f483bc582a6f5c9c3882182a Mon Sep 17 00:00:00 2001 From: Jan Tytgat Date: Mon, 13 Jan 2025 16:48:21 +0100 Subject: [PATCH 11/11] Update example Signed-off-by: Jan Tytgat --- examples/encrypt/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/encrypt/main.go b/examples/encrypt/main.go index 10b9266..25214c8 100644 --- a/examples/encrypt/main.go +++ b/examples/encrypt/main.go @@ -9,7 +9,7 @@ import ( func main() { var err error var key string - if key, err = transcrypt.CreateHexKey(1024); err != nil { + if key, err = transcrypt.CreateHexKey(12); err != nil { panic(err) }