Files

601 lines
16 KiB
Go
Raw Permalink Normal View History

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