From d8773e7349889e1842abc399dd1b5505e0dd21fc Mon Sep 17 00:00:00 2001 From: root Date: Thu, 2 Apr 2026 14:24:29 +0100 Subject: [PATCH] Initial: fixing internal package metadata --- cmd/syscrypt/main.go | 97 + go.mod | 25 + go.sum | 34 + internal/actions/apikey/apikey.go | 1 + internal/actions/decrypt/decrypt.go | 600 ++++++ internal/actions/encrypt/encrypt.go | 829 ++++++++ internal/actions/keygen/keygen.go | 504 +++++ internal/configuration/configuration.go | 58 + internal/utils/salt.go | 141 ++ internal/utils/utils.go | 2391 +++++++++++++++++++++++ internal/vars/vars.go | 108 + syscrypt.go | 47 + 12 files changed, 4835 insertions(+) create mode 100755 cmd/syscrypt/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100755 internal/actions/apikey/apikey.go create mode 100755 internal/actions/decrypt/decrypt.go create mode 100755 internal/actions/encrypt/encrypt.go create mode 100755 internal/actions/keygen/keygen.go create mode 100755 internal/configuration/configuration.go create mode 100755 internal/utils/salt.go create mode 100755 internal/utils/utils.go create mode 100755 internal/vars/vars.go create mode 100755 syscrypt.go diff --git a/cmd/syscrypt/main.go b/cmd/syscrypt/main.go new file mode 100755 index 0000000..e231005 --- /dev/null +++ b/cmd/syscrypt/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "flag" + "fmt" + "io" + "os" + + "sources.truenas.cloud/code/syscrypt/internal/actions/decrypt" + "sources.truenas.cloud/code/syscrypt/internal/actions/encrypt" + "sources.truenas.cloud/code/syscrypt/internal/actions/keygen" + "sources.truenas.cloud/code/syscrypt/internal/config" + "sources.truenas.cloud/code/syscrypt/internal/utils" + "sources.truenas.cloud/code/syscrypt/internal/vars" +) + +func main() { + + fs := flag.NewFlagSet("syscrypt", flag.ContinueOnError) + fs.Usage = func() {} + fs.SetOutput(io.Discard) + + fs.BoolVar(&vars.VersionFlag, "v", false, "prints the version") + fs.BoolVar(&vars.VersionFlag, "version", false, "prints the version") + fs.StringVar(&vars.CommandFlag, "c", "", "command flag") + fs.BoolVar(&vars.ArmoredFlag, "a", false, "generates an armored encrypted file") + fs.StringVar(&vars.KeyFlag, "k", "", "public or private key file") + fs.StringVar(&vars.InputFlag, "i", "", "input file") + fs.StringVar(&vars.OutputFlag, "o", "", "output file") + fs.BoolVar(&vars.LockFlag, "L", false, "lock the encrypted file to API Key and Master Password") + fs.StringVar(&vars.ApiFlag, "A", "", "API key for your locked encrypted file") + fs.StringVar(&vars.MasterPass, "P", "", "Master password value") + fs.StringVar(&vars.Comment, "C", "", "Commend for API Key or Public/Private Keys") + fs.StringVar(&vars.StringFlag, "s", "", "String instead of a file") + fs.BoolVar(&vars.PQFlag, "PQ", false, "sets the keys to post-quantum mode") + fs.StringVar(&vars.KeyPath, "K", "", "key path (optional)") + fs.StringVar(&vars.FriendlyName, "f", "", "Friendly Name") + + _ = fs.Parse(os.Args[1:]) + + if vars.VersionFlag { + fmt.Printf("%s version: %s\nBuild Date: %s\n", config.BuildProject, config.BuildVersion, config.BuildDate) + os.Exit(1) + } + + if err := utils.CheckFirstFlag(); err != nil { + msg := fmt.Sprintf("%s: %s", vars.CommandFlag, err) + utils.HandleFailure(msg) + os.Exit(1) + } + + firstFlag := vars.CommandFlag + + if firstFlag != "firstrun" { + + _, err := utils.CheckExtraFlags() + if err != nil { + msg := fmt.Sprintf("%s: %s", vars.CommandFlag, err) + utils.HandleFailure(msg) + os.Exit(1) + } + + } + + switch vars.CommandFlag { + case "firstrun": + keygen.FirstRun() + case "apikey": + case "dictionary": + case "encrypt": + encrypt.Validate() + case "decrypt": + decrypt.Validate() + case "keygen": + + defaultKey := config.KeyFolder + "/" + config.MasterKey + defaultKeyExists := utils.FileExists(defaultKey) + + fmt.Printf("default key: %s\n", defaultKey) + os.Exit(1) + + if !defaultKeyExists { + msg := fmt.Sprintf("%s: No default keys found. \nPlease run 'syscrypt -c firstrun -PQ' before running any other command.\n"+ + "This will generate your default keys.", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } else { + keygen.Validate() + } + + default: + fmt.Printf("\nUnrecognized command '%s' \n\n", vars.CommandFlag) + fmt.Printf("Allowed commands: firstrun, encrypt, decrypt, encode, decode, keygen, apikeygen, reset, resetpassword\n\n") + os.Exit(1) + } + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ea2527b --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module sources.truenas.cloud/code/syscrypt + +go 1.25.5 + +require github.com/charmbracelet/lipgloss v1.1.0 + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.2 + github.com/cloudflare/circl v1.6.3 + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/crypto v0.49.0 + golang.org/x/sys v0.42.0 // indirect + +) + +replace sources.truenas.cloud/code/syscrypt => ./ diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..59f4905 --- /dev/null +++ b/go.sum @@ -0,0 +1,34 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/internal/actions/apikey/apikey.go b/internal/actions/apikey/apikey.go new file mode 100755 index 0000000..b01f60a --- /dev/null +++ b/internal/actions/apikey/apikey.go @@ -0,0 +1 @@ +package apikey diff --git a/internal/actions/decrypt/decrypt.go b/internal/actions/decrypt/decrypt.go new file mode 100755 index 0000000..369be45 --- /dev/null +++ b/internal/actions/decrypt/decrypt.go @@ -0,0 +1,600 @@ +package decrypt + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "os" + "strings" + + "github.com/cloudflare/circl/kem/kyber/kyber768" + "golang.org/x/crypto/argon2" + cc20 "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/curve25519" + "golang.org/x/crypto/hkdf" + "sources.truenas.cloud/code/syscrypt" + "sources.truenas.cloud/code/syscrypt/internal/utils" + "sources.truenas.cloud/code/syscrypt/internal/vars" +) + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func Validate() { + + utils.ClearTerminal() + + decryptAllowedFlags := map[string]struct{}{ + "-C": {}, + "-k": {}, + "-i": {}, + "-c": {}, + "-o": {}, + "-A": {}, + "-P": {}, + } + + utils.ValidateAllowedFlags(decryptAllowedFlags) + + decryptRequiredFlags := map[string]bool{ + "-k": true, + "-i": true, + } + + _ = decryptRequiredFlags + + //utils.ValidateRequiredFlags(decryptRequiredFlags, "decrypt") + + // -- Keys + + isKeySet, keyHasValue := utils.IsFlagPassed("k") + keyValue, _ := utils.GetFlagValue("k") + keyIsValidPath := utils.IsValidPath(keyValue) + keyExists := utils.FileExists(keyValue) + keyIsValidJson, _ := utils.ValidateJSON(keyValue) + + if isKeySet && !keyHasValue { + msg := fmt.Sprintf("%s: -k KEY: requires a value.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if isKeySet && !keyIsValidPath { + msg := fmt.Sprintf("%s: -k KEY: requires a valid file path.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if isKeySet && !keyExists { + msg := fmt.Sprintf("%s: -k KEY: key file does not exist.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if !keyIsValidJson { + msg := fmt.Sprintf("%s: -k KEY: Invalid JSON format.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + var privateKeyPrefix string + + privateKey, _ := utils.FetchFileKey(keyValue, "key") + privateKeyParts := strings.Split(privateKey, "--") + if len(privateKeyParts) != 2 { + msg := fmt.Sprintf("%s: -k KEY: Mismatch: Invalid Prefix '%s'. Invalid Length.\n", vars.CommandFlag, privateKeyParts[0]+vars.EndAnchor) + utils.HandleFailure(msg) + os.Exit(1) + } + + privateKeyStart := privateKeyParts[0] + privateKeyPrefix = strings.ReplaceAll(vars.PrivateKeyPrefixLabel, "--", "") + + var match bool + if privateKeyStart == privateKeyPrefix { + match = true + } else { + match = false + } + + if !match { + msg := fmt.Sprintf("%s: -k KEY: Mismatch: Invalid Prefix '%s'\n", vars.CommandFlag, privateKeyStart+vars.EndAnchor) + utils.HandleFailure(msg) + os.Exit(1) + } + + // -- Input + + isInputSet, inputHasValue := utils.IsFlagPassed("i") + inputValue, _ := utils.GetFlagValue("i") + inputIsValidPath := utils.IsValidPath(inputValue) + inputExists := utils.FileExists(inputValue) + + if isInputSet && !inputHasValue { + msg := fmt.Sprintf("%s: -i INPUT: Requires a value.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if isInputSet && !inputIsValidPath { + msg := fmt.Sprintf("%s: -i INPUT: Requires a valid file path.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if !inputExists { + msg := fmt.Sprintf("%s: -i INPUT: File does not exist.\n%s", vars.CommandFlag, inputValue) + utils.HandleFailure(msg) + os.Exit(1) + } + + inputFileMode := utils.GetFileMode(inputValue) + + if inputFileMode == 0 { + msg := fmt.Sprintf("%s: -i INPUT: File does not appear to be a valid encrypted file.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + var apiValue string + var apiIsValidPath bool + var apiExists bool + var apiIsValidJson bool + var password string + + isAPISet, apiHasValue := utils.IsFlagPassed("A") + if isAPISet { + apiValue, _ = utils.GetFlagValue("A") + apiIsValidPath = utils.IsValidPath(apiValue) + apiExists = utils.FileExists(apiValue) + apiIsValidJson, _ = utils.ValidateJSON(apiValue) + } + + _ = apiHasValue + _ = apiExists + _ = apiIsValidJson + + var lockValidationRequired bool + + switch inputFileMode { + case 1: // classic - do nothing + lockValidationRequired = false + case 2: // classic locked - apikey and password required + lockValidationRequired = true + + case 3: // hybrid - do nothing + lockValidationRequired = false + case 4: // hybrid locked - apikey and password required + lockValidationRequired = true + default: + lockValidationRequired = false + msg := fmt.Sprintf("%s: -i INPUT: Unable to determine encryption mode. Invalid File.\n%s\n", vars.CommandFlag, inputValue) + utils.HandleFailure(msg) + os.Exit(1) + } + + if isAPISet && lockValidationRequired { + // + + if !isAPISet { + msg := fmt.Sprintf("%s: LOCKED FILE: -A APIKEY: flag is required.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if isAPISet && !apiHasValue { + msg := fmt.Sprintf("%s: LOCKED FILE: -A APIKEY: requires a value.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if isAPISet && !apiIsValidPath { + msg := fmt.Sprintf("%s: LOCKED FILE: -A APIKEY: requires a valid file path.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + password := utils.PromptPassword() + + _ = password + + // + } + + _ = apiIsValidPath + _ = password + + // -- Output + + isOutputSet, outputHasValue := utils.IsFlagPassed("o") + outputValue, _ := utils.GetFlagValue("o") + outputIsValidPath := utils.IsValidPath(outputValue) + outputExists := utils.FileExists(outputValue) + + if isOutputSet && !outputHasValue { + msg := fmt.Sprintf("%s: -o OUTPUT: requires a value.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if isOutputSet && !outputIsValidPath { + msg := fmt.Sprintf("%s: -o OUTPUT: requires a valid file path.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + // confirm overwrite..... + + if isOutputSet && outputExists { + utils.ConfirmOverwrite(outputValue) + } + + // Get Private Key + + privateKey, err := utils.FetchFileKey(keyValue, "key") + if err != nil { + msg := fmt.Sprintf("%s: -k KEY: Error: %s", vars.CommandFlag, err) + utils.HandleFailure(msg) + os.Exit(1) + } + + keyBytes, err := os.ReadFile(keyValue) + if err != nil { + msg := fmt.Sprintf("%s: -k KEY: Could not read key file.\n%s\n", vars.CommandFlag, err) + utils.HandleFailure(msg) + os.Exit(1) + } + + var pKey syscrypt.PrivateKeyWrapper + err = json.Unmarshal(keyBytes, &pKey) + if err != nil { + msg := fmt.Sprintf("%s: -k KEY: File is not a valid syscrypt key file. %v\n", vars.CommandFlag, err) + utils.HandleFailure(msg) + os.Exit(1) + } + + DecryptFile(outputValue, inputValue, pKey) + + _ = err + +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func validateFile(inputValue string) (encryptionType bool, isLocked bool, isArmored bool) { + + //switch mode { + //case 0x01: + // fmt.Printf("classic") + //case 0x02: + // fmt.Printf("classic with lock") + //case 0x03: + // fmt.Printf("post quantum") + //case 0x04: + // fmt.Printf("post quantum with lock") + //default: + // fmt.Printf("invalid") + //} + + return false, false, false + + //file, err := os.Open(inputValue) + //if err != nil { + // fmt.Printf("Error: cannot open %s\n", inputValue) + // os.Exit(1) + //} + //defer file.Close() + + //header := make([]byte, 1) + //_, err = file.Read(header) + //if err != nil { + // fmt.Println("Error: file is empty or unreadable") + // os.Exit(1) + //} + + //file, err := os.Open(filePath) + //if err != nil { + // return false, false, err + //} + //defer file.Close() + + //buffer := make([]byte, 512) + //n, err := file.Read(buffer) + //if err != nil && n == 0 { + // return false, false, err + //} + + //preview := string(buffer[:n]) + //nPreview := strings.ReplaceAll(preview, vars.PrivateKeyHeader+"\n", "") + //nPreview = strings.ReplaceAll(nPreview, vars.PrivateKeyFooter+"\n", "") + //isBase64 := IsValidBase64WithLines(preview) + + //if strings.HasPrefix(preview, vars.PrivateKeyHeader) { + // isArmored = true + //} else { + // isArmored = false + //} + + //mode := header[0] + + //switch mode { + //case 0x01: + // return 1 // Classical Encryption + //case 0x02: + // return 2 // Classical Encryption [with lock] + //case 0x03: + // return 3 // Post Quantum Encryption + //case 0x04: + // return 4 // Post Quantum Encryption [with lock] + //default: + // return 5 // Invalid or Armored + //} + +} + +func DecryptFile(out string, input string, priv syscrypt.PrivateKeyWrapper) { + + outputValue, _ := utils.GetFlagValue("o") + inputValue, _ := utils.GetFlagValue("i") + + isOutputSet, outputHasValue := utils.IsFlagPassed("o") + isOutputValidPath := utils.IsValidPath(outputValue) + outputExists := utils.FileExists(outputValue) + passValue, _ := utils.GetFlagValue("P") + apiValue, _ := utils.GetFlagValue("A") + + raw, _ := os.ReadFile(input) + var blob []byte + + if strings.Contains(string(raw), vars.PrivateKeyHeader) { + c := strings.ReplaceAll(string(raw), vars.PrivateKeyHeader, "") + c = strings.ReplaceAll(c, vars.PrivateKeyFooter, "") + c = strings.ReplaceAll(c, "\n", "") + c = strings.ReplaceAll(c, "\r", "") + blob, _ = base64.StdEncoding.DecodeString(strings.TrimSpace(c)) + } else { + + blob = raw + + } + + // unblind header + + ephPubX := blob[2:34] + mode := blob[0] ^ ephPubX[0] + serialSize := int(blob[1] ^ ephPubX[1]) + + serialOffset := 34 + dataPtr := serialOffset + serialSize + + recoveredSerial := make([]byte, serialSize) + for i := 0; i < serialSize; i++ { + recoveredSerial[i] = blob[serialOffset+i] ^ ephPubX[(i+2)%32] + } + + if priv.PrivateKey.Serial != string(recoveredSerial) { + fmt.Printf("Access Denied: Serial Mismatch or invalid key type.\n") + os.Exit(1) + } + + isHybrid := (mode == 0x03 || mode == 0x04) + isLocked := (mode == 0x02 || mode == 0x04) + + // Password Verification for Locked files + var passKey []byte + + if isLocked { + // + apiKey, err := utils.FetchFileKey(apiValue, "key") + if err != nil { + fmt.Printf("Error: %s", err) + os.Exit(1) + } + passKey = argon2.IDKey([]byte(passValue), []byte(apiKey), 1, 64*1024, 4, 32) + + vH := hkdf.New(sha256.New, passKey, nil, []byte("syscrypt-pass-verify")) + vK := make([]byte, 32) + io.ReadFull(vH, vK) + vAead, _ := cc20.New(vK) + res, err := vAead.Open(nil, make([]byte, 12), blob[dataPtr:dataPtr+vars.PassTagSize], nil) + if err != nil || !bytes.Equal(res, []byte("SYSC-PASS-OK")) { + fmt.Printf("Invalid Master Password.") + os.Exit(1) + } + dataPtr += vars.PassTagSize + // + } + + // Slice Hybrid & Data + + var kyberCT []byte + if isHybrid { + kyberCT = blob[dataPtr : dataPtr+vars.KyberCTSize] + dataPtr += vars.KyberCTSize + } + + nonce := blob[dataPtr : dataPtr+12] + ciphertext := blob[dataPtr+12:] + + // Final Key Reconstruction + + cleanPrivX := strings.TrimPrefix(priv.PrivateKey.Key, vars.PrivateKeyPrefixLabel) + myPrivX, _ := hex.DecodeString(cleanPrivX) + sharedX, _ := curve25519.X25519(myPrivX, ephPubX) + + var sharedML []byte + if isHybrid { + scheme := kyber768.Scheme() + cleanML := strings.TrimPrefix(priv.PrivateKey.MLKEMKey, vars.PQPrivateKeyPrefixLabel) + skBytes, _ := hex.DecodeString(cleanML) + skK, _ := scheme.UnmarshalBinaryPrivateKey(skBytes) + sharedML, _ = scheme.Decapsulate(skK, kyberCT) + } + + combined := append(sharedX, sharedML...) + if isLocked { + combined = append(combined, passKey...) + } + + h := hkdf.New(sha256.New, combined, nil, []byte("syscrypt-v1-hybrid")) + symmK := make([]byte, 32) + io.ReadFull(h, symmK) + aead, _ := cc20.New(symmK) + plaintext, err := aead.Open(nil, nonce, ciphertext, nil) + if err != nil { + fmt.Printf("Decryption failed") + os.Exit(1) + } + + if isOutputSet { + os.WriteFile(outputValue, plaintext, 0644) + fmt.Printf("Successfully decrypted file to: %s\n", outputValue) + } + + if !isOutputSet { + fmt.Printf("%s\n", plaintext) + } + + _ = inputValue + _ = isOutputSet + _ = outputHasValue + _ = isOutputValidPath + _ = outputExists + _ = dataPtr + _ = kyberCT + _ = nonce + _ = ciphertext + _ = sharedX + + //outputValue, _ := utils.GetFlagValue("o") + //inputValue, _ := utils.GetFlagValue("i") + + //isOutputSet, outputHasValue := utils.IsFlagPassed("o") + //outputValue, _ := utils.GetFlagValue("k") + //outputIsValidPath := utils.IsValidPath(outValue) + //outputExists := utils.FileExists(outputValue) + + /* + + rawInput, err := os.ReadFile(inputValue) + if err != nil { + fmt.Printf("Error: Unable to read %s\n", inputValue) + os.Exit(1) + } + + var blob []byte + inputStr := string(rawInput) + + if strings.Contains(inputStr, vars.PrivateKeyHeader) { + content := strings.ReplaceAll(inputStr, vars.PrivateKeyHeader, "") + content = strings.ReplaceAll(content, vars.PrivateKeyFooter, "") + content = strings.ReplaceAll(content, "\n", "") + content = strings.ReplaceAll(content, "\r", "") + content = strings.TrimSpace(content) + blob, err = base64.StdEncoding.DecodeString(content) + if err != nil { + fmt.Printf("Error: Malformed Armored Base64 data.\n") + os.Exit(1) + } + + } else { + + blob = rawInput + + } + + if len(blob) < 35 { + fmt.Printf("Error: -i INPUT: File is too small to be a valid syscrypt encrypted message.\n") + os.Exit(1) + } + + mode := blob[0] + serialSize := int(blob[1]) + + ephOffset := 2 + serialSize + headerOffset := ephOffset + 32 + + if len(blob) < headerOffset { + fmt.Printf("Error: Header truncated or invalid serial length.\n") + } + + maskedSerial := blob[2:ephOffset] + ephPubX := blob[ephOffset:headerOffset] + + recoveredBytes := make([]byte, serialSize) + for i := 0; i < serialSize; i++ { + recoveredBytes[i] = maskedSerial[i] ^ ephPubX[i%32] + } + fileSerial := string(recoveredBytes) + + if fileSerial != priv.PrivateKey.Serial { + fmt.Printf("Error: File requires key [%s], but you provided [%s]\n", fileSerial, priv.PrivateKey.Serial) + os.Exit(1) + } + + var kyberCT, nonce, ciphertext []byte + isHybrid := mode == 0x03 || mode == 0x04 + isLocked := mode == 0x02 || mode == 0x04 + + if isHybrid { + kyberCT = blob[headerOffset : headerOffset+1088] + nonce = blob[headerOffset+1088 : headerOffset+1100] + ciphertext = blob[headerOffset+1100:] + } else { + nonce = blob[headerOffset : headerOffset+12] + ciphertext = blob[headerOffset+12:] + } + + cleanPriv := strings.TrimPrefix(priv.PrivateKey.Key, vars.PrivateKeyPrefixLabel) + myPrivX, _ := hex.DecodeString(cleanPriv) + sharedX, _ := curve25519.X25519(myPrivX, ephPubX) + + var sharedML []byte + if isHybrid { + scheme := kyber768.Scheme() + skBytes, _ := hex.DecodeString(priv.PrivateKey.MLKEMKey) + skK, _ := scheme.UnmarshalBinaryPrivateKey(skBytes) + sharedML, _ = scheme.Decapsulate(skK, kyberCT) + } + + combined := append(sharedX, sharedML...) + if isLocked { + apiValue, _ := utils.GetFlagValue("A") + passValue, _ := utils.GetFlagValue("P") + + passKey := argon2.IDKey([]byte(passValue), []byte(apiKey), 1, 64*1024, 4, 32) + combined = append(combined, passKey...) + defer utils.Zeroize(passKey) + } + + h := hkdf.New(sha256.New, combined, nil, []byte("syscrypt-v1-hybrid")) + symmKey := make([]byte, 32) + io.ReadFull(h, symmKey) + defer utils.Zeroize(symmKey) + + aead, _ := cc20.New(symmKey) + plaintext, err := aead.Open(nil, nonce, ciphertext, nil) + if err != nil { + fmt.Printf("Error: Decryption failed. Potentional data corruption or wrong password.") + os.Exit(1) + } + + if isOutputSet && outputIsValidPath { + + err = os.WriteFile(outputValue, plaintext, 0644) + if err != nil { + fmt.Printf("Error: Unable to write decrypted file %s\n", outputValue) + os.Exit(1) + } + + } else { + /// do stuff that isnt output + } + */ + +} diff --git a/internal/actions/encrypt/encrypt.go b/internal/actions/encrypt/encrypt.go new file mode 100755 index 0000000..af1b997 --- /dev/null +++ b/internal/actions/encrypt/encrypt.go @@ -0,0 +1,829 @@ +package encrypt + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "os" + "strings" + + "github.com/cloudflare/circl/kem/kyber/kyber768" + "golang.org/x/crypto/argon2" + cc20 "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/curve25519" + "golang.org/x/crypto/hkdf" + "sources.truenas.cloud/code/syscrypt" + "sources.truenas.cloud/code/syscrypt/internal/utils" + "sources.truenas.cloud/code/syscrypt/internal/vars" +) + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func Validate() { + + encryptAllowedFlags := map[string]struct{}{ + "-C": {}, + "-L": {}, + "-a": {}, + "-k": {}, + "-i": {}, + "-c": {}, + "-o": {}, + "-A": {}, + "-P": {}, + "-PQ": {}, + } + + for _, arg := range os.Args { + if arg == "-L" { + encryptAllowedFlags["-A"] = struct{}{} + encryptAllowedFlags["-P"] = struct{}{} + break + } + } + + utils.ValidateAllowedFlags(encryptAllowedFlags) + + encryptRequiredFlags := map[string]bool{ + "-k": true, + "-i": true, + "-o": true, + } + + for _, arg := range os.Args { + if arg == "-L" { + encryptRequiredFlags["-A"] = true + encryptRequiredFlags["-P"] = true + break + } + } + + //utils.ValidateRequiredFlags(encryptRequiredFlags, "encrypt") + + isKeySet, keyHasValue := utils.IsFlagPassed("k") + keyValue, _ := utils.GetFlagValue("k") + keyIsValidPath := utils.IsValidPath(keyValue) + + isInputSet, inputHasValue := utils.IsFlagPassed("i") + inputValue, _ := utils.GetFlagValue("i") + inputIsValidPath := utils.IsValidPath(inputValue) + + isOutputSet, outputHasValue := utils.IsFlagPassed("o") + outputValue, _ := utils.GetFlagValue("o") + outputIsValidPath := utils.IsValidPath(outputValue) + outputFileExists := utils.FileExists(outputValue) + + isArmoredSet, armoredHasValue := utils.IsFlagPassed("a") + + isLockSet, lockHasValue := utils.IsFlagPassed("L") + lockValue, _ := utils.GetFlagValue("L") + + _ = lockValue + + isAPISet, _ := utils.IsFlagPassed("A") + apiValue, _ := utils.GetFlagValue("A") + apiIsValidPath := utils.IsValidPath(apiValue) + apiFileExists := utils.FileExists(apiValue) + + //isPassSet, _ := utils.IsFlagPassed("P") + //passValue, _ := utils.GetFlagValue("P") + //passIsValidPath := utils.IsValidPath(passValue) + + /////////////////////////////////////////////////////////// + + // -- Key + + if isKeySet && !keyHasValue { + msg := fmt.Sprintf("%s: -k KEY: Requires a value.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if isKeySet && !keyIsValidPath { + msg := fmt.Sprintf("%s: -k KEY: Requires a valid file path.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + keyExists := utils.FileExists(keyValue) + if !keyExists { + msg := fmt.Sprintf("%s: -k KEY: Key file does not exist.\n%s\n", vars.CommandFlag, keyValue) + utils.HandleFailure(msg) + os.Exit(1) + } + + // -- Input + + if isInputSet && !inputHasValue { + msg := fmt.Sprintf("%s: -i INPUT: Requires a value.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if isInputSet && !inputIsValidPath { + msg := fmt.Sprintf("%s: -i INPUT: Requires a valid file path.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + inputExists := utils.FileExists(inputValue) + if !inputExists { + msg := fmt.Sprintf("%s: -i INPUT: Input file does not exist \n%s\n", vars.CommandFlag, inputValue) + utils.HandleFailure(msg) + os.Exit(1) + } + + // -- Output + + if isOutputSet && !outputHasValue { + msg := fmt.Sprintf("%s: -o OUTPUT: Requires a value.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if isOutputSet && !outputIsValidPath { + msg := fmt.Sprintf("%s: -o OUTPUT: Requires a valid file path.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + // -- Armored + + if isArmoredSet && armoredHasValue { + msg := fmt.Sprintf("%s: -a ARMORED: Armored does not support a value.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + // -- Lock + + if isLockSet && lockHasValue { + msg := fmt.Sprintf("%s: -L LOCK: Lock does not support a value.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if isLockSet && !isAPISet { + msg := fmt.Sprintf("%s: -L LOCK: Lock requires the -A APIKEY flag and value to be set.", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + //if isLockSet && !isPassSet { + // msg := fmt.Sprintf("%s: -L LOCK: Lock requires the -P MASTER PASSWORD flag and value to be set.", vars.CommandFlag) + // utils.HandleFailure(msg) + // os.Exit(1) + //} + + // -- API + + if isLockSet && apiValue == "" { + msg := fmt.Sprintf("%s: -A APIKEY: Requires a value.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if isLockSet && !apiIsValidPath { + msg := fmt.Sprintf("%s: -A APIKEY: Requires a valid file path.\n", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if isLockSet && !apiFileExists { + msg := fmt.Sprintf("%s: -A APIKEY: Key file does not exist.\n%s\n", vars.CommandFlag, apiValue) + utils.HandleFailure(msg) + os.Exit(1) + } + + // -- Password + + //if isLockSet && passValue == "" { + // msg := fmt.Sprintf("%s: -P MASTER PASSWORD: Requires a value.\n", vars.CommandFlag) + // utils.HandleFailure(msg) + // os.Exit(1) + //} + + if isLockSet { + //masterPass := config.MasterPass + //masterPassIsValidPath := utils.IsValidPath(masterPass) + + //if masterPass == "" { + // msg := fmt.Sprintf("%s: Unable to determine location of the Master Password file.\nUnable to continue.\n"+ + // "Please run syscrypt -c keygen again to generate the default keys.", vars.CommandFlag) + // utils.HandleFailure(msg) + // os.Exit(1) + //} + + //if !masterPassIsValidPath { + // msg := fmt.Sprintf("%s: Invalid Master Password location.\n%s"+ + // "Please run syscrypt -c keygen again to generate the default keys.", vars.CommandFlag, masterPass) + // utils.HandleFailure(msg) + // os.Exit(1) + //} + } + + //if isLockSet && !passIsValidPath { + // msg := fmt.Sprintf("%s: -P MASTER PASSWORD: Requires a valid file path.\n", vars.CommandFlag) + // utils.HandleFailure(msg) + // os.Exit(1) + //} + + if outputFileExists { + utils.ConfirmOverwrite(outputValue) + } + + /////////////////////////////////////////////////////////// + + fmt.Printf("\n\n") + fmt.Printf("keyset: %v\n", isKeySet) + fmt.Printf("keyvalue: %v\n", keyValue) + fmt.Printf("keyvalidpath: %v\n", keyIsValidPath) + fmt.Printf("\n") + + fmt.Printf("inputset: %v\n", isInputSet) + fmt.Printf("inputvalue: %v\n", inputValue) + fmt.Printf("inputvalidpath: %v\n", inputIsValidPath) + fmt.Printf("\n") + + fmt.Printf("outputset: %v\n", isOutputSet) + fmt.Printf("outputvalue: %v\n", outputValue) + fmt.Printf("outputvalidpath: %v\n", outputIsValidPath) + fmt.Printf("\n") + + // Open Files + + keyBytes, err := os.ReadFile(keyValue) + if err != nil { + fmt.Printf("Error: Could not read key file at %s\n", keyValue) + } + + var pub syscrypt.PublicKeyWrapper + err = json.Unmarshal(keyBytes, &pub) + if err != nil { + fmt.Printf("Error: key file is not a valid syscrypt JSON: %v\n", err) + os.Exit(1) + } + + EncryptFile(outputValue, inputValue, pub) + +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func zeroize(data []byte) { + for i := range data { + data[i] = 0 + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +const PassTagSize = 28 + +// func EncryptFile(dst io.Writer, src io.Reader, pub syscrypt.PublicKeyWrapper) { +func EncryptFile(outputValue string, inputValue string, pub syscrypt.PublicKeyWrapper) { + + // Get Values + + isLockSet, _ := utils.IsFlagPassed("L") + isArmoredSet, _ := utils.IsFlagPassed("a") + keyValue, _ := utils.GetFlagValue("k") + passValue, _ := utils.GetFlagValue("P") + apiIsSet, _ := utils.IsFlagPassed("A") + apiValue, _ := utils.GetFlagValue("A") + serialKey, _ := utils.FetchFileKey(keyValue, "serial") + + src, _ := os.Open(inputValue) + plaintext, _ := io.ReadAll(src) + src.Close() + + // Keys + + cleanPubX := strings.TrimPrefix(pub.PublicKey.Key, vars.DefaultPrefixLabel) + recipientPubX, _ := hex.DecodeString(cleanPubX) + ephPrivX := make([]byte, 32) + io.ReadFull(rand.Reader, ephPrivX) + ephPubX, _ := curve25519.X25519(ephPrivX, curve25519.Basepoint) + sharedX, _ := curve25519.X25519(ephPrivX, recipientPubX) + + var kyberCT []byte + var sharedML []byte + + isHybrid := pub.PublicKey.MLKEMKey != "" + + if isHybrid { + scheme := kyber768.Scheme() + cleanML := strings.TrimPrefix(pub.PublicKey.MLKEMKey, vars.PQPublicKeyPrefixLabel) + pkBytes, err := hex.DecodeString(cleanML) + if err != nil { + fmt.Printf("Hex Decode Error: %v\n", err) + return + } + + pkK, err := scheme.UnmarshalBinaryPublicKey(pkBytes) + if err != nil { + fmt.Printf("Error: kyber key unmarshal fail: %v\n", err) + return + } + + kyberCT, sharedML, err = scheme.Encapsulate(pkK) + if err != nil { + fmt.Printf("Error: Kyber encapsulation failed: %v\n", err) + } + } + + // Password Tag Generation + + var passKey, passVerifyTag []byte + if isLockSet { + apiKey, _ := utils.FetchFileKey(apiValue, "key") + passKey = argon2.IDKey([]byte(passValue), []byte(apiKey), 1, 64*1024, 4, 32) + + vH := hkdf.New(sha256.New, passKey, nil, []byte("syscrypt-pass-verify")) + vKey := make([]byte, 32) + io.ReadFull(vH, vKey) + vAead, _ := cc20.New(vKey) + vNonce := make([]byte, 12) + passVerifyTag = vAead.Seal(nil, vNonce, []byte("SYSC-PASS-OK"), nil) + } + + // Final Symmetric Key + + combined := append(sharedX, sharedML...) + if isLockSet { + combined = append(combined, passKey...) + } + + h := hkdf.New(sha256.New, combined, nil, []byte("syscrypt-v1-hybrid")) + symmKey := make([]byte, 32) + io.ReadFull(h, symmKey) + aead, _ := cc20.New(symmKey) + for i := range symmKey { + symmKey[i] = 0 + } + + nonce := make([]byte, 12) + io.ReadFull(rand.Reader, nonce) + ciphertext := aead.Seal(nil, nonce, plaintext, nil) + + // Blinded Header Assembly + + serialBytes := []byte(serialKey) + serialSize := len(serialBytes) + + var mode byte + if isHybrid && isLockSet { + mode = 0x04 + } else if isHybrid { + mode = 0x03 + } else if isLockSet { + mode = 0x02 + } else { + mode = 0x01 + } + + var finalBlob []byte + finalBlob = append(finalBlob, mode^ephPubX[0]) + finalBlob = append(finalBlob, byte(serialSize)^ephPubX[1]) + finalBlob = append(finalBlob, ephPubX...) + + for i := 0; i < serialSize; i++ { + finalBlob = append(finalBlob, serialBytes[i]^ephPubX[(i+2)%32]) + } + + if isLockSet { + finalBlob = append(finalBlob, passVerifyTag...) + } + + if isHybrid { + finalBlob = append(finalBlob, kyberCT...) + } + + finalBlob = append(finalBlob, nonce...) + finalBlob = append(finalBlob, ciphertext...) + + dst, err := os.Create(outputValue) + if err != nil { + fmt.Printf("Error: Unable to create output file %s", outputValue) + } + defer dst.Close() + + if isArmoredSet { + dst.WriteString(vars.PrivateKeyHeader + "\n") + enc := base64.StdEncoding.EncodeToString(finalBlob) + + for i := 0; i < len(enc); i += 64 { + end := i + 64 + if end > len(enc) { + end = len(enc) + } + dst.WriteString(enc[i:end] + "\n") + } + dst.WriteString(vars.PrivateKeyFooter + "\n") + + } else { + + dst.Write(finalBlob) + + } + + _ = apiIsSet + + fmt.Printf("Success: file saved at: %s\n", outputValue) + + /* + + // 1. Setup Flags + + isLockSet, _ := utils.IsFlagPassed("L") + isAPISet, _ := utils.IsFlagPassed("A") + isArmoredSet, _ := utils.IsFlagPassed("a") + keyValue, _ := utils.GetFlagValue("k") + apiValue, _ := utils.GetFlagValue("A") + passValue, _ := utils.GetFlagValue("P") + + // 2. Read Plaintext + + src, _ := os.Open(inputValue) + plaintext, _ := io.ReadAll(src) + src.Close() + + // 3. Classical Key Exchange (X25519) + + cleanPubX := strings.TrimPrefix(pub.PublicKey.Key, "syscrypt-") + recipientPubX, _ := hex.DecodeString(cleanPubX) + + ephPrivX := make([]byte, 32) + io.ReadFull(rand.Reader, ephPrivX) + ephPubX, _ := curve25519.X25519(ephPrivX, curve25519.Basepoint) + sharedX, _ := curve25519.X25519(ephPrivX, recipientPubX) + + // 4. Post-Quantum Key Exchange (Kyber768) + + //var kyberCT, sharedML []byte + //isHybrid := pub.PublicKey.MLKEMKey != "" + + var kyberCT []byte + var sharedML []byte + isHybrid := pub.PublicKey.MLKEMKey != "" + + if isHybrid { + scheme := kyber768.Scheme() + cleanML := strings.TrimPrefix(pub.PublicKey.MLKEMKey, "syscrypt-pq-") + pkBytes, err := hex.DecodeString(cleanML) + if err != nil { + fmt.Printf("Hex Decode Error: %v\n", err) + return + } + + pkK, err := scheme.UnmarshalBinaryPublicKey(pkBytes) + if err != nil { + fmt.Printf("Error: kyber key unmarshal failed: %v\n", err) + return + } + + var encapErr error + kyberCT, sharedML, encapErr = scheme.Encapsulate(pkK) + if encapErr != nil { + fmt.Printf("Error: Kyber encapsulation failed: %v\n", encapErr) + return + } + + } + + // 5. Entropy Binding + + combined := append(sharedX, sharedML...) + if isLockSet { + var apiKey string + if isAPISet { + apiKey, _ = utils.FetchFileKey(apiValue, "key") + } + passKey := argon2.IDKey([]byte(passValue), []byte(apiKey), 1, 64*1024, 4, 32) + combined = append(combined, passKey...) + } + + // 6. Key Derivation & Encryption + + h := hkdf.New(sha256.New, combined, nil, []byte("syscrypt-v1-hybrid")) + symmetricKey := make([]byte, 32) + io.ReadFull(h, symmetricKey) + + aead, _ := cc20.New(symmetricKey) + for i := range symmetricKey { + symmetricKey[i] = 0 + } + + nonce := make([]byte, aead.NonceSize()) + io.ReadFull(rand.Reader, nonce) + ciphertext := aead.Seal(nil, nonce, plaintext, nil) + + // 7. Metadata/Serial Masking + + serialKey, _ := utils.FetchFileKey(keyValue, "serial") + serialBytes := []byte(serialKey) + serialSize := len(serialBytes) + maskedSerial := make([]byte, serialSize) + for i := 0; i < serialSize; i++ { + maskedSerial[i] = serialBytes[i] ^ ephPubX[i%32] + } + + // 8. Determine Mode + + var mode byte + if isHybrid && isLockSet { + mode = 0x04 + } else if isHybrid { + mode = 0x03 + } else if isLockSet { + mode = 0x02 + } else { + mode = 0x01 + } + + // 9. Final Assembly + + var finalBlob []byte + finalBlob = append(finalBlob, mode) + finalBlob = append(finalBlob, byte(serialSize)) + finalBlob = append(finalBlob, maskedSerial...) + finalBlob = append(finalBlob, ephPubX...) + + if isHybrid { + if len(kyberCT) != 1088 { + fmt.Printf("CRITICAL: kyberCT is %d bytes, expected 1088\n", len(kyberCT)) + return + } + finalBlob = append(finalBlob, kyberCT...) + } + + finalBlob = append(finalBlob, nonce...) + finalBlob = append(finalBlob, ciphertext...) + + // 10. Write output + + dst, _ := os.Create(outputValue) + defer dst.Close() + + if isArmoredSet { + dst.WriteString(vars.PrivateKeyHeader + "\n") + encoded := base64.StdEncoding.EncodeToString(finalBlob) + + for i := 0; i < len(encoded); i += 64 { + end := i + 64 + if end > len(encoded) { + end = len(encoded) + } + dst.WriteString(encoded[i:end] + "\n") + } + dst.WriteString(vars.PrivateKeyFooter + "\n") + + } else { + dst.Write(finalBlob) + } + + fmt.Printf("File created: %s (mode %d, %d bytes)\n", outputValue, mode, len(finalBlob)) + + */ + + /* + + isLockSet, _ := utils.IsFlagPassed("L") + isAPISet, _ := utils.IsFlagPassed("A") + isPassSet, _ := utils.IsFlagPassed("P") + isArmoredSet, _ := utils.IsFlagPassed("a") + ///////// + isKeyPassed, _ := utils.IsFlagPassed("k") + keyValue, _ := utils.GetFlagValue("k") + _ = isKeyPassed + ////////// + + // set values + + apiValue, _ := utils.GetFlagValue("A") + passValue, _ := utils.GetFlagValue("P") + + _ = passValue + + var apiKey string + var pass string + + if isAPISet { + apiKey, _ = utils.FetchFileKey(apiValue, "key") + } + + if isPassSet { + pass = "" + } + + //isInputSet, inputHasValue := utils.IsFlagPassed("i") + //inputValue, _ := utils.GetFlagValue("i") + + src, _ := os.Open(inputValue) + + plaintext, _ := io.ReadAll(src) + + cleanPub := strings.TrimPrefix(pub.PublicKey.Key, "syscrypt") + recipientPubX, _ := hex.DecodeString(cleanPub) + + ephPrivX := make([]byte, 32) + io.ReadFull(rand.Reader, ephPrivX) + ephPubX, _ := curve25519.X25519(ephPrivX, curve25519.Basepoint) + sharedX, _ := curve25519.X25519(ephPrivX, recipientPubX) + + var kyberCT, sharedML []byte + + isHybrid := pub.PublicKey.MLKEMKey != "" + if isHybrid { + scheme := kyber768.Scheme() + pkBytes, _ := hex.DecodeString(pub.PublicKey.MLKEMKey) + pkK, _ := scheme.UnmarshalBinaryPublicKey(pkBytes) + sharedML, kyberCT, _ = scheme.Encapsulate(pkK) + } + + combined := append(sharedX, sharedML...) + if isLockSet { + passKey := argon2.IDKey([]byte(pass), []byte(apiKey), 1, 64*1024, 4, 32) + combined = append(combined, passKey...) + zeroize(passKey) + } + + h := hkdf.New(sha256.New, combined, nil, []byte("syscrypt-v1-hybrid")) + symmetricKey := make([]byte, 32) + io.ReadFull(h, symmetricKey) + zeroize(symmetricKey) + + aead, _ := cc20.New(symmetricKey) + nonce := make([]byte, aead.NonceSize()) + io.ReadFull(rand.Reader, nonce) + ciphertext := aead.Seal(nil, nonce, plaintext, nil) + + var mode byte + + switch { + case isHybrid && isLockSet: + mode = 0x04 // Post-Quantum + Password Protected + case isHybrid: + mode = 0x03 // Post-Quantum only + case isLockSet: + mode = 0x02 // Classical + Password Protected + default: + mode = 0x01 // Classical Only + } + + serialKey, err := utils.FetchFileKey(keyValue, "serial") + if err != nil { + msg := "unable to fetch key serial. Unable to proceed." + utils.HandleFailure(msg) + os.Exit(1) + } + + serialBytes := []byte(serialKey) + serialSize := len(serialBytes) + + maskedSerial := make([]byte, serialSize) + for i := 0; i < serialSize; i++ { + maskedSerial[i] = serialBytes[i] ^ ephPubX[i%32] + } + + var finalBlob []byte + + finalBlob = append(finalBlob, mode) + finalBlob = append(finalBlob, byte(serialSize)) + finalBlob = append(finalBlob, maskedSerial...) + finalBlob = append(finalBlob, ephPubX...) + + if isHybrid { + finalBlob = append(finalBlob, kyberCT...) + } + + finalBlob = append(finalBlob, nonce...) + finalBlob = append(finalBlob, ciphertext...) + + dst, err := os.Create(outputValue) + if err != nil { + fmt.Printf("error: unable to create %s\n", outputValue) + os.Exit(1) + } + defer dst.Close() + + if isArmoredSet { + + dst.WriteString(vars.PrivateKeyHeader + "\n") + encoded := base64.StdEncoding.EncodeToString(finalBlob) + + for i := 0; i < len(encoded); i += 64 { + end := i + 64 + if end > len(encoded) { + end = len(encoded) + } + dst.WriteString(encoded[i:end] + "\n") + } + + dst.WriteString(vars.PrivateKeyFooter + "\n") + + } else { + dst.Write(finalBlob) + } + + fmt.Printf("final blob: %v\n", finalBlob) + fmt.Printf("is hybrid: %v\n", isHybrid) + + _ = ciphertext + _ = isArmoredSet + _ = ephPubX + _ = kyberCT + _ = mode + _ = serialBytes + + ////////////////////////////////////////// + + //serialKey, err := utils.FetchFileKey(keyValue, "serial") + //if err != nil { + // msg := "Unable to fetch key serial. Unable to proceed." + // utils.HandleFailure(msg) + // os.Exit(1) + //} + + //serialBytes := []byte(serialKey) + //serialSize := len(serialBytes) + + //maskedSerial := make([]byte, serialSize) + //for i := 0; i < serialSize; i++ { + // maskedSerial[i] = serialBytes[i] ^ ephPubX[i%32] + //} + + //fmt.Printf("serial: %s\n", serialKey) + //fmt.Printf("masked: %s\n", maskedSerial) + + //os.Exit(1) + + /////////////////////////////////////////// + + //var finalBlob []byte + //finalBlob = append(finalBlob, mode) + //finalBlob = append(finalBlob, ephPubX...) + + //if isHybrid { + // finalBlob = append(finalBlob, kyberCT...) + //} + + //finalBlob = append(finalBlob, nonce...) + //finalBlob = append(finalBlob, ciphertext...) + + //dst, err := os.Create(outputValue) + //if err != nil { + // fmt.Printf("Error: unable to create %s\n", outputValue) + // os.Exit(1) + //} + //defer dst.Close() + + //if isArmoredSet { + // dst.WriteString(vars.PrivateKeyHeader + "\n") + // encoded := base64.StdEncoding.EncodeToString(finalBlob) + + // for i := 0; i < len(encoded); i += 64 { + // end := i + 64 + // if end > len(encoded) { + // end = len(encoded) + // } + // dst.WriteString(encoded[i:end] + "\n") + // } + + // dst.WriteString(vars.PrivateKeyFooter + "\n") + //} else { + + // dst.Write(finalBlob) + //} + + //fmt.Printf("Encryption Complete: %s\n", outputValue) + + //dst, err := os.Create(outputValue) + //if err != nil { + // fmt.Printf("Error: unable to create %s\n", outputValue) + // os.Exit(1) + //} + //defer dst.Close() + + //dst.Write([]byte{mode}) + //dst.Write(ephPubX) + + //switch mode { + //case 0x03, 0x04: + // dst.Write(kyberCT) + //} + + //dst.Write(nonce) + //dst.Write(ciphertext) + + // validate headers + + */ + +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/internal/actions/keygen/keygen.go b/internal/actions/keygen/keygen.go new file mode 100755 index 0000000..8224254 --- /dev/null +++ b/internal/actions/keygen/keygen.go @@ -0,0 +1,504 @@ +package keygen + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "strconv" + "time" + + "sources.truenas.cloud/code/syscrypt/internal/utils" + "sources.truenas.cloud/code/syscrypt/internal/vars" +) + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func FirstRun() { + + defaultFolder := configuration.ConfigKeyFolder + + //fmt.Printf("default folder: %s\n", defaultFolder) + + //fmt.Printf("[DEBUG KEYGEN] Value: %s | Addr: %p\n", config.KeyFolder, &config.KeyFolder) + + //defaultFolder := config.KeyFolder + + //defaultKey := config.KeyFolder + "/" + config.MasterKey + //defaultKeyExists := utils.FileExists(defaultKey) + //defaultFolderExists := utils.FolderExists(defaultFolder) + + //fmt.Printf("CONFIG KEY FOLDER: %s\n", config.KeyFolder) + + _ = defaultFolder + + os.Exit(1) + +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func Validate() { + + keygenAllowedFlags := map[string]struct{}{ + "-C": {}, + "-i": {}, + "-c": {}, + "-PQ": {}, + "-k": {}, + "-f": {}, + } + + utils.ValidateAllowedFlags(keygenAllowedFlags) + + keygenRequiredFlags := []vars.FlagRequirement{ + {Flag: "-f", IsRequired: true}, + } + + // friendly name -f + // keypath -K + // output -o + + utils.ValidateRequiredFlags(keygenRequiredFlags, "keygen") + + // friendlyname + + isFriendlySet, friendlyHasValue := utils.IsFlagPassed("f") + friendlyValue, _ := utils.GetFlagValue("f") + isFriendlyValid := utils.IsAlphaNumeric(friendlyValue) + + if isFriendlySet && !friendlyHasValue { + msg := fmt.Sprintf("%s: -f FRIENDLYNAME: requires a value.\n"+ + "Do not enter spaces or special characters. e.g. 'masterkey'", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if isFriendlySet && friendlyHasValue && !isFriendlyValid { + msg := fmt.Sprintf("%s: -f FRIENDLYNAME: invalid friendly name.\n"+ + "The friendly name requires an alphanumeric value. e.g. 'masterkey'\n"+ + "Do not enter paths, spaces, special characters or file extensions.", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + // check default keys + + // keypath + + isKeyPathSet, keyPathHasValue := utils.IsFlagPassed("k") + keyPathValue, _ := utils.GetFlagValue("k") + keyPathIsValid := utils.IsValidPath(keyPathValue) + keyPathExists := utils.FolderExists(keyPathValue) + + if isKeyPathSet && !keyPathHasValue { + msg := fmt.Sprintf("%s: -k KEYPATH: requires a value.", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if isKeyPathSet && !keyPathIsValid { + msg := fmt.Sprintf("%s: -k KEYPATH: requires a valid folder path.", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + if isKeyPathSet && keyPathIsValid && !keyPathExists { + msg := fmt.Sprintf("%s: -k KEYPATH: folder path '%s' does not exist.", vars.CommandFlag, keyPathValue) + utils.HandleFailure(msg) + os.Exit(1) + } + + var keyFolder string + var defaultSet bool + + if isKeyPathSet { + defaultSet = false + keyFolder = keyPathValue + } else { + defaultSet = true + keyFolder = config.KeyFolder + } + + if defaultSet { + keyPathIsValid = utils.IsValidPath(keyFolder) + } + + if defaultSet && !keyPathIsValid { + msg := fmt.Sprintf("%s: DEFAULT KEYPATH '%s' was not found.", vars.CommandFlag, config.KeyFolder) + utils.HandleFailure(msg) + os.Exit(1) + } + + // comment + + isCommentSet, commentHasValue := utils.IsFlagPassed("C") + if isCommentSet && !commentHasValue { + msg := fmt.Sprintf("%s: -C COMMENT: flag is set but no value was provided.", vars.CommandFlag) + utils.HandleFailure(msg) + os.Exit(1) + } + + fmt.Printf("key folder: %s\n", keyFolder) + + _ = isKeyPathSet + _ = keyPathHasValue + _ = keyPathIsValid + _ = keyPathExists + _ = keyFolder + + // comment + + // output + + KeyGen() + +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func generateRandomSerial() string { + token := make([]byte, 15) + rand.Read(token) + return hex.EncodeToString(token) +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func getCurrentDate() string { + return time.Now().Format("2006-01-02 15:04:05") +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func saveKeyFile(filename string, permission string, data interface{}) { + + fmt.Printf("%v\n", data) + os.Exit(1) + + if permission == "" { + fmt.Printf("error: permission code required.\n") + return + } + + fileData, err := json.MarshalIndent(data, "", " ") + if err != nil { + fmt.Printf("error encoding json: %v\n", err) + return + } + + err = os.WriteFile(filename, fileData, 0600) + if err != nil { + fmt.Printf("Error writing %s: %v\n", filename, err) + return + } + + permUint, _ := strconv.ParseUint(permission, 8, 32) + err = os.Chmod(filename, os.FileMode(permUint)) + if err != nil { + fmt.Printf("Error adjusting permissions on %s: %v\n", filename, err) + return + } + + fmt.Printf("Success: '%s' saved successfully.\n", filename) + +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func KeyGen() { + + outputFolder, _ := utils.GetFlagValue("o") + isPQSet, _ := utils.IsFlagPassed("PQ") + commentValue, _ := utils.GetFlagValue("C") + + if outputFolder == "" { + outputFolder = config.KeyFolder + } + + _ = isPQSet + _ = commentValue + + //fmt.Printf("%s", config.KeyFolder) + + //outputValue, _ := utils.GetFlagValue("o") + //isPQSet, _ := utils.IsFlagPassed("PQ") + //commentValue, _ := utils.GetFlagValue("C") + + //pubWrap := syscrypt.PublicKeyWrapper{} + //privWrap := syscrypt.PrivateKeyWrapper{} + //apiWrap := syscrypt.ApiKeyWrapper{} + /// + //var privX, pubX [32]byte + //io.ReadFull(rand.Reader, privX[:]) + //curve25519.ScalarBaseMult(&pubX, &privX) + + //serial := generateRandomSerial() + //date := getCurrentDate() + + //password := (generatepassword) + + //pubWrap.PublicKey.Key = vars.DefaultPrefixLabel + hex.EncodeToString(pubX[:]) + //pubWrap.PublicKey.Serial = serial + //pubWrap.PublicKey.Date = date + //pubWrap.PublicKey.Comment = commentValue + + //privWrap.PrivateKey.Key = vars.PrivateKeyPrefixLabel + hex.EncodeToString(privX[:]) + //privWrap.PrivateKey.Serial = serial + //privWrap.PrivateKey.Date = date + //privWrap.PrivateKey.Comment = commentValue + + //apiWrap.ApiKey.Key = "" + //apiWrap.ApiKey.Serial = serial + //apiWrap.ApiKey.Date = date + //apiWrap.ApiKey.Comment = commentValue + + /* + type ApiKeyWrapper struct { + ApiKey struct { + Key string `json:"key"` + Serial string `json:"serial"` + Date string `json:"date"` + Comment string `json:"comment"` + } `json:"APIKey"` + } + + */ + + //if isPQSet { + // scheme := kyber768.Scheme() + // pkK, skK, err := scheme.GenerateKeyPair() + // if err != nil { + // fmt.Printf("error: keygen failed: %v\n", err) + // return + // } + + // pkBytes, _ := pkK.MarshalBinary() + // skBytes, _ := skK.MarshalBinary() + + // pubWrap.PublicKey.MLKEMKey = vars.PQPublicKeyPrefixLabel + hex.EncodeToString(pkBytes) + // privWrap.PrivateKey.MLKEMKey = vars.PQPrivateKeyPrefixLabel + hex.EncodeToString(skBytes) + //} + + //pubJSON, err := json.MarshalIndent(pubWrap, "", " ") + //if err != nil { + // fmt.Printf("Marshal Error: %v\n", err) + // return + //} + + //privJSON, err := json.MarshalIndent(privWrap, "", " ") + //if err != nil { + // fmt.Printf("Marshal Error: %v\n", err) + // return + //} + + //MasterFolder := config.KeyFolder + + // Public Key File + + //pubFullPath := outputValue + //pubDir := filepath.Dir(pubFullPath) + //pubBase := filepath.Base(pubFullPath) + //pubExt := filepath.Ext(pubBase) + //pubFileName := strings.TrimSuffix(pubBase, pubExt) + //pubFilePath := MasterFolder + "/" + pubFileName + ".pub" + pubExt + + // API Key File + + //apiFullPath := outputValue + //apiDir := filepath.Dir(apiFullPath) + //apiBase := filepath.Base(apiFullPath) + //apiExt := filepath.Ext(apiBase) + //apiFileName := strings.TrimSuffix(apiBase, apiExt) + //apiFilePath := MasterFolder + "/" + apiFileName + ".api" + apiExt + + // Master Password + /* + passFullPath := outputValue + passDir := filepath.Dir(passFullPath) + passBase := filepath.Base(passFullPath) + passExt := filepath.Ext(passBase) + passFileName := strings.TrimSuffix(passBase, passExt) + passFilePath := MasterFolder + "/" + passFileName + ".secret" + passExt + + fmt.Printf("fullpath: %s\n", passFullPath) + fmt.Printf("dir: %s\n", passDir) + fmt.Printf("base: %s\n", passBase) + fmt.Printf("ext: %s\n", passExt) + fmt.Printf("filename: %s\n", passFileName) + fmt.Printf("path: %s\n", passFilePath) + + */ + + // Save to disk + + //err = os.WriteFile(pubFilePath, pubJSON, 0644) + //if err != nil { + // fmt.Printf("File Error: %v\n", err) + // os.Exit(1) + //} + + //err = os.WriteFile(outputValue, privJSON, 0600) + //if err != nil { + // fmt.Printf("File Error: %v\n", err) + // os.Exit(1) + //} + + //saveKeyFile(outputValue, "600", privJSON) + + //saveKeyFile(filename string, permission string, data interface{}) + + ////pubFullPath := outputValue + ////pubDir := filepath.Dir(pubFullPath) + ////pubBase := filepath.Base(pubFullPath) + ////pubExt := filepath.Ext(pubBase) + ////pubClean := strings.TrimSuffix(pubBase, pubExt) + ////pubFileOut := pubClean + ".pub" + pubExt + ////pubFileOut = pubDir + "/" + pubFileOut + + //_ = outputValue + //_ = commentValue + //_ = pubJSON + //_ = privJSON + //_ = pubDir + //_ = pubFilePath + //_ = apiDir + //_ = apiFilePath + //_ = apiWrap + //_ = passDir + //_ = passFilePath + + //fmt.Printf("%v\n", pkK) + //fmt.Printf("%v\n", skK) + + ////outputValue, _ := utils.GetFlagValue("o") + + ////isPQSet, _ := utils.IsFlagPassed("PQ") + ////commentValue, _ := utils.GetFlagValue("C") + + ////privX := make([]byte, 32) + ////rand.Read(privX) + ////pubX, _ := curve25519.X25519(privX, curve25519.Basepoint) + + ////pubWrap := syscrypt.PublicKeyWrapper{} + ////privWrap := syscrypt.PrivateKeyWrapper{} + ////apiWrap := syscrypt.ApiKeyWrapper{} + + ////_ = apiWrap + /// + ////serial := generateRandomSerial() + ////date := getCurrentDate() + + ////if isPQSet { + //// pubK, privK, _ := kyber768.GenerateKeyPair(rand.Reader) + //// pBytes, _ := pubK.MarshalBinary() + //// sBytes, _ := privK.MarshalBinary() + //// pubWrap.PublicKey.MLKEMKey = vars.PQPublicKeyPrefixLabel + hex.EncodeToString(pBytes) + //// privWrap.PrivateKey.MLKEMKey = vars.PQPrivateKeyPrefixLabel + hex.EncodeToString(sBytes) + ////} + + ////pubWrap.PublicKey.Key = vars.DefaultPrefixLabel + hex.EncodeToString(pubX) + ////pubWrap.PublicKey.Serial = serial + ////pubWrap.PublicKey.Date = date + ////pubWrap.PublicKey.Comment = commentValue + + ////privWrap.PrivateKey.Key = vars.PrivateKeyPrefixLabel + hex.EncodeToString(privX) + ////privWrap.PrivateKey.Serial = serial + ////privWrap.PrivateKey.Date = date + ////privWrap.PrivateKey.Comment = commentValue + + //apiWrap.ApiKey.Key = GenerateAPIKey(xxx, ) + //apiWrap.ApiKey.Serial = serial + //apiWrap.ApiKey.Date = date + //apiWrap.ApiKey.Comment = commentValue + + //type ApiKeyWrapper struct { + // ApiKey struct { + // Key string `json:"key"` + // Serial string `json:"serial"` + // Date string `json:"date"` + // Comment string `json:"comment"` + // } `json:"APIKey"` + //} + + ////fmt.Printf("public key: %s\n", pubWrap.PublicKey.Key) + ////fmt.Printf("private key: %s\n", privWrap.PrivateKey.Key) + ////fmt.Printf("pq: %s\n", pubWrap.PublicKey.MLKEMKey) + + //preview, _ := json.MarshalIndent(privWrap, "", " ") + //fmt.Println(string(preview)) + + ////pubFullPath := outputValue + ////pubDir := filepath.Dir(pubFullPath) + ////pubBase := filepath.Base(pubFullPath) + ////pubExt := filepath.Ext(pubBase) + ////pubClean := strings.TrimSuffix(pubBase, pubExt) + ////pubFileOut := pubClean + ".pub" + pubExt + ////pubFileOut = pubDir + "/" + pubFileOut + + ////apiFullPath := outputValue + ////apiDir := filepath.Dir(apiFullPath) + ////apiBase := filepath.Base(apiFullPath) + ////apiExt := filepath.Ext(apiBase) + ////apiClean := strings.TrimSuffix(apiBase, apiExt) + ////apiFileOut := apiClean + ".api" + apiExt + ////apiFileOut = apiDir + "/" + apiFileOut + + // Generate Master Password if one doesnt already exist + + //mpExists := utils.ReadJson("config.masterpass", config.ConfigPath) + + ////_ = pubDir + ////_ = apiDir + ////_ = apiFileOut + + ////fmt.Printf("public out: %s\n", pubDir+"/"+pubFileOut) + ////fmt.Printf("api out: %s\n", apiDir+"/"+apiFileOut) + + ////saveKeyFile(pubFileOut, "0600", pubWrap) + ////saveKeyFile(outputValue, "0644", privWrap) + //saveKeyFile(xxxx, 0600, xxxxxx) + + //func saveKeyFile(filename string, permission string, data interface{}) { + + //type PublicKeyWrapper struct { + // PublicKey struct { + // Key string `json:"key"` + // MLKEMKey string `json:"mlkem_key,omitempty"` + // Serial string `json:"serial"` + // Date string `json:"date"` + // Comment string `json:"comment"` + // } `json:"PublicKey"` + //} + + //privRaw := make([]byte, 32) + //serial := utils.GenerateSerial(27) + //io.ReadFull(rand.Reader, privRaw) + //pubRaw, _ := curve25519.X25519(privRaw, curve25519.Basepoint) + //dateStr := time.Now().Format("2006-01-02 15:04:05") + + //privStruct := syscrypt.PrivateKeyWrapper{} + //privStruct.PrivateKey.Key = vars.PrivateKeyPrefixLabel + hex.EncodeToString(privRaw) + //privStruct.PrivateKey.Serial = serial + //privStruct.PrivateKey.Date = dateStr + //privStruct.PrivateKey.Comment = vars.Comment + + //pubStruct := syscrypt.PublicKeyWrapper{} + //pubStruct.PublicKey.Key = vars.DefaultPrefixLabel + hex.EncodeToString(pubRaw) + //pubStruct.PublicKey.Serial = serial + //pubStruct.PublicKey.Date = dateStr + //pubStruct.PublicKey.Comment = vars.Comment + + //pBytes, _ := json.MarshalIndent(privStruct, "", " ") + //pubBytes, _ := json.MarshalIndent(pubStruct, "", " ") + + //fmt.Printf("%s\n", pBytes) + //fmt.Printf("%s\n", pubBytes) + //fmt.Printf("%s\n", pubStruct.PublicKey.Serial) + + //utils.GenerateKeys(pubStruct.PublicKey.Key, privStruct.PrivateKey.Key, serial) + +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go new file mode 100755 index 0000000..e3b81a3 --- /dev/null +++ b/internal/configuration/configuration.go @@ -0,0 +1,58 @@ +package configuration + +import ( + "fmt" + "os" +) + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +const ( + + // BUILD DATA + + BuildProject = "syscrypt " + BuildVersion = "1.2.1" + BuildDate = "20260222-183358" + BuildSource = "https://www.github.com/mintsalad/syscrypt" + + // JSON + + ConfigPath = "/opt/syscrypt/config.json" + + // KEYS + + MasterExt = ".d" + MasterKey = ".masterkey.d" + MasterPublicKey = ".masterkey.pub.d" + MasterKeySalt = ".register.d" + MasterAPIKey = ".master.api.d" + Dictionary = ".library.d" + DictionaryTmp = ".library.tmp.txt" + StatePath = ".ratelimit" + LockPath = ".lockout" + + // LOCKOUT SETTINGS + + MaxTokens = 5 + RefillSeconds = 2 + MaxFailures = 3 + LockoutMinutes = 15 +) + +var KeyFolder string + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func init() { + + if _, err := os.Stat(ConfigPath); os.IsNotExist(err) { + fmt.Printf("config file is missing at %s\n", ConfigPath) + } + + //KeyFolder = utils.ReadJSON(".config.keys.Path", ConfigPath) + +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//func ReadJson(path string, filePath string) (string, error) { diff --git a/internal/utils/salt.go b/internal/utils/salt.go new file mode 100755 index 0000000..78f3046 --- /dev/null +++ b/internal/utils/salt.go @@ -0,0 +1,141 @@ +package utils + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "strings" + + "sources.truenas.cloud/code/syscrypt" + "sources.truenas.cloud/code/syscrypt/internal/configuration" + "sources.truenas.cloud/code/syscrypt/internal/vars" +) + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func HashEntry(value, x25519Str string) string { + key := strings.TrimPrefix(x25519Str, vars.PrivateKeyPrefixLabel) + h := hmac.New(sha256.New, []byte(key)) + h.Write([]byte(value)) + + return hex.EncodeToString(h.Sum(nil)) +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func AddSaltEntry(ref, serial, salt, x25519Key string) error { + + var vault syscrypt.Vault + //vaultPath := config.KeyFolder + "/" + config.MasterKeySalt + vaultPath := configuration.KeyFolder + "/" + configuration.MasterKeySalt + + if data, err := os.ReadFile(vaultPath); err == nil { + json.Unmarshal(data, &vault) + } + + for _, e := range vault.Entries { + if e.Ref == ref { + return fmt.Errorf("reference %s already exists", ref) + } + } + + encSalt := salt + + newEntry := syscrypt.SaltEntry{ + Ref: ref, + Salt: encSalt, + } + + vault.Entries = append(vault.Entries, newEntry) + updated, _ := json.MarshalIndent(vault, "", " ") + + return os.WriteFile(vaultPath, updated, 0644) + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func VerifySaltEntry(ref, x25519Key string) (salt string, exists bool, err error) { + //var vault syscrypt.Vault + //vaultPath := config.KeyFolder + "/" + config.MasterKeySalt + + /* + data, err := os.ReadFile(vaultPath) + if err != nil { + return "", false, err + } + json.Unmarshal(data, &vault) + + for _, e := range vault.Entries { + if e.Ref == ref { + // + //decSerial, errS := DecryptString(e.Serial, x25519Key) + decSalt, errSalt := DecryptString(e.Salt, x25519Key) + + if errSalt != nil { + return "", false, fmt.Errorf("failed to decrypt entry") + } + + return decSalt, true, nil + + // + // + } + } + + */ + + //return "", false, fmt.Errorf("reference %s not found", ref) + return "", false, nil + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func EncryptString(plaintext, x25519Key string) (string, error) { + keyBytes := sha256.Sum256([]byte(x25519Key)) + block, _ := aes.NewCipher(keyBytes[:]) + gcm, _ := cipher.NewGCM(block) + + nonce := make([]byte, gcm.NonceSize()) + rand.Read(nonce) + + return base64.StdEncoding.EncodeToString(gcm.Seal(nonce, nonce, []byte(plaintext), nil)), nil +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func DecryptString(cipherText string, x25519Key string) (string, error) { + data, err := base64.StdEncoding.DecodeString(cipherText) + if err != nil { + return "", err + } + + keyBytes := sha256.Sum256([]byte(x25519Key)) + block, _ := aes.NewCipher(keyBytes[:]) + gcm, _ := cipher.NewGCM(block) + nonceSize := gcm.NonceSize() + + nonce, ciphertextBytes := data[:nonceSize], data[nonceSize:] + plain, err := gcm.Open(nil, nonce, ciphertextBytes, nil) + + return string(plain), err + + //keyBytes := sha256.Sum256([]byte(x25519Key)) + //block, _ := aes.NewCipher(keyBytes[:]) + //gcm, _ := cipher.NewGCM(block) + + //nonceSize := gcm.NonceSize() + //nonce, cipher := cipherText[:nonceSize], cipherText[nonceSize:] + //plain, err := gcm.Open(nil, nonce, cipher, nil) + //return string(plain), err +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100755 index 0000000..8eac99e --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,2391 @@ +package utils + +import ( + "bufio" + "bytes" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "math/big" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + "unicode" + "unicode/utf16" + + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/x/term" + "github.com/cloudflare/circl/kem/kyber/kyber768" + "golang.org/x/crypto/argon2" + cc20 "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/curve25519" + "golang.org/x/crypto/hkdf" + "sources.truenas.cloud/code/syscrypt" + "sources.truenas.cloud/code/syscrypt/internal/vars" +) + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +var BypassList = []string{ + //config.MasterPass, +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func IsAlphaNumeric(s string) bool { + + if len(s) == 0 { + return false + } + + for _, r := range s { + if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')) { + return false + } + } + return true +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func IsPublicKey(key string) bool { + + if key != strings.ToLower(key) { + return false + } + + prefix := vars.DefaultPrefixLabel + return strings.HasPrefix(key, prefix) +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func IsPrivateKey(key string) bool { + + prefix := vars.PrivateKeyPrefixLabel + + return strings.HasPrefix(key, prefix) + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func IsValidEncryption(filePath string) bool { + + fileExists := FileExists(filePath) + if !fileExists { + return false + } + + _, err := os.Open(filePath) + if err != nil { + return false + } + + fileRead, err := os.ReadFile(filePath) + if err != nil { + return false + } + + content := string(fileRead) + firstLine, _ := GetFirstLine(filePath) + isBase64 := IsValidBase64WithLines(content) + + if !(firstLine == vars.PrivateKeyHeader || strings.HasPrefix(firstLine, vars.LockVersionLabel) || strings.HasPrefix(firstLine, vars.SyscryptLabel)) { + return false + } + + if firstLine == vars.PrivateKeyHeader && !isBase64 { + return false + } + + return true +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func IsValidPath(path string) bool { + + if path == "" || strings.ContainsRune(path, 0) { + return false + } + + if runtime.GOOS == "windows" { + + badChars := `<>"|?*` + checkPath := path + + if len(path) >= 2 && path[1] == ':' { + checkPath = path[2:] + } + + if strings.ContainsAny(checkPath, badChars) { + return false + } + + } + + hasSeperator := strings.ContainsRune(path, filepath.Separator) + isRelative := strings.HasPrefix(path, "."+string(filepath.Separator)) + hasDrive := runtime.GOOS == "windows" && len(path) >= 2 && path[1] == ':' + + if !hasSeperator && !isRelative && !hasDrive { + return false + } + + return true + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func ArgumentMap(selectedArg string) (result string) { + + arguments := map[string]string{ + "-v": "VERSION", + "-c": "COMMAND", + "-a": "ARMORED", + "-k": "KEY", + "-i": "INPUT", + "-o": "OUTPUT", + "-L": "LOCK", + "-A": "APIKEY", + "-P": "MASTERPASSWORD", + "-C": "COMMENT", + "-f": "FRIENDLYNAME", + "-K": "KEYPATH", + } + + for flag, description := range arguments { + + if flag == selectedArg { + result = description + break + } + + } + return result + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func ValidateAllowedFlags(rules map[string]struct{}) { + args := os.Args[1:] + seen := make(map[string]bool) + + for _, arg := range args { + + if !strings.HasPrefix(arg, "-") { + continue + } + + if seen[arg] { + fmt.Printf(" ERROR: syscrypt: Flag '%s' is used more than once\n", arg) + os.Exit(1) + } + + seen[arg] = true + + if _, exists := rules[arg]; !exists { + fmt.Printf(" ERROR: syscrypt: Unknown or misplaced flag '%s'\n", arg) + os.Exit(1) + } + + } +} + +//input := os.Args[1:] + +//if len(input) == 0 { +// return +//} + +//context := input[0] +//if len(input) > 1 { +// context = input[1] +//} + +//for i := 0; i < len(input); i++ { +// // + +// current := input[i] +// needsValue, exists := rules[current] +// if !exists { +// msg := " ERROR: syscrypt: " + context + ": the flag '" + current + "' is not valid for " + context +// HandleFailure(msg, nil) +// os.Exit(1) +// } + +// if needsValue { +// if i+1 >= len(input) { +// msg := " ERROR: syscrypt: " + context + ": requires a value." +// HandleFailure(msg, nil) +// os.Exit(1) +// } +// i++ +// } + +// // +//} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func CheckFirstFlag() error { + + if len(os.Args) < 2 { + return errors.New("Missing -c COMMAND flag. See usage for details.") + } + + firstArg := os.Args[1] + if firstArg == "-v" || firstArg == "-version" { + return nil + } + + if firstArg != "-c" { + return fmt.Errorf("invalid flag %q: expected -c COMMAND", firstArg) + } + + return nil + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func CleanIntro(inputflag string) string { + cleantext := inputflag + cleantext = strings.ReplaceAll(cleantext, "--", "") + cleantext = strings.ReplaceAll(cleantext, "\n", "") + cleantext = strings.Join(strings.Fields(cleantext), "") + + return cleantext +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +////### + +func SelectIntro() (string, error) { + + var isLocked bool + var isArmored bool + var lockedState string + + isLockSet, _ := IsFlagPassed("L") + isKeySet, _ := IsFlagPassed("k") + isInputSet, _ := IsFlagPassed("i") + + lockValue, _ := GetFlagValue("L") + keyValue, _ := GetFlagValue("k") + inputValue, _ := GetFlagValue("i") + + serialFetch, err := FetchFileKey(keyValue, "serial") + if err != nil { + msg := fmt.Sprintf("unable to parse serial number from key file. %s\n", keyValue) + HandleFailure(msg) + os.Exit(1) + } + + switch vars.CommandFlag { + case "encrypt": + isLocked, _ = IsFlagPassed("L") + isArmored = false + case "decrypt": + isLocked, _, _ = IsFileLocked(inputValue) + default: + msg := "unable to select locked state header.\n" + HandleFailure(msg) + os.Exit(1) + } + + sLen := len(serialFetch) + var prefixLen int + + if isLocked { + prefixLen = 17 + } else { + prefixLen = 12 + } + + totalRawBytes := prefixLen + sLen + 2 + remainder := totalRawBytes % 3 + paddingNeeded := 0 + + if remainder != 0 { + paddingNeeded = 3 - remainder + } + + if isLocked { + lockedState = vars.LockVersionLabel + vars.EndAnchor + } else { + lockedState = vars.VersionLabel + vars.EndAnchor + } + + clean := strings.ReplaceAll(lockedState, "[serial]", serialFetch) + clean = clean + strings.Repeat(" ", paddingNeeded) + + /* + + fmt.Printf("totalRawBytes: %v\n", totalRawBytes) + fmt.Printf("len: %v\n", prefixLen) + fmt.Printf("serial length: %v\n", sLen) + fmt.Printf("command: %s\n", vars.CommandFlag) + fmt.Printf("isLockSet: %v\n", isLockSet) + fmt.Printf("isKeySet: %v\n", isKeySet) + fmt.Printf("isInputSet: %v\n", isInputSet) + + fmt.Printf("lockValue: %s\n", lockValue) + fmt.Printf("keyvalue: %s\n", keyValue) + fmt.Printf("inputValue: %s\n", inputValue) + fmt.Printf("serial: %s\n", serialFetch) + fmt.Printf("islocked: %v\n", isLocked) + + */ + + _ = isLockSet + _ = isKeySet + _ = isInputSet + _ = lockValue + _ = keyValue + _ = inputValue + _ = isLocked + _ = isArmored + _ = lockedState + _ = serialFetch + _ = isLocked + + return clean, nil + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func CheckExtraFlags() ([]string, error) { + + args := os.Args + cIndex := -1 + + for i, arg := range args { + if strings.ToLower(arg) == "-c" { + cIndex = i + break + } + } + + if cIndex == -1 || cIndex+1 >= len(args) { + return nil, errors.New("missing -c COMMAND flag or its associated value. See Usage for details.") + } + + remaining := args[cIndex+2:] + + var foundFlags []string + for _, arg := range remaining { + + if strings.HasPrefix(arg, "-") { + foundFlags = append(foundFlags, arg) + } + + } + + if len(foundFlags) == 0 { + return nil, fmt.Errorf("error: no additional flags provided after -C COMMAND %s", args[cIndex+1]) + } + return foundFlags, nil +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func HasValidPrefix(str, prefix string) bool { + return strings.HasPrefix(str, prefix) +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func ValidateRequiredFlags(required []vars.FlagRequirement, action string) { + + for _, req := range required { + + isFlagSet, hasValue := IsFlagPassed(req.Flag[1:]) + flag := req.Flag[1:] + + if !isFlagSet { + context := ArgumentMap("-" + flag) + msg := fmt.Sprintf("%s: -%s %s: flag is required.\n", vars.CommandFlag, flag, context) + HandleFailure(msg) + os.Exit(1) + } + + if isFlagSet && req.IsRequired && !hasValue { + context := ArgumentMap("-" + flag) + msg := fmt.Sprintf("%s: -%s %s: requires a value.", vars.CommandFlag, flag, context) + HandleFailure(msg) + os.Exit(1) + } + + } + /* + //func ValidateRequiredFlags(required map[string]bool, action string) { + + for flag, isRequired := range required { + + //fmt.Printf("%v\n", flag) + //fmt.Printf("%v\n", isRequired) + //fmt.Printf("\n\n") + + //flagtmp := strings.ReplaceAll(flag, "-", "") + present, hasValue := IsFlagPassed(flag[1:]) + + fmt.Printf("%s present: %v\n", flag, present) + fmt.Printf("has value: %v\n", hasValue) + fmt.Printf("\n\n") + + if !present { + //flagTemp := strings.ReplaceAll(flag, "-", "") + context := ArgumentMap(flag) + msg := fmt.Sprintf("%s: %s (%s) was not provided.", vars.CommandFlag, flag, context) + HandleFailure(msg) + os.Exit(1) + } + + if present && isRequired && !hasValue { + context := ArgumentMap(flag) + msg := fmt.Sprintf("%s: %s (%s) requires a value.", vars.CommandFlag, flag, context) + HandleFailure(msg) + os.Exit(1) + } + + //if present && isRequired && !hasValue { + // fmt.Printf("error") + //} + + //_ = hasValue + + //flagtmp := strings.ReplaceAll(flag, "-", "") + //present, hasValue := IsFlagPassed(flagtmp[1:]) + + //fmt.Printf("%s\n", flagtmp) + //fmt.Printf("%v\n", present) + //fmt.Printf("%v\n", hasValue) + + //if !present && isRequired { + // context := ArgumentMap(flag) + // //msg := fmt.Printf("%s: %s: The required flag '%s' (%s) was not provided.", vars.CommandFlag, action, flag, context) + // msg := fmt.Sprintf("%s: %s: The required flag '%s' (%s) was not provided.", vars.CommandFlag, action, flag, context) + // HandleFailure(msg) + // os.Exit(1) + //} + + //if present && isRequired && !hasValue { + // fmt.Printf("fuck") + //} + + //if !present { + // context := ArgumentMap(flag) + // msg := " ERROR: syscrypt: " + action + ": the required flag " + flag + " '" + context + "' was not provided." + // HandleFailure(msg) + // os.Exit(1) + //} + + //if present && !hasValue && isRequired { + // context := ArgumentMap(flag) + // msg := fmt.Sprintf("%s: %s: The required flag %s. '%s' requires a value.", vars.CommandFlag, action, flag, context) + // HandleFailure(msg) + // os.Exit(1) + //} + + //_ = hasValue + //_ = isRequired + + } + + */ +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func ConvertToUTF16(s string) []byte { + codePoints := utf16.Encode([]rune(s)) + buf := make([]byte, len(codePoints)*2) + + for i, cp := range codePoints { + binary.LittleEndian.PutUint16(buf[i*2:], cp) + } + return buf +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func RandomCode(length int) (string, error) { + pool := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+" + result := make([]byte, length) + poolLen := big.NewInt(int64(len(pool))) + + for i := 0; i < length; i++ { + num, err := rand.Int(rand.Reader, poolLen) + if err != nil { + return "", err + } + result[i] = pool[num.Int64()] + } + return string(result), nil +} + +func RandomString(length int) string { + const charset = "ABCDEFGHJKMNPQRSTUVWXYZ123456789" + result := make([]byte, length) + + _, err := rand.Read(result) + if err != nil { + fmt.Println(" ERROR: syscrypt: Failed to generate random sequence.") + os.Exit(1) + } + + for i := 0; i < length; i++ { + result[i] = charset[result[i]%byte(len(charset))] + } + return string(result) +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func NotifyPushover(message string) { + /* + + if config.PushoverActive == "1" { + go func() { + http.PostForm(config.PushoverEndpoint, url.Values{ + "token": {config.PushoverToken}, + "user": {config.PushoverUser}, + "message": {message}, + "title": {"syscrypt Security Alert"}, + "priority": {"1"}, + }) + }() + } + + */ + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func AbsPath(path string) string { + abs, err := filepath.Abs(path) + if err != nil { + return path + } + return abs +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func BypassCheck(path string) bool { + if path == "" { + return false + } + + absInput := AbsPath(path) + for _, b := range BypassList { + if absInput == AbsPath(b) { + return true + } + } + return false +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func BootStrap() bool { + //return config.Bootstrap == "1" + return false +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func CheckLockout() { + /* + data, err := os.ReadFile(config.LockPath) + if err != nil { + return + } + + var failures int + var lastFail int64 + host, _ := os.Hostname() + fmt.Sscanf(string(data), "%d %d", &failures, &lastFail) + + if failures >= config.MaxFailures { + remaining := (lastFail + int64(config.LockoutMinutes*60)) - time.Now().Unix() + if remaining > 0 { + fmt.Fprintf(os.Stderr, " ERROR: syscrypt: System locked. Try again in %d minutes \n", (remaining/60)+1) + NotifyPushover(fmt.Sprintf("CRITICAL: host %s is now LOCKED OUT for %d minutes due to repeated failures.", host, config.LockoutMinutes)) + os.Exit(403) + } else { + _ = os.Remove(config.LockPath) + } + } + + */ + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func RecordFailure() { + /* + var failures int + var lastFail int64 + data, err := os.ReadFile(config.LockPath) + + if err == nil { + fmt.Sscanf(string(data), "%d %d", &failures, &lastFail) + } + + failures++ + _ = os.WriteFile(config.LockPath, []byte(fmt.Sprintf("%d %d", failures, time.Now().Unix())), 0600) + + host, _ := os.Hostname() + + if failures >= config.MaxFailures { + NotifyPushover(fmt.Sprintf("CRITICAL: Host %s is now LOCKED OUT for %d minutes due to repeated failures.", host, config.LockoutMinutes)) + } else { + + NotifyPushover(fmt.Sprintf("WARNING: Failed access attempt on %s. Strike %d/%d.", host, failures, config.LockoutMinutes)) + } + */ +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func RateLimit() { + /* + now := time.Now().Unix() + var tokens int64 + var lastUpdate int64 + + data, err := os.ReadFile(config.StatePath) + + if err == nil { + fmt.Sscanf(string(data), "%d %d", &tokens, &lastUpdate) + + delta := now - lastUpdate + tokens += delta / config.RefillSeconds + if tokens > config.MaxTokens { + tokens = config.MaxTokens + } + + if delta >= config.RefillSeconds { + lastUpdate = now + } + + } else { + tokens = config.MaxTokens + lastUpdate = now + } + + host, _ := os.Hostname() + + if tokens <= 0 { + _ = os.WriteFile(config.StatePath, []byte(fmt.Sprintf("%d %d", 0, now)), 0600) + fmt.Fprintf(os.Stderr, " ERROR: syscrypt: Rate limit exceeded.\n") + NotifyPushover(fmt.Sprintf("WARNING: Rate limit exceeded. %s.", host)) + } + + tokens-- + _ = os.WriteFile(config.StatePath, []byte(fmt.Sprintf("%d %d", tokens, lastUpdate)), 0600) + */ +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func ReadFirstLine(filePath string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + + if scanner.Scan() { + ScannerText := scanner.Text() + ScannerText = strings.TrimSuffix(ScannerText, "--") + ScannerText = strings.Join(strings.Fields(ScannerText), "") + return ScannerText, nil + } + + if err := scanner.Err(); err != nil { + return "", err + } + + return "", nil +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func GetFirstLine(filePath string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", err + } + + scanner := bufio.NewScanner(file) + + if scanner.Scan() { + ScannerText := scanner.Text() + return ScannerText, nil + } + + if err := scanner.Err(); err != nil { + return "", err + } + + return "", nil +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func GetFirstLineFromString(s string) string { + if i := strings.Index(s, "\n"); i >= 0 { + return s[:i] + } + return s +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func GetLastLine(s string) (string, error) { + file, err := os.Open(s) + if err != nil { + return "", err + } + defer file.Close() + + var lineOffsets []int64 + lineOffsets = append(lineOffsets, 0) + + reader := bufio.NewReader(file) + var offset int64 + + for { + line, err := reader.ReadBytes('\n') + offset += int64(len(line)) + + if err != nil { + // + if err == io.EOF { + break + } + return "", err + // + } + + lineOffsets = append(lineOffsets, offset) + } + + if len(lineOffsets) == 0 { + return "", fmt.Errorf("file is empty") + } + + lastLineOffset := lineOffsets[len(lineOffsets)-1] + + if lastLineOffset == offset && len(lineOffsets) > 1 { + lastLineOffset = lineOffsets[len(lineOffsets)-2] + } + + _, err = file.Seek(lastLineOffset, io.SeekStart) + if err != nil { + return "", err + } + + finalScanner := bufio.NewScanner(file) + if finalScanner.Scan() { + return finalScanner.Text(), nil + } + + return "", finalScanner.Err() +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func StripHeaders(s string) string { + s = strings.ReplaceAll(s, "-----BEGIN ENCRYPTED DATA-----\n", "") + s = strings.ReplaceAll(s, "-----END ENCRYPTED DATA-----\n", "") + isBase64 := IsValidBase64WithLines(s) + + var content string + + if isBase64 { + content = "" /// decode base64 message + } else { + content = "" // return string untouched + } + + _ = content + + return s +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func CheckHeader(path string, header string) error { + file, err := os.Open(path) + if err != nil { + return fmt.Errorf(" ERROR: syscrypt: CheckHeader: could not open file: %v", err) + } + defer file.Close() + buffer := make([]byte, len(header)) + + _, err = file.Read(buffer) + if err != nil { + return fmt.Errorf(" ERROR: syscrypt: checkHeader: could not read file header: %v", err) + } + + if string(buffer) != header { + return fmt.Errorf(" ERROR: syscrypt: checkHeader: Invalid file. Header does not match %s", header) + } + return nil +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func CheckPermission(path string, want os.FileMode) { + st, err := os.Stat(path) + if err != nil { + return + } + + if !st.Mode().IsRegular() { + fmt.Fprintf(os.Stderr, " ERROR: syscrypt: Invalid type for '%s'. Expected 'file' type.\n", path) + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func FolderExists(path string) bool { + + info, err := os.Stat(path) + if err != nil { + return false + } + + return info.IsDir() +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func FetchFileKey(path string, object string) (string, error) { + + if _, err := exec.LookPath("jq"); err != nil { + fmt.Fprintf(os.Stderr, "syscrypt: FetchFileKey: jq is not installed.") + os.Exit(1) + } + + info, err := os.Stat(path) + if err != nil { + + if os.IsNotExist(err) { + msg := "syscrypt: FetchFileKey: Unable to open file: " + path + "\n" + HandleFailure(msg) + os.Exit(1) + //fmt.Fprintf(os.Stderr, " ERROR: syscrypt: FetchFileKey: Unable to open file: %s", path+"\n") + //os.Exit(1) + } + + } + + if info.IsDir() { + fmt.Fprintf(os.Stderr, "syscrypt: FetchFileKey: Path is a directory. File is expected. %s", path+"\n") + os.Exit(1) + } + + cmd := exec.Command("jq", "-e", "-r", ".[] | ."+object, path) + + var out bytes.Buffer + var stderr bytes.Buffer + + cmd.Stdout = &out + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "syscrypt: FetchFileKey: Invalid Key file or malformed JSON at %s: %s", path, stderr.String()+"\n") + os.Exit(1) + } + + key := strings.TrimSpace(out.String()) + + if key == "" || key == "null" { + fmt.Fprintf(os.Stderr, "syscrypt: No 'key' field found in JSON root at %s\n", path) + os.Exit(1) + } + + return key, err +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func GenerateSerial(n int) string { + const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + max := big.NewInt(int64(len(alphabet))) + + b := make([]byte, n) + for idx := 0; idx < n; idx++ { + rn, err := rand.Int(rand.Reader, max) + if err != nil { + msg := fmt.Sprintf(" ERROR: syscrypt-keygen: genSerialUpperAlnum: serial gen failed: %v", err) + HandleFailure(msg) + os.Exit(1) + } + b[idx] = alphabet[rn.Int64()] + } + + return string(b) +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func GenerateAPIKey(salt, password, serial, privateKey string) string { + + h := hmac.New(sha256.New, []byte(privateKey)) + h.Write([]byte("v1")) + h.Write([]byte(salt)) + h.Write([]byte(password)) + h.Write([]byte(serial)) + + signature := hex.EncodeToString(h.Sum(nil)) + + return signature +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func ValidateAPIKey(apiKey, salt, password, serial, privateKey string) bool { + expectedKey := GenerateAPIKey(salt, password, serial, privateKey) + if len(apiKey) != len(expectedKey) { + return false + } + + result := hmac.Equal([]byte(apiKey), []byte(expectedKey)) + return result +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func GeneratePassword(length int) (string, error) { + const charset = "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + "!@#$%^&*()-_=+[]{}|;:,.<>?" + + password := make([]byte, length) + for i := range password { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + return "", err + } + password[i] = charset[num.Int64()] + } + + return string(password), nil +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func GenerateKeys(publicKey interface{}, privateKey interface{}, serial interface{}) (string, error) { + + privateKeyString := privateKey.(string) + publicKeyString := publicKey.(string) + serialString := serial.(string) + //masterPass := config.MasterPass + + //fmt.Printf("masterpass: %v\n", masterPass) + fmt.Printf("private key: %v\n", privateKeyString) + fmt.Printf("public key: %v\n", publicKeyString) + fmt.Printf("serial: %v\n", serialString) + + isOutputSet, _ := IsFlagPassed("o") + outputValue, _ := GetFlagValue("o") + outputIsValidPath := IsValidPath(outputValue) + + if !isOutputSet { + msg := fmt.Sprintf("%s: -o OUTPUT: is required when using the keygen tool.\n", vars.CommandFlag) + HandleFailure(msg) + os.Exit(1) + } + + if isOutputSet && outputValue == "" { + msg := fmt.Sprintf("%s: -o OUTPUT: a value is required when using the keygen tool.\n", vars.CommandFlag) + HandleFailure(msg) + os.Exit(1) + } + + if !outputIsValidPath { + msg := fmt.Sprintf("%s: -o OUTPUT: a valid path is required when using the keygen tool.\n", vars.CommandFlag) + HandleFailure(msg) + os.Exit(1) + } + + if privateKeyString == "" { + msg := fmt.Sprintf("%s: privateKeyString value is empty.\n", vars.CommandFlag) + HandleFailure(msg) + os.Exit(1) + } + + isCommentSet, _ := IsFlagPassed("C") + commentValue, _ := GetFlagValue("C") + + if isCommentSet && commentValue == "" { + msg := fmt.Sprintf("%s: -C COMMENT: comment flag was set but no value was provided.\n", vars.CommandFlag) + HandleFailure(msg) + os.Exit(1) + } + + if publicKeyString == "" { + msg := fmt.Sprintf("%s: publicKeyString value is empty.\n", vars.CommandFlag) + HandleFailure(msg) + os.Exit(1) + } + + if serialString == "" { + msg := fmt.Sprintf("%s: serialString value is empty.\n", vars.CommandFlag) + HandleFailure(msg) + os.Exit(1) + } + + isValidPrivateKey := IsPrivateKey(privateKeyString) + isValidPublicKey := IsPublicKey(publicKeyString) + + if !isValidPrivateKey { + msg := fmt.Sprintf("%s: Invalid Private Key format. Expected '%s', received: '%s'.\n", vars.CommandFlag, vars.PrivateKeyPrefixLabel+"[your private key]", privateKeyString) + HandleFailure(msg) + os.Exit(1) + } + + if !isValidPublicKey { + msg := fmt.Sprintf("%s: Invalid Public key format. Expected '%s', received: '%s'.\n", vars.CommandFlag, vars.DefaultPrefixLabel+"[your public key]", publicKeyString) + HandleFailure(msg) + os.Exit(1) + } + + privStruct := syscrypt.PrivateKeyWrapper{} + pubStruct := syscrypt.PublicKeyWrapper{} + + fmt.Printf("--------------------------------------\n") + + fmt.Printf("privstruct: %s\n", privStruct) + fmt.Printf("pubstruct: %s\n", pubStruct) + + //isKeySet, keyHasValue := utils.IsFlagPassed("k") + //keyValue, _ := utils.GetFlagValue("k") + //keyIsValidPath := utils.IsValidPath(keyValue) + + //isInputSet, inputHasValue := utils.IsFlagPassed("i") + //inputValue, _ := utils.GetFlagValue("i") + //inputIsValidPath := utils.IsValidPath(inputValue) + + //isOutputSet, outputHasValue := utils.IsFlagPassed("o") + //outputValue, _ := utils.GetFlagValue("o") + //outputIsValidPath := utils.IsValidPath(outputValue) + + //isArmoredSet, armoredHasValue := utils.IsFlagPassed("a") + + //isLockSet, lockHasValue := utils.IsFlagPassed("L") + + //isAPISet, apiHasValue := utils.IsFlagPassed("A") + //apiValue, _ := utils.GetFlagValue("A") + //apiIsValidPath := utils.IsValidPath(apiValue) + + //isPassSet, passHasValue := utils.IsFlagPassed("P") + //passValue, _ := utils.GetFlagValue("P") + //passIsValidPath := utils.IsValidPath(passValue) + + return "", nil + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func ExtractSerial(filePath string) (string, error) { + + fileExists := FileExists(filePath) + if !fileExists { + return "", fmt.Errorf("Extract Serial: unable to open file '%s'", filePath) + } + + data, err := os.ReadFile(filePath) + if err != nil { + return "", err + } + + var workingData []byte + content := string(data) + + if strings.Contains(content, vars.PrivateKeyHeader) { + content = strings.ReplaceAll(content, vars.PrivateKeyHeader, "") + content = strings.ReplaceAll(content, vars.PrivateKeyFooter, "") + content = strings.TrimSpace(content) + + decoded, err := base64.StdEncoding.DecodeString(content) + if err != nil { + return "", fmt.Errorf("Extract Serial: base64 decode failed: %w", err) + } + + workingData = decoded + + } else { + + workingData = data + + } + + decodedStr := string(workingData) + + startAnchor := vars.StartAnchor + endAnchor := vars.EndAnchor + + anchorIdx := strings.Index(decodedStr, startAnchor) + if anchorIdx == -1 { + return "", fmt.Errorf("Extract Serial: malformed encrypted file: start anchor not found") + } + + serialStart := anchorIdx + len(startAnchor) + endIdx := strings.Index(decodedStr[serialStart:], endAnchor) + if endIdx == -1 { + return "", fmt.Errorf("Extract Serial: malformed encrypted file: end anchor '--' not found") + } + + absoluteEndIdx := serialStart + endIdx + serial := decodedStr[serialStart:absoluteEndIdx] + return strings.TrimSpace(serial), nil + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//func GenerateAPIKey(masterKey string) (string, error) { + +//randomID := make([]byte, 16) +//if _, err := rand.Read(randomID); err != nil { +// return "", err +//} + +//publicID := hex.EncodeToString(randomID) + +//h := hmac.New(sha256.New, []byte(masterKey)) +//h.Write([]byte(publicID)) +//signature := hex.EncodeToString(h.Sum(nil))[:16] + +//return fmt.Sprintf("%s.%s", publicID, signature), nil + +//} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//func GenerateAPIKeyFile(masterKey string) (string, error) { +// APIKeyOut, err := os.OpenFile(xxxx, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) +// if err != nil { +// } +//} + +func ResetDictionary() error { + /* + var mapPath = config.DictionaryTmp + chars := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ " + pool := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+" + blockSize := 4 + + if err := os.MkdirAll(filepath.Dir(mapPath), 0755); err != nil { + return err + } + + file, err := os.Create(mapPath) + if err != nil { + return err + } + defer file.Close() + + usedCodes := make(map[string]bool) + + for _, char := range chars { + + for { + code, err := generateRandomBlock(pool, blockSize) + if err != nil { + return err + } + + if !usedCodes[code] { + + usedCodes[code] = true + + _, err := fmt.Fprintf(file, "[%c]:%s\n", char, code) + if err != nil { + return err + } + break + + } + + } + + } + + return nil + + */ + return nil + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func generateRandomBlock(pool string, size int) (string, error) { + bytes := make([]byte, size) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + + for i, b := range bytes { + bytes[i] = pool[b%byte(len(pool))] + } + return string(bytes), nil +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func Encode(input string) (string, error) { + /* + binaryPath := config.BinaryPath + "/syscrypt-encode" + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, binaryPath, "-s", input) + cmd.Env = append(os.Environ(), "SYSCRYPT_INTERNAL_CALL=1") + + out, err := cmd.CombinedOutput() + + if ctx.Err() == context.DeadlineExceeded { + return "", fmt.Errorf(" ERROR: syscrypt: Encode: timed out %s", string(out)) + } + + if err != nil { + return "", fmt.Errorf(" ERROR: syscrypt: Encode: failed to encode string: %w %s", err, string(out)) + } + + return strings.TrimSpace(string(out)), nil + */ + return "", nil +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// func HandleFailure(msg string, err error) error { +func HandleFailure(msg string) { + + //ClearTerminal() + + isInternal := os.Getenv("SYSCRYPT_INTERNAL_CALL") == "1" + + bannerStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#000000")). + Background(lipgloss.Color("#FFD700")). + Padding(0, 2) + + boxStyle := lipgloss.NewStyle(). + //Border(lipgloss.RoundedBorder()). + Border(lipgloss.NormalBorder(), true, false, true, false). + BorderForeground(lipgloss.Color("#797979")). + Padding(1, 1). + Margin(1, 1) + + codeStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#c4b810")). + Underline(false) + + errorStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#FF0000")) + + fmt.Println("\n" + bannerStyle.Render(" SYSCRYPT ERROR ")) + + content := msg + + //if err != nil { + // fmt.Println(boxStyle.Render(content)) + //} + + if isInternal { + fmt.Println(content) + } else { + fmt.Println(boxStyle.Render(content)) + } + + //if isInternal { + // //return fmt.Errorf("%s", msg) + //} else { + // //return fmt.Println + //} + + //if isInternal { + // return fmt.Errorf("%s", fullMsg) + //} + + _ = errorStyle + _ = codeStyle + _ = boxStyle + _ = isInternal + + //fullMsg := msg + //if err != nil { + // Alert("\n") + // fullMsg = fmt.Sprintf("%s: %v", msg, err) + // Alert("\n") + //} + + //if isInternal { + // return fmt.Errorf("%s", fullMsg) + //} + + //fmt.Fprintf(os.Stderr, "%s\n", fullMsg) + //os.Exit(1) + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func Alert(msg string) { + fullMsg := msg + if fullMsg == "" { + fmt.Println(" ERROR: syscrypt: Alert requires a message.") + os.Exit(1) + } + + fmt.Printf("%v\n", fullMsg) + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func Decode(input string, key string) (string, error) { + /* + binaryPath := config.BinaryPath + "/syscrypt-decode" + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, binaryPath, "-s", input) + cmd.Env = append(os.Environ(), "SYSCRYPT_INTERNAL_CALL=1") + + out, err := cmd.CombinedOutput() + + if ctx.Err() == context.DeadlineExceeded { + return "", fmt.Errorf(" ERROR: syscrypt: Decode: timed out %s", string(out)) + } + + if err != nil { + return "", fmt.Errorf(" ERROR: syscrypt: Decode: failed to encode string: %w %s", err, string(out)) + } + + return strings.TrimSpace(string(out)), nil + */ + return "", nil +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func DecryptFile(path string, password string) (string, error) { + + /* + binaryPath := config.BinaryPath + "/syscrypt" + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, binaryPath, "-d", "-i", config.KeyPath, "-k", password, path) + cmd.Env = append(os.Environ(), "SYSCRYPT_INTERNAL_CALL=1") + + out, err := cmd.CombinedOutput() + + if ctx.Err() == context.DeadlineExceeded { + return "", fmt.Errorf(" ERROR: syscrypt: DecryptFile: timed out: %s", string(out)) + } + + if err != nil { + return "", fmt.Errorf(" ERROR: syscrypt: DecryptFile: decrypt failed: %w %s", err, string(out)) + } + + return strings.TrimSpace(string(out)), nil + */ + return "", nil +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func EncryptFile(path string, password string) (string, error) { + /* + + binaryPath := config.BinaryPath + "/syscrypt" + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, binaryPath, "-a", "-R", config.KeyPublicPath, "-k", password, path) + cmd.Env = append(os.Environ(), "SYSCRYPT_INTERNAL_CALL=1") + + out, err := cmd.CombinedOutput() + if ctx.Err() == context.DeadlineExceeded { + return "", fmt.Errorf(" ERROR: syscrypt: EncryptFile: timed out: %s", string(out)) + } + + if err != nil { + return "", fmt.Errorf(" ERROR: syscrypt: EncryptFile: encrypt failed: %w %s", err, string(out)) + } + + return strings.TrimSpace(string(out)), nil + + */ + + return "", nil + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func CopyFile(src, dst string) error { + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + destFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer destFile.Close() + + _, err = io.Copy(destFile, sourceFile) + return err +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func IsFileEmpty(path string) bool { + info, err := os.Stat(path) + + if err != nil { + return true + } + + return info.Size() == 0 +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func FileExists(path string) bool { + _, err := os.Stat(path) + if err != nil { + + if os.IsNotExist(err) { + return false + } + + } + return true +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func ConfirmOverwrite(path string) { + + _, err := os.Stat(path) + if os.IsNotExist(err) { + return + } + + bannerStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#000000")). + Background(lipgloss.Color("#FFD700")). + Padding(0, 2) + + boxStyle := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#d32020")). + Padding(1, 4). + Margin(1, 0) + + codeStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#c4b810")). + Underline(false) + + errorStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#FF0000")) + + fmt.Println("\n" + bannerStyle.Render(" ACTION REQUIRED ")) + + randomcode := RandomString(6) + styledCode := codeStyle.Render(randomcode) + + fileName := filepath.Base(path) + msg := "\n\n WARNING: The output file '" + fileName + "' already exists. \n" + + " By proceeding, you may destroy an existing encrypted file or key.\n\n" + + " Type " + styledCode + " in the prompt below. \n\n" + + content := msg + + fmt.Println(boxStyle.Render(content)) + + fmt.Print(" Type the code shown above and press ENTER > ") + var userInput string + fmt.Scanln(&userInput) + + ClearTerminal() + + if strings.TrimSpace(userInput) != randomcode { + fmt.Printf("\n%s\n", errorStyle.Render(" INVALID CODE. Aborting operation.\n")) + os.Exit(1) + } + + //return strings.TrimSpace(userInput) == randomcode + + //reader := bufio.NewReader(os.Stdin) + + //input, err := reader.ReadString('\n') + //if err != nil { + // fmt.Println("\n ERROR: failed to read input") + // os.Exit(1) + //} + + //answer := strings.TrimSpace(input) + + //if answer != randomcode { + // fmt.Printf(" ABORTED: Your response did not match the generated code. Action aborted.\n") + // os.Exit(1) + //} + + //fmt.Println(msg) + + //reader := bufio.NewReader(os.Stdin) + //input, err := reader.ReadString('\n') + //if err != nil { + // fmt.Println("\n ERROR: failed to read input") + // os.Exit(1) + //} + + //fmt.Println("\t") + + //answer := strings.TrimSpace(input) + + //if answer != randomcode { + // fmt.Printf(" ABORTED: Your response didnt match the generated code. Action aborted.\n") + // os.Exit(1) + //} + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func ConfirmKeyOverwrite(path string) { + + _, err := os.Stat(path) + if os.IsNotExist(err) { + return + } + + bannerStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#000000")). + Background(lipgloss.Color("#790000")). + Padding(0, 2) + + boxStyle := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#d32020")). + Padding(1, 4). + Margin(1, 0) + + codeStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#ffffff")). + Underline(false) + + errorStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#FF0000")) + + fmt.Println("\n" + bannerStyle.Render(" ACTION REQUIRED ")) + + randomcode := RandomString(3) + styledCode := codeStyle.Render(randomcode) + + fileName := codeStyle.Render(filepath.Base(path)) + + msg := "\n\n DANGER: THE KEY FILE " + fileName + " ALREADY EXISTS.\n\n" + + " BY PROCEEDING, YOU WILL DESTROY YOUR EXISTING KEY PAIRS LINKED TO THIS KEY.\n\n" + + " PLEASE BACK UP YOUR KEYS BEFORE PROCEEDING.\n\n" + + " OVERWRITTEN KEYS CANNOT BE RECOVERED. Press Ctrl + C to abort. \n\n" + + " type " + styledCode + " in the prompt below to proceed. \n\n" + + content := msg + + fmt.Println(boxStyle.Render(content)) + + fmt.Print(" Type the code shown above and press ENTER > ") + var userInput string + fmt.Scanln(&userInput) + + ClearTerminal() + + if strings.TrimSpace(userInput) != randomcode { + fmt.Printf("\n%s\n", errorStyle.Render(" INVALID CODE. Aborting operation.\n")) + os.Exit(1) + } + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func ClearTerminal() { + var cmd *exec.Cmd + + if runtime.GOOS == "windows" { + cmd = exec.Command("cls") + } else { + cmd = exec.Command("clear") + } + + cmd.Stdout = os.Stdout + cmd.Run() +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func EmptyVariable(input string, pointer string) error { + if input == "" { + return fmt.Errorf(" ERROR: syscrypt: variable '%s' is empty.\n", pointer) + } + return nil +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func IsEmpty(input string) bool { + return strings.TrimSpace(input) == "" +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func ValidateHeader(path string, expectedHeader string) error { + if strings.TrimSpace(path) == "" { + return fmt.Errorf(" ERROR: syscrypt: ValidateHeader: path is empty.") + } + + if strings.TrimSpace(expectedHeader) == "" { + return fmt.Errorf(" ERROR: syscrypt: ValidateHeader: expectedHeader is empty.") + } + + file, err := os.Open(path) + if err != nil { + return fmt.Errorf(" ERROR: syscrypt: ValidateHeader: failed to open '%s' '%w'", path, err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + if !scanner.Scan() { + if err := scanner.Err(); err != nil { + return fmt.Errorf(" ERROR: syscrypt: ValidateHeader: '%s' could not be read. %w", path, err) + } + return fmt.Errorf(" ERROR: syscrypt: ValidateHeader: '%s' is empty or could not be read.", path) + } + + actualHeader := scanner.Text() + + if strings.TrimSpace(actualHeader) != strings.TrimSpace(expectedHeader) { + return fmt.Errorf(" ERROR: syscrypt: ValidateHEader: Header mismatch in '%s'. Expected '%s', got '%s'", path, expectedHeader, actualHeader) + } + return nil +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func IsFlagPassed(name string) (bool, bool) { + + //for _, arg := range os.Args { + // if arg == "-"+name || arg == "--"+name { + // return true + // } + //} + //return false + + for i, arg := range os.Args { + if arg == "-"+name || arg == "--"+name { + found := true + hasValue := i+1 < len(os.Args) && os.Args[i+1] != "" && os.Args[i+1][0] != '-' + return found, hasValue + } + } + return false, false +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func PreFlight() { + /* + + // Check Config for empty variables + + EmptyVariable(config.KeyPath, "KeyPath") + EmptyVariable(config.KeyPublicPath, "KeyPublicPath") + EmptyVariable(config.ExpectedSerial, "ExpectedSerial") + + EmptyVariable(config.EncryptKeyPath, "EncryptKeyPath") + EmptyVariable(config.EncryptPublicKeyPath, "EncryptPublicKeyPath") + + if config.Bootstrap == "0" { + EmptyVariable(config.ExpectedEncryptSerial, "ExpectedEncryptSerial") + } + + EmptyVariable(config.MasterPass, "MasterPass") + EmptyVariable(config.Dictionary, "Dictionary") + + // Check if files in config exist + + FileExists(config.KeyPath) + FileExists(config.KeyPublicPath) + FileExists(config.EncryptKeyPath) + FileExists(config.EncryptPublicKeyPath) + FileExists(config.MasterPass) + FileExists(config.Dictionary) + + // Check file permissions in config + + CheckPermission(config.KeyPath, 0600) + CheckPermission(config.KeyPublicPath, 0644) + CheckPermission(config.EncryptKeyPath, 0600) + CheckPermission(config.EncryptPublicKeyPath, 0644) + CheckPermission(config.MasterPass, 0600) + CheckPermission(config.Dictionary, 0600) + + // Validate file headers + + ValidateHeader(config.KeyPath, "SYSCRYPT-KEY-") + ValidateHeader(config.KeyPublicPath, "syscrypt") + ValidateHeader(config.EncryptKeyPath, "-----BEGIN ENCRYPTED DATA-----") + ValidateHeader(config.EncryptPublicKeyPath, "-----BEGIN ENCRYPTED DATA-----") + ValidateHeader(config.MasterPass, "-----BEGIN ENCRYPTED DATA-----") + ValidateHeader(config.Dictionary, "-----BEGIN ENCRYPTED DATA-----") + + */ + +} + +//// ENCRYPT FUNCTIONS ///////////////////////////////////////////////////////////////////////////////////////// + +// msg := " ERROR: syscrypt: '-A' API Key flag is only used during decryption.\n" +//utils.HandleFailure(msg, nil) + +///// DECRYPT FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func AuthorizeOrExit(challengeFlag string, inputPath string) { + + _disabled := "" + _ = _disabled + + // Validate Master Keys + + //_storedMasterSerial, err := FetchFileKey(syscryptcfg.KeyPath, "serial") + ///if err != nil { + // fmt.Fprintf(os.Stderr, " ERROR: syscrypt: Failed to fetch Master Private Key file.\n") + // os.Exit(1) + //} + + //if _storedMasterSerial != syscryptcfg.ExpectedSerial { + // fmt.Fprintf(os.Stderr, " ERROR: syscrypt: The stored 'Master Primary Key Serial Number' does not match the bound 'Master Primary Key Serial Number'. '%s'.\n", _storedMasterSerial) + // os.Exit(1) + //} + + // Validate Encryption Keys + + //if syscryptcfg.Bootstrap == "0" { + + // _storedEncryptSerial, err := FetchFileKey(syscryptcfg.EncryptKeyPath, "serial") + // if err != nil { + // fmt.Fprintf(os.Stderr, " ERROR: syscrypt: Failed to fetch Encryption Private key file.\n") + // } + + // if _storedEncryptSerial != syscryptcfg.ExpectedEncryptSerial { + // fmt.Fprintf(os.Stderr, " ERROR: syscrypt: The stored 'Encryption Primary Key Serial Number' does not match the bound 'Encryption Primary Key Serial Number'. '%s'.\n", _storedEncryptSerial) + // os.Exit(1) + // } + + //} + + // Decrypt Master Password + + //if syscryptcfg.Bootstrap == "0" { + + // _masterPass, err := DecryptFile(syscryptcfg.MasterPass, "") + // if err != nil { + // fmt.Fprintf(os.Stderr, " ERROR: syscrypt: Failed to decrypt Master password.\n") + // os.Exit(1) + // } + + // if challengeFlag != _masterPass { + // RecordFailure() + // fmt.Fprintf(os.Stderr, " ERROR: syscrypt: Invalid Master Password.\n") + // os.Exit(1) + // } + + //} + + ///////////////////////////////////////////////////// + + //if _storedMasterKey != syscryptcfg.ExpectedPrivateKey { + // fmt.Fprintf(os.Stderr, " ERROR: The Stored Master Private Key does not match the bound Master Private Key '%s' '%s'.\n", syscryptcfg.ExpectedPrivateKey, _storedMasterKey) + // os.Exit(1) + //} + + //if _storedMasterPublicKey != syscryptcfg.ExpectedPublicKey { + // fmt.Fprintf(os.Stderr, " ERROR: The Stored Master Public Key does not match the bound Master Public Key '%s' '%s'.\n", syscryptcfg.ExpectedPublicKey, _storedMasterPublicKey) + // os.Exit(1) + //} + + // Validate Encryption Keys + + //if syscryptcfg.Bootstrap == "0" { + + // _storedEncryptKey, err := FetchFileKey(syscryptcfg.EncryptKeyPath) + // if err != nil { + // fmt.Fprintf(os.Stderr, " ERROR: syscrypt: Failed to fetch Encryption Private Key file.\n") + // os.Exit(1) + // } + + // if _storedEncryptKey != syscryptcfg.ExpectedEncryptPrivateKey { + // fmt.Fprintf(os.Stderr, " ERROR: syscrypt: The stored Encryption Private key does not match the bound Encryption Private Key.\n") + // os.Exit(1) + // } + //} + + // Decrypt Master Password + + //_masterPass, err := DecryptFile(syscryptcfg.MasterPass, "x") + //if err != nil { + // fmt.Fprintf(os.Stderr, " ERROR: syscrypt: Failed to decrypt Master Password.\n") + //} + + //_ = _masterPass + + //if syscryptcfg.Bootstrap == "0" { + // + // if challengeFlag != _masterPass { + // RecordFailure() + // fmt.Fprintf(os.Stderr, " ERROR: syscrypt: Invalid Master Password.\n") + // os.Exit(1) + // } + //} + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func ValidateJSON(path string) (bool, error) { + data, err := os.ReadFile(path) + if err != nil { + return false, fmt.Errorf("failed to read file: %w", err) + } + + var js map[string]any + if err := json.Unmarshal(data, &js); err != nil { + + var jsArray []any + if err := json.Unmarshal(data, &jsArray); err != nil { + return false, fmt.Errorf("invalid JSON format: %w", err) + } + + } + + return true, nil + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func ValidateSerial(blob []byte, keySerial string) bool { + + if len(blob) < 35 { + return false + } + + serialSize := int(blob[1]) + if len(blob) < 2+serialSize+32 { + return false + } + + maskedSerial := blob[2 : 2+serialSize] + ephPubX := blob[2+serialSize : 2+serialSize+32] + + recoveredBytes := make([]byte, serialSize) + for i := 0; i < serialSize; i++ { + recoveredBytes[i] = maskedSerial[i] ^ ephPubX[i%32] + } + recoveredSerial := string(recoveredBytes) + + if recoveredSerial != keySerial { + return false + } + + return true +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func Zeroize(data []byte) { + for i := range data { + data[i] = 0 + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func IsFileLocked(filePath string) (isLocked bool, isArmored bool, err error) { + + file, err := os.Open(filePath) + if err != nil { + return false, false, err + } + defer file.Close() + + buffer := make([]byte, 512) + n, err := file.Read(buffer) + if err != nil && n == 0 { + return false, false, err + } + + preview := string(buffer[:n]) + nPreview := strings.ReplaceAll(preview, vars.PrivateKeyHeader+"\n", "") + nPreview = strings.ReplaceAll(nPreview, vars.PrivateKeyFooter+"\n", "") + isBase64 := IsValidBase64WithLines(preview) + + if strings.HasPrefix(preview, vars.PrivateKeyHeader) { + isArmored = true + } else { + isArmored = false + } + + var content string + + if isBase64 { + + b, err := base64.StdEncoding.DecodeString(nPreview) + if err != nil { + return false, false, err + } + + content = string(b) + + } else { + + content = preview + + } + + //fmt.Printf("content: %s\n", content) + + //if !(strings.HasPrefix(content, vars.SyscryptLabel) || strings.HasPrefix(content, vars.LockedLabel)) { + // return false, false, err + //} + + lines := strings.Split(content, "\n") + contentFirstLine := strings.TrimSpace(lines[0]) + + if strings.HasPrefix(contentFirstLine, vars.LockedLabel) { + isLocked = true + } else if strings.HasPrefix(contentFirstLine, vars.SyscryptLabel) { + isLocked = false + } else { + return false, isArmored, fmt.Errorf("invalid header") + } + + //fmt.Printf("islocked: %v\n", isLocked) + //fmt.Printf("prefix default: %v\n\n", strings.HasPrefix(contentFirstLine, vars.SyscryptLabel)) + //fmt.Printf("prefix locked: %v\n\n", strings.HasPrefix(contentFirstLine, vars.LockedLabel)) + //fmt.Printf("first line: %v\n\n", contentFirstLine) + //fmt.Printf("\n------\n") + + //fmt.Printf("file path: %s\n\n", filePath) + //fmt.Printf("content: \n%s\n\n", content) + //fmt.Printf("content first line: \n%s\n\n", contentFirstLine) + //fmt.Printf("\n\n\n") + + //fmt.Printf("islocked: %v\n", isLocked) + + return isLocked, isArmored, err + + //if !strings.HasPrefix(contentFirstLine, vars.SyscryptLabel) { + // isLocked = false + //} else { + // isLocked = true + //} + + //if !strings.HasPrefix(contentFirstLine, vars.LockedLabel) { + // isLocked = false + //} else { + // isLocked = true + //} + + //return isLocked, isArmored, err +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func GetFlagValue(flagPrefix string) (string, error) { + + flagPrefix = "-" + flagPrefix + + if flagPrefix == "" { + return "", fmt.Errorf("GetFlagValue: flag prefix cannot be empty") + } + + if !strings.HasPrefix(flagPrefix, "-") { + return "", fmt.Errorf("GetFlagValue: '%s' is an invalid flag", flagPrefix) + } + + if len(flagPrefix) < 2 || !unicode.IsLetter(rune(flagPrefix[1])) { + return "", fmt.Errorf("GetFlagValue: '%s' must be followed by a letter", flagPrefix) + } + + for i, arg := range os.Args { + if arg == flagPrefix { + + if i+1 < len(os.Args) { + return os.Args[i+1], nil + } + return "", nil + + } + } + return "", nil + + //for i, arg := range os.Args { + + // for arg == flagPrefix && i+1 < len(os.Args) { + // return os.Args[i+1] + // } + + //} + + //return "" +} + +func IsValidBase64String(s string) bool { + var base64Regex = regexp.MustCompile(`^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$`) + return base64Regex.MatchString(s) +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func IsValidBase64WithLines(s string) bool { + lines := strings.Split(strings.TrimSpace(s), "\n") + validBase64 := false + + for _, line := range lines { + // + + line = strings.TrimSpace(line) + + if line == "" || strings.HasPrefix(line, "---") || strings.HasPrefix(line, "->") { + continue + } + + if _, err := base64.StdEncoding.DecodeString(line); err != nil { + return false + } + + validBase64 = true + + // + } + + return validBase64 +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func ValidatePassword(s string, priv syscrypt.PrivateKeyWrapper) bool { + + rawInput, err := os.ReadFile(s) + if err != nil { + return false + } + + var blob []byte + inputStr := string(rawInput) + if strings.Contains(inputStr, vars.PrivateKeyHeader) { + content := strings.ReplaceAll(inputStr, vars.PrivateKeyHeader, "") + content = strings.ReplaceAll(content, vars.PrivateKeyFooter, "") + content = strings.ReplaceAll(strings.ReplaceAll(content, "\n", ""), "\r", "") + blob, _ = base64.StdEncoding.DecodeString(strings.TrimSpace(content)) + } else { + blob = rawInput + } + + mode := blob[0] + serialSize := int(blob[1]) + ephOffset := 2 + serialSize + headerOffset := ephOffset + 32 + + maskedSerial := blob[2:ephOffset] + ephPubX := blob[ephOffset:headerOffset] + recoveredBytes := make([]byte, serialSize) + for i := 0; i < serialSize; i++ { + recoveredBytes[i] = maskedSerial[i] ^ ephPubX[i%32] + } + + if string(recoveredBytes) != priv.PrivateKey.Serial { + fmt.Printf("Validation failed: This file was not encrypted with the supplied key %s.", maskedSerial) + os.Exit(1) + } + + isHybrid := mode == 0x03 || mode == 0x04 + isLocked := mode == 0x02 || mode == 0x04 + + var nonce, ciphertext []byte + + if isHybrid { + nonce = blob[headerOffset+1088 : headerOffset+1100] + ciphertext = blob[headerOffset+1100:] + } else { + nonce = blob[headerOffset : headerOffset+12] + ciphertext = blob[headerOffset+12:] + } + + cleanPriv := strings.TrimPrefix(priv.PrivateKey.Key, "SYSCRYPT-PRIVATE-KEY-") + myPrivX, _ := hex.DecodeString(cleanPriv) + defer Zeroize(myPrivX) + + sharedX, _ := curve25519.X25519(myPrivX, ephPubX) + defer Zeroize(sharedX) + + var sharedML []byte + + if isHybrid { + scheme := kyber768.Scheme() + skBytes, _ := hex.DecodeString(priv.PrivateKey.MLKEMKey) + skK, _ := scheme.UnmarshalBinaryPrivateKey(skBytes) + sharedML, _ = scheme.Decapsulate(skK, blob[headerOffset:headerOffset+1088]) + Zeroize(skBytes) + defer Zeroize(sharedML) + } + + combined := append(sharedX, sharedML...) + if isLocked { + apiValue, _ := GetFlagValue("A") + passValue, _ := GetFlagValue("P") + apiKey, _ := FetchFileKey(apiValue, "key") + + if passValue == "" { + fmt.Printf("validation failed: file is password protected (-P required).") + return false + } + + passKey := argon2.IDKey([]byte(passValue), []byte(apiKey), 1, 64*1024, 4, 32) + combined = append(combined, passKey...) + Zeroize(passKey) + } + defer Zeroize(combined) + + h := hkdf.New(sha256.New, combined, nil, []byte("syscrypt-v1-hybrid")) + symmKey := make([]byte, 32) + io.ReadFull(h, symmKey) + defer Zeroize(symmKey) + + aead, _ := cc20.New(symmKey) + + _, err = aead.Open(nil, nonce, ciphertext, nil) + if err != nil { + return false // wrong password + } + + return true +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func Base64Decode(s string) (string, error) { + data, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return "", err + } + + return string(data), err + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func GetFileMode(inputValue string) byte { + rawInput, err := os.ReadFile(inputValue) + if err != nil { + return 0 + } + + var workingBlob []byte + + if len(rawInput) > 0 && rawInput[0] == 45 { + // + content := string(rawInput) + content = strings.ReplaceAll(content, vars.PrivateKeyHeader, "") + content = strings.ReplaceAll(content, vars.PrivateKeyFooter, "") + content = strings.ReplaceAll(content, "\n", "") + content = strings.ReplaceAll(content, "\r", "") + + decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(content)) + if err != nil { + return 0 + } + + workingBlob = decoded + + } else { + + workingBlob = rawInput + + } + + if len(workingBlob) >= 3 { + maskedMode := workingBlob[0] + ephKeyByte0 := workingBlob[2] + return maskedMode ^ ephKeyByte0 + } + + //if len(workingBlob) > 0 { + // return workingBlob[0] + //} + + return 0 +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func PromptPassword() string { + var styleBox = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#7158e2")). + Padding(1, 4). + MarginTop(1). + Width(60). + Align(lipgloss.Center) + + var styleTitle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#32ff7e")). + Bold(true) + + var styleBody = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#ffffff")) + + //var styleSubtext = lipgloss.NewStyle(). + // Foreground(lipgloss.Color("#808e9b")). + // Italic(true) + + title := styleTitle.Render(" PASSWORD REQUIRED ") + body := styleBody.Render("Please enter your Master Password below: ") + + promptMsg := fmt.Sprintf("%s\n\n%s\n", title, body) + fmt.Println(styleBox.Render(promptMsg)) + + bytePassword, err := term.ReadPassword(uintptr(os.Stdin.Fd())) + fmt.Println(" > ") + + if err != nil { + fmt.Printf("Error accessing terminal: %v\n", err) + os.Exit(1) + } + + password := string(bytePassword) + + if len(password) == 0 { + errorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#ff4d4d")).Bold(true) + fmt.Println(errorStyle.Render("Access Denied: Password cannot be empty.")) + os.Exit(1) + } + + return password + +} + +func ReadJson(path string, filePath string) (string, error) { + cmd := exec.Command("jq", "-r", path, filePath) + + var out bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + return "", fmt.Errorf("jq error: %v - %s", err, stderr.String()) + } + + result := bytes.TrimSpace(out.Bytes()) + + if string(result) == "null" { + return "", fmt.Errorf("jq - key not found: %s", path) + } + + return string(result), nil + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +//func DecryptFile(path string, password string) (string, error) { + +// binaryPath := config.BinaryPath + "/syscrypt" + +// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) +// defer cancel() + +// cmd := exec.CommandContext(ctx, binaryPath, "-d", "-i", config.KeyPath, "-k", password, path) +// cmd.Env = append(os.Environ(), "SYSCRYPT_INTERNAL_CALL=1") + +// out, err := cmd.CombinedOutput() +// if ctx.Err() == context.DeadlineExceeded { +// return "", fmt.Errorf(" ERROR: syscrypt: DecryptFile: timed out: %s", string(out)) +// } +// +// if err != nil { +// return "", fmt.Errorf(" ERROR: syscrypt: DecryptFile: decrypt failed: %w %s", err, string(out)) +// } +// return strings.TrimSpace(string(out)), nil +//} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/internal/vars/vars.go b/internal/vars/vars.go new file mode 100755 index 0000000..66dee2e --- /dev/null +++ b/internal/vars/vars.go @@ -0,0 +1,108 @@ +package vars + +var ( + + // tag + + Tag_P256Label = "syscrypt/p256tag" // TagLabel + Tag_MLKem768p256Label = "syscrypt/mlkem768p256tag" // MLKemLabel + Tag_Syscrypt1Tag1 = "syscrypt1tag1" // LabelSyscrypt1Tag1 + Tag_MLKem768X25519 = "syscrypt/mlkem768x25519" // MLKem768X25519 + Tag_Syscrypt1TagPq1 = "syscrypt1tagpq1" // + + // post quantum + + Pq_PrivateKeyLabel = "SYSCRYPT-PQ-PRIVATE-KEY-" // PrivateKeyPQPrefix + Pq_PublicKeyLabel = "syscrypt1pq1" // PublicKeyPQPrefix + + // plugins + + Plugin_Label = "syscrypt-plugin-" // PluginLabel + Plugin_PrivateKeyLabel = "SYSCRYPT-PLUGIN-" //PluginPrivateKeyLabel + + // ssh + + SSH_Label = "ssh-" + + // Github + + Github_label = "github:" + + // OAEP + + OAEP_Label = "syscrypt/base/v1/ssh-rsa" // OAEPLabel + + // ED25519 + + ED25519_Label = "syscrypt/base/v1/ssh-ed25519" // ED25519Label + + // X25519 + + X25519_Label = "syscrypt/base/v1/X25519" // X25519Label + + // encrypt, decrypt + + DefaultPrefixLabel = "syscrypt--" // DefaultPrefix + PrivateKeyPrefixLabel = "SYSCRYPT-PRIVATE-KEY--" // PrivateKeyPrefix + PublicKeyPrefixLabel = "syscrypt--" // PublicKeyPrefix + DefaultPrivateKeyPrefixLabel = "SYSCRYPT--" + PQPublicKeyPrefixLabel = "syscrypt-pq--" + PQPrivateKeyPrefixLabel = "SYSCRYPT-PRIVATE-KEY-PQ--" + + // Other + + SyscryptLabel = "syscrypt/base/v1/" + ScryptLabel = "syscrypt/base/v1/scrypt" + PrivateKeyHeader = "-----BEGIN ENCRYPTED DATA-----" // PrivateKeyHeader + PrivateKeyStartTrunc = "-----BEGIN" + PrivateKeyEndTrunc = "-----END" + PrivateKeyFooter = "-----END ENCRYPTED DATA-----" // PrivateKeyFooter + VersionLabel = "syscrypt/base/v1/[serial]" + + // Lock + + LockPrivateKeyStartTrunc = "-----BEGIN LOCK ENCRYPTED DATA-----" + LockPrivateKeyEndTrunc = "-----END LOCK ENCRYPTED DATA-----" + LockVersionLabel = "syscrypt/lock/v1/[serial]" + LockedLabel = "syscrypt/lock/" + LockedLabelVersion = "syscrypt/lock/v1/" + + // Anchors + + StartAnchor = "/v1/" + EndAnchor = "--" + BoundaryAnchor = []byte("---") + FooterAnchor = "---" +) + +var ( + Version string + VersionFlag bool // version + CommandFlag string // -c + ArmoredFlag bool // -a + KeyFlag string // -k + InputFlag string // -i + OutputFlag string // -o + LockFlag bool // -L + ApiFlag string // -A + MasterPass string // -P + Comment string // -COMMENT + StringFlag string // -s + PQFlag bool // post quantum + KeyPath string // -K + FriendlyName string // -f +) + +const ( + PassTagSize = 28 // 12 bytes nonce/ct + 16 bytes poly1305 tag + KyberCTSize = 1088 // Kyber768 Ciphertext size +) + +type FlagRequirement struct { + Flag string + IsRequired bool +} + +type ExpectedKeys struct { + File string +} diff --git a/syscrypt.go b/syscrypt.go new file mode 100755 index 0000000..7a156e6 --- /dev/null +++ b/syscrypt.go @@ -0,0 +1,47 @@ +package syscrypt + +import "sources.truenas.cloud/code/syscrypt/internal/vars" + +type PrivateKeyWrapper struct { + PrivateKey struct { + Key string `json:"key"` + MLKEMKey string `json:"mlkem_key,omitempty"` + Serial string `json:"serial"` + Salt string `json:"salt"` + Date string `json:"date"` + Comment string `json:"comment"` + } `json:"PrivateKey"` +} + +type PublicKeyWrapper struct { + PublicKey struct { + Key string `json:"key"` + MLKEMKey string `json:"mlkem_key,omitempty"` + Serial string `json:"serial"` + Salt string `json:"salt"` + Date string `json:"date"` + Comment string `json:"comment"` + } `json:"PublicKey"` +} + +type ApiKeyWrapper struct { + ApiKey struct { + Key string `json:"key"` + Serial string `json:"serial"` + Salt string `json:"salt"` + Date string `json:"date"` + Comment string `json:"comment"` + } `json:"APIKey"` +} + +type SaltEntry struct { + Ref string `json:"ref"` + Salt string `json:"salt"` +} + +type Vault struct { + Entries []SaltEntry `json:"entries"` +} + +var ArmorHeader = vars.PrivateKeyHeader +var ArmorFooter = vars.PrivateKeyFooter