Skip to content

Commit

Permalink
[feat] string obfuscator - for swift
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelhenry committed Mar 18, 2023
1 parent ebe8afb commit d134c25
Show file tree
Hide file tree
Showing 14 changed files with 215 additions and 29 deletions.
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,14 @@ jobs:
with:
go-version-file: go.mod
- run: go test ./parser -coverprofile=coverage.out -v

- name: Test Swift Demo sample project
run: |
cd Examples/swift
go run ../../main.go --file Sources/Demo/Secrets.swift --obfuscate-for=swift
swift test
env:
X_API_KEY: ${{ secrets.X_API_KEY }}

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
9 changes: 9 additions & 0 deletions Examples/swift/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
20 changes: 20 additions & 0 deletions Examples/swift/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// swift-tools-version: 5.7

import PackageDescription

let package = Package(
name: "Demo",
products: [
.library(
name: "Demo",
targets: ["Demo"]),
],
targets: [
.target(
name: "Demo",
dependencies: []),
.testTarget(
name: "DemoTests",
dependencies: ["Demo"]),
]
)
21 changes: 21 additions & 0 deletions Examples/swift/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# swift

A demo project for using envject in a swift project.

## To run the demo project

Please see the [Secrets.swift](Sources/Demo/Secrets.swift) and unit test case [DemoTests](Tests/DemoTests/DemoTests.swift).

### Without string obfuscation

```bash
X_API_KEY="Some secret key" go run ../../main.go --file Sources/Demo/Secrets.swift
swift test
```

### With string obfuscation

```bash
X_API_KEY="Some secret key" go run ../../main.go --file Sources/Demo/Secrets.swift --obfuscate-for=swift
swift test
```
3 changes: 3 additions & 0 deletions Examples/swift/Sources/Demo/Secrets.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
enum Secrets {
static let apiKey = "${X_API_KEY}"
}
8 changes: 8 additions & 0 deletions Examples/swift/Tests/DemoTests/DemoTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import XCTest
@testable import Demo

final class DemoTests: XCTestCase {
func testUnobfuscated() throws {
XCTAssertEqual(Secrets.apiKey, "Some secret key")
}
}
16 changes: 15 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"flag"
"fmt"
"michaelhenry/envject/parser"
"michaelhenry/envject/value_encoders"
"os"
"strings"
)


