From df500c6f1cee0becec47888354cf493d866f4060 Mon Sep 17 00:00:00 2001 From: Jan Tytgat Date: Fri, 25 Oct 2024 17:11:27 +0200 Subject: [PATCH] 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 +}