Add Encrypt() and Decrypt() functionality

This commit is contained in:
Jan Tytgat
2024-10-25 17:11:27 +02:00
parent 7821020da7
commit df500c6f1c
8 changed files with 413 additions and 0 deletions

76
.github/workflows/codeql.yml vendored Normal file
View File

@ -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}}"

6
go.mod
View File

@ -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
)

10
go.sum Normal file
View File

@ -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=

View File

@ -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
}
}

87
pkg/transcrypt/convert.go Normal file
View File

@ -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
}

113
pkg/transcrypt/crypto.go Normal file
View File

@ -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
}
}

47
pkg/transcrypt/decrypt.go Normal file
View File

@ -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
}

55
pkg/transcrypt/encrypt.go Normal file
View File

@ -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
}