Merge pull request #1 from jantytgat/0.1.1-dev.20241024

0.1.1 dev.20241024
This commit is contained in:
Jan Tytgat
2025-01-13 16:50:35 +01:00
committed by GitHub
13 changed files with 1312 additions and 1 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}}"

53
examples/encrypt/main.go Normal file
View File

@ -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(12); 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)
}

9
go.mod
View File

@ -1,3 +1,10 @@
module github.com/jantytgat/go-transcrypt
go 1.23.2
go 1.23
require (
github.com/minio/sio v0.4.1
golang.org/x/crypto v0.32.0
)
require golang.org/x/sys v0.29.0 // indirect

8
go.sum Normal file
View File

@ -0,0 +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.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=

View File

@ -0,0 +1,23 @@
package transcrypt
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":
return AES_256_GCM
case "CHACHA20_POLY1305":
return CHACHA20_POLY1305
default:
return CHACHA20_POLY1305
}
}

View File

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

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

@ -0,0 +1,110 @@
package transcrypt
import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"reflect"
"regexp"
"strings"
"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:
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
case reflect.String:
return hex.EncodeToString([]byte(v.String())), nil
default:
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")
}
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 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
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, salt); err != nil {
return nil, reflect.Invalid, sio.Config{}, fmt.Errorf("cannot create crypto config: %w", err)
}
return encryptedBytes, kind, cryptoConfig, nil
}

View File

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

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

@ -0,0 +1,148 @@
package transcrypt
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"io"
"reflect"
"github.com/minio/sio"
"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.
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)
}
}
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
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
}
// 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":
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
}
}

View File

@ -0,0 +1,288 @@
package transcrypt
import (
"reflect"
"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
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_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)
}
})
}
}

View File

@ -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"
)
// 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.
// 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
}

View File

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