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 */ } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////