Skip to content

Commit

Permalink
Merge pull request #1 from saylorsolutions/feature/xorgen
Browse files Browse the repository at this point in the history
Command xorgen
  • Loading branch information
drognisep authored Jul 23, 2023
2 parents 64839d8 + eff760b commit 4fb803f
Show file tree
Hide file tree
Showing 9 changed files with 459 additions and 2 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
This repo has a few helpful crypto utilities that I've used or plan to use.

---
## Note
**Note:**

Use at your own risk!
While I've done my best to ensure that these functions are working as intended, I don't recommend using any of this for anything that could risk life, limb, property, or any serious material harm without extensive security review.
While I've done my best to ensure that this functionality is working as intended, I don't recommend using any of this for anything that could risk life, limb, property, or any serious material harm without extensive security review.

If you suspect that a security issue exists, please notify me by creating an issue here on GitHub, and I will address it as soon as possible.
If you are a security professional, I can always use another set of eyes to verify the security and integrity of these utilities.
Expand All @@ -14,3 +15,9 @@ When the time comes where I am no longer maintaining this repository, either by
If this is the case - and even before then - feel free to fork this repository and enhance as you see fit.

---

## Packages
* **xor:** Provides some utilities for XOR screening, including an io.Reader and io.Writer implementation that screens in flight.

## Applications
* **xorgen:** Provides a CLI that can be used with go:generate comments to easily embed XOR screened and compressed files.
58 changes: 58 additions & 0 deletions cmd/xorgen/internal/tmpl/screen_embed.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Code generated by xorgen, DO NOT EDIT.
package {{.Package}}

import (
"bytes"
{{- if .Compressed }}
"compress/gzip"
"github.com/saylorsolutions/gocryptx/pkg/xor"
"io"
{{- else }}
"github.com/saylorsolutions/gocryptx/pkg/xor"
{{- end }}
)

var (
key{{.FileMethodName}} = {{ .KeyString }}
data{{.FileMethodName}} = {{ .DataString }}
offset{{.FileMethodName}} = {{ .Offset }}
)

func {{if .Exposed}}U{{else}}u{{end}}nscreen{{.FileMethodName}}() ([]byte, error) {
r, err := xor.NewReader(bytes.NewReader(data{{.FileMethodName}}), key{{.FileMethodName}}, offset{{.FileMethodName}})
if err != nil {
return nil, err
}
{{- if .Compressed }}
uncompress, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
var out bytes.Buffer
_, err = io.Copy(&out, uncompress)
if err != nil {
return nil, err
}
uncompress.Close()
return out.Bytes(), nil
{{- else }}
out := make([]byte, len(data))
_, err = r.Read(out)
if err != nil {
return nil, err
}
return out, nil
{{- end }}
}

func {{if .Exposed}}S{{else}}s{{end}}tream{{.FileMethodName}}() (io.Reader, error) {
{{- if .Compressed }}
r, err := xor.NewReader(bytes.NewReader(data{{.FileMethodName}}), key{{.FileMethodName}}, offset{{.FileMethodName}})
if err != nil {
return nil, err
}
return gzip.NewReader(r)
{{- else }}
return xor.NewReader(bytes.NewReader(data), key, offset)
{{- end }}
}
221 changes: 221 additions & 0 deletions cmd/xorgen/internal/tmpl/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package tmpl

import (
"bytes"
"compress/gzip"
_ "embed"
"fmt"
"github.com/saylorsolutions/gocryptx/pkg/xor"
"io"
"os"
"path/filepath"
"regexp"
"text/template"
"unicode"
)

const (
idealMinKeyLen = 20
)

var (
//go:embed screen_embed.go.tmpl
tmplText string
tmplTemplate = template.Must(template.New("template").Parse(tmplText))
)

type Params struct {
Package string
Exposed bool
Compressed bool
FileMethodName string
KeyString string
DataString string
Offset int

keyData []byte
fileData []byte
targetFileName string
}

// ParamOpt operates on Params in a standard and predictable way, and is used in GenerateFile.
// If any ParamOpt returns an error, then file generation ceases and the error is returned.
type ParamOpt = func(params *Params) error

// CompressData indicates that data should be compressed.
func CompressData(val ...bool) ParamOpt {
return func(params *Params) error {
if len(val) > 0 {
params.Compressed = val[0]
return nil
}
params.Compressed = true
return nil
}
}

// ExposeFunctions indicates that generated functions should be exposed.
func ExposeFunctions(val ...bool) ParamOpt {
return func(params *Params) error {
if len(val) > 0 {
params.Exposed = val[0]
return nil
}
params.Exposed = true
return nil
}
}

// UseKeyOffset sets a key to be used instead of generating one randomly.
func UseKeyOffset(key []byte, offset int) ParamOpt {
return func(params *Params) error {
params.keyData = key
params.Offset = offset
return nil
}
}

// RandomKey generates a random key and offset based on the payload size.
func RandomKey() ParamOpt {
return randomKey
}

// GenerateFile will generate a file embedding the input file with XOR screening.
// Various generation options may be passed as zero or more ParamOpt.
func GenerateFile(input string, opts ...ParamOpt) error {
params := new(Params)
if err := populateContextData(params); err != nil {
return err
}
if err := populateFileData(params, input); err != nil {
return err
}

for _, opt := range opts {
if err := opt(params); err != nil {
return err
}
}

if len(params.keyData) == 0 {
if err := randomKey(params); err != nil {
return err
}
}
if err := screenData(params); err != nil {
return err
}

out, err := os.Create(params.targetFileName + ".go")
if err != nil {
return err
}
defer func() {
_ = out.Close()
}()

if err := tmplTemplate.Execute(out, params); err != nil {
return err
}
return nil
}

func populateContextData(params *Params) error {
cwd, err := os.Getwd()
if err != nil {
return err
}
params.Package = filepath.Base(cwd)
return nil
}

var (
fileCleansePattern = regexp.MustCompile(`[^a-zA-Z0-9_]`)
)

func populateFileData(params *Params, file string) error {
f, err := os.Open(file)
if err != nil {
return err
}
defer func() {
_ = f.Close()
}()

data, err := io.ReadAll(f)
if err != nil {
return err
}
params.fileData = data
_, fname := filepath.Split(file)
params.FileMethodName = fileCleansePattern.ReplaceAllString(unicap(fname), "_")
params.targetFileName = fileCleansePattern.ReplaceAllString(fname, "_")
return nil
}

func randomKey(params *Params) error {
var (
key []byte
offset int
err error
length = len(params.fileData)
)
switch {
case length > 3*idealMinKeyLen:
key, offset, err = xor.GenKeyAndOffset(length / 3)
case length > 2*idealMinKeyLen:
key, offset, err = xor.GenKeyAndOffset(length / 2)
default:
key, offset, err = xor.GenKeyAndOffset(length)
}
if err != nil {
return err
}
params.keyData = key
params.Offset = offset
return nil
}

func screenData(params *Params) error {
var buf bytes.Buffer

if params.Compressed {
var buf bytes.Buffer
w, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
if err != nil {
return err
}
_, err = w.Write(params.fileData)
if err != nil {
return err
}
if err := w.Close(); err != nil {
return err
}
params.fileData = buf.Bytes()
}

w, err := xor.NewWriter(&buf, params.keyData, params.Offset)
if err != nil {
return err
}
_, err = w.Write(params.fileData)
if err != nil {
return err
}
params.KeyString = fmt.Sprintf("%#v", params.keyData)
params.DataString = fmt.Sprintf("%#v", buf.Bytes())
return nil
}

func unicap(s string) string {
runes := []rune(s)
switch len(runes) {
case 0:
return ""
case 1:
return string(unicode.ToUpper(runes[0]))
default:
return string(append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...))
}
}
1 change: 1 addition & 0 deletions cmd/xorgen/internal/tmpl/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A test message that should be screened
41 changes: 41 additions & 0 deletions cmd/xorgen/internal/tmpl/test_txt.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions cmd/xorgen/internal/tmpl/tmpl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//go:generate xorgen -Ec test.txt
package tmpl

import (
"github.com/stretchr/testify/assert"
"io"
"strings"
"testing"
)

func TestUnscreenTest_txt(t *testing.T) {
data, err := UnscreenTest_txt()
assert.NoError(t, err)
assert.Equal(t, "A test message that should be screened", string(data))
}

func TestStreamTest_txt(t *testing.T) {
r, err := StreamTest_txt()
assert.NoError(t, err)
var buf strings.Builder
_, err = io.Copy(&buf, r)
assert.NoError(t, err)
assert.Equal(t, "A test message that should be screened", buf.String())
}
Loading

0 comments on commit 4fb803f

Please sign in to comment.