Expand All @@ -13,6 +15,7 @@ func main() {
sourcePath := flag.String("file", "", "File to inject the environment variables")
outputPath := flag.String("output", "", "The output file. (This creates a new file instead of overriding the original file.)")
ignore := flag.String("ignore", "", "Regex pattern to ignore.")
obfuscateFor := flag.String("obfuscate-for", "", "Obfuscate for particular programming language. (Example: swift)")

flag.Bool("debug", false, "Enable debug mode")
flag.Parse()
Expand All @@ -25,8 +28,19 @@ func main() {
}


var valueEncoder value_encoders.ValueEncoder
switch strings.ToLower(*obfuscateFor) {
case "swift":
valueEncoder = value_encoders.NewSwiftValueEncoder()
// code to be executed when expression equals value1
default:
valueEncoder = &value_encoders.RawValueEncoder{}
// code to be executed when expression does not match any of the cases
}


fileContent := string(fileBytes)
updatedContent := parser.ReplaceEnvVariables(fileContent, *ignore)
updatedContent := parser.ReplaceEnvVariables(fileContent, *ignore, valueEncoder)

// Check if debug flag is true
if flag.Lookup("debug").Value.String() == "true" {
Expand Down
17 changes: 17 additions & 0 deletions obfuscators/obfuscator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package obfuscators

func Obfuscate(str string, salt []byte) []byte {
result := make([]byte, 0, len(str))
for i, char := range []byte(str) {
result = append(result, char^salt[i%len(salt)])
}
return result
}

func Deobfuscate(bytes []byte, salt []byte) (string, error) {
result := make([]byte, 0, len(bytes))
for i, b := range bytes {
result = append(result, b^salt[i%len(salt)])
}
return string(result), nil
}
13 changes: 9 additions & 4 deletions parser/parser.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package parser

import (
"michaelhenry/envject/value_encoders"
"os"
"regexp"
"strings"
)


func ReplaceEnvVariables(input string, ignorePattern string) string {
func ReplaceEnvVariables(input string, ignorePattern string, valueEncoder value_encoders.ValueEncoder) string {
// Handling the following formats:
// - $ENV_NAME
// - ${ENV_NAME}
// - $(ENV_NAME)
envPattern := regexp.MustCompile(`\$(\w+)|\$(\{?(\w+)\})\.?|\$(\(?(\w+)\))\.?`)

// Replace all matches with the corresponding environment variable value
return envPattern.ReplaceAllStringFunc(input, func(match string) string {
modifiedContent := envPattern.ReplaceAllStringFunc(input, func(match string) string {
envName := strings.TrimPrefix(match, "$")
envName = strings.TrimPrefix(envName, "(")
envName = strings.TrimPrefix(envName, "{")
Expand All @@ -30,9 +30,14 @@ func ReplaceEnvVariables(input string, ignorePattern string) string {

envValue, found := os.LookupEnv(envName)
if found {
return envValue
return valueEncoder.Encode(envValue)
} else {
return match // If the variable is not found, leave the original string as-is
}
})

if modifiedContent != input && valueEncoder.AdditionalCode() != "" {
modifiedContent += valueEncoder.AdditionalCode()
}
return modifiedContent
}
62 changes: 38 additions & 24 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package parser

import (
"michaelhenry/envject/obfuscators"
"michaelhenry/envject/value_encoders"
"os"
"testing"
)


func TestVenvInject(t *testing.T){

os.Setenv("KEY1", "This is value 1")
Expand All @@ -25,7 +28,7 @@ func TestVenvInject(t *testing.T){
let key3 = "This is value 3"
}
`
got := ReplaceEnvVariables(configText, "")
got := ReplaceEnvVariables(configText, "", &value_encoders.RawValueEncoder{})

if got != want {
t.Errorf("got %q, wanted %q", got, want)
Expand Down Expand Up @@ -58,7 +61,7 @@ func TestVenvInjectWithInvalidFormatting(t *testing.T){
let key5 = "$(KEY5"
}
`
got := ReplaceEnvVariables(configText, "")
got := ReplaceEnvVariables(configText, "", &value_encoders.RawValueEncoder{})

if got != want {
t.Errorf("got %q, wanted %q", got, want)
Expand All @@ -79,41 +82,52 @@ func TestVenvInjectWithNoSetEnvValues(t *testing.T){
}
`
want := configText
got := ReplaceEnvVariables(configText, "")
got := ReplaceEnvVariables(configText, "", &value_encoders.RawValueEncoder{})

if got != want {
t.Errorf("got %q, wanted %q", got, want)
}
}

func TestVenvInjectWithIgnoreOption(t *testing.T){
func TestVenvInjectSwiftFormat(t *testing.T){

os.Setenv("KEY1", "This is value 1")
os.Setenv("KEY2", "This is value 2")
os.Setenv("KEY3", "This is value 3")
os.Setenv("IGNORE_ME", "This should be ignored")
os.Setenv("IGNORE_THIS", "This should be ignored")
os.Setenv("KEY1", "hello world")
os.Setenv("KEY2", "another secret key")
os.Setenv("KEY3", "321jdsj3244+=&&3298329894ahdha***@3232>>>ad")

configText := `
struct AppConfig {
let key1 = "$KEY1"
let key2 = "${KEY2}"
let key3 = "$(KEY3)"
let key4 = "${IGNORE_ME}"
let key5 = "${IGNORE_THIS}"
}`
enum Secrets {
let key1 = "$KEY1"
let key2 = "$KEY2"
let key3 = "$KEY3"
let key4 = "${IGNORE_ME}"
let key5 = "${IGNORE_THIS}"
}`

want := `
struct AppConfig {
let key1 = "This is value 1"
let key2 = "This is value 2"
let key3 = "This is value 3"
let key4 = "${IGNORE_ME}"
let key5 = "${IGNORE_THIS}"
}`
enum Secrets {
let key1 = "\({ deobfuscate([0x1a, 0x04, 0x02, 0x08, 0x00, 0x4d, 0x28, 0x1c, 0x17, 0x0f, 0x16], salt: salt) }())"
let key2 = "\({ deobfuscate([0x13, 0x0f, 0x01, 0x10, 0x07, 0x08, 0x2d, 0x53, 0x16, 0x06, 0x11, 0x17, 0x11, 0x06, 0x41, 0x05, 0x01, 0x16], salt: salt) }())"
let key3 = "\({ deobfuscate([0x41, 0x53, 0x5f, 0x0e, 0x0b, 0x1e, 0x35, 0x40, 0x57, 0x57, 0x46, 0x4e, 0x49, 0x54, 0x47, 0x5d, 0x56, 0x56, 0x55, 0x6c, 0x41, 0x5c, 0x5b, 0x4b, 0x51, 0x15, 0x1a, 0x05, 0x06, 0x05, 0x45, 0x47, 0x75, 0x33, 0x56, 0x51, 0x41, 0x57, 0x4a, 0x4c, 0x5f, 0x0f, 0x00], salt: salt) }())"
let key4 = "${IGNORE_ME}"
let key5 = "${IGNORE_THIS}"
}
// MARK: - Auto generated by envject (https://github.com/michaelhenry/envject)
private let salt: [UInt8] = [0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74]
private func deobfuscate(_ bytes: [UInt8], salt: [UInt8]) -> String { bytes.enumerated().reduce(into: "") { $0.append(Character(UnicodeScalar($1.element ^ salt[$1.offset % salt.count])))}}`

got := ReplaceEnvVariables(configText, "^IGNORE_")
got := ReplaceEnvVariables(configText, "^IGNORE_", &value_encoders.SwiftValueEncoder{Salt: []byte("random_secret")})

if got != want {
t.Errorf("got %q, wanted %q", got, want)
}
}

func TestDeobfuscate(t *testing.T){
salt := []byte("random_secret")
got, _ := obfuscators.Deobfuscate([]byte{0x1a, 0x04, 0x02, 0x08, 0x00, 0x4d, 0x28, 0x1c, 0x17, 0x0f, 0x16}, salt)
want := "hello world"
if got != want {
t.Errorf("got %q, wanted %q", got, want)
}
}
21 changes: 21 additions & 0 deletions utils/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package utils

import (
"crypto/rand"
"fmt"
"strings"
)

func ConvertBytesToHexString(bytes []byte) string {
hexStrings := make([]string, len(bytes))
for i, b := range bytes {
hexStrings[i] = fmt.Sprintf("0x%02x", b)
}
return strings.Join(hexStrings, ", ")
}

func GenerateRandomBytes(size int) ([]byte, error) {
b := make([]byte, size)
_, err := rand.Read(b)
return b, err
}
11 changes: 11 additions & 0 deletions value_encoders/raw_value_encoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package value_encoders

type RawValueEncoder struct {}

func (v *RawValueEncoder) Encode(value string) string {
return value
}

func (v *RawValueEncoder) AdditionalCode() string {
return ""
}
28 changes: 28 additions & 0 deletions value_encoders/swift_value_encoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package value_encoders

import (
"fmt"
"michaelhenry/envject/obfuscators"
"michaelhenry/envject/utils"
)

type SwiftValueEncoder struct {
Salt []byte
}

func (v *SwiftValueEncoder) Encode(value string) string {
bytes := obfuscators.Obfuscate(value, v.Salt)
return fmt.Sprintf("\\({ deobfuscate([%v], salt: salt) }())", utils.ConvertBytesToHexString(bytes))
}

func (v *SwiftValueEncoder) AdditionalCode() string {
return fmt.Sprintf(`
// MARK: - Auto generated by envject (https://github.com/michaelhenry/envject)
private let salt: [UInt8] = [%s]
private func deobfuscate(_ bytes: [UInt8], salt: [UInt8]) -> String { bytes.enumerated().reduce(into: "") { $0.append(Character(UnicodeScalar($1.element ^ salt[$1.offset %% salt.count])))}}`, utils.ConvertBytesToHexString(v.Salt))
}

func NewSwiftValueEncoder() *SwiftValueEncoder {
bytes, _ := utils.GenerateRandomBytes(16)
return &SwiftValueEncoder{Salt: bytes}
}
6 changes: 6 additions & 0 deletions value_encoders/value_encoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package value_encoders

type ValueEncoder interface {
Encode(value string) string
AdditionalCode() string
}

0 comments on commit d134c25

Please sign in to comment.