/* |-------------------------------------------------------------------------- | Description |-------------------------------------------------------------------------- | | Name: | - Cipher | | Purpose: | - Provide reusable AES-256 encryption and decryption for file content. | - Support standard and salted encryption formats through a unified interface. | |-------------------------------------------------------------------------- */ /* |-------------------------------------------------------------------------- | Instruction |-------------------------------------------------------------------------- | | Guideline: | - Use encrypt-standard to encrypt file content using AES-256 without salt. | - Use encrypt-salted to encrypt file content using AES-256 with a cryptographically secure generated salt. | - Use decrypt-standard to decrypt AES-256 encrypted file content without salt metadata. | - Use decrypt-salted to decrypt self-contained encrypted files with embedded signature, version, salt, and ciphertext metadata. | - Supply the AES-256 compatible key, input file path, and output file path through the configuration. | | Example: | - ObrimCipher("encrypt-standard", map[string]any{ | "key": key, | "input": inputPath, | "output": outputPath, | }) | | - ObrimCipher("encrypt-salted", map[string]any{ | "key": key, | "input": inputPath, | "output": outputPath, | }) | | - ObrimCipher("decrypt-standard", map[string]any{ | "key": key, | "input": inputPath, | "output": outputPath, | }) | | - ObrimCipher("decrypt-salted", map[string]any{ | "key": key, | "input": inputPath, | "output": outputPath, | }) | |-------------------------------------------------------------------------- */ /* |-------------------------------------------------------------------------- | Credit |-------------------------------------------------------------------------- | | Contributor: | - Rajon Ahmed | - Blockonite | |-------------------------------------------------------------------------- */ package cipher import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "errors" "fmt" "os" "path/filepath" ) // Utility execution type identifiers. const ( obrimCipherTypeEncryptStandard = "encrypt-standard" obrimCipherTypeEncryptSalted = "encrypt-salted" obrimCipherTypeDecryptStandard = "decrypt-standard" obrimCipherTypeDecryptSalted = "decrypt-salted" ) // Cipher format metadata values. const ( obrimCipherAlgorithm = "AES-256-GCM" obrimCipherFormat = "OBRIM-CIPHER" obrimCipherVersion = "1" obrimCipherSaltSize = 32 ) // Utility execution success result codes. const ( obrimCipherSuccessEncryptStandard = "SUCCESS_ENCRYPT_STANDARD" obrimCipherSuccessEncryptSalted = "SUCCESS_ENCRYPT_SALTED" obrimCipherSuccessDecryptStandard = "SUCCESS_DECRYPT_STANDARD" obrimCipherSuccessDecryptSalted = "SUCCESS_DECRYPT_SALTED" ) // Utility execution failure result codes. const ( obrimCipherFailureInvalidType = "FAILURE_INVALID_TYPE" obrimCipherFailureInvalidConfig = "FAILURE_INVALID_CONFIG" obrimCipherFailureMissingKey = "FAILURE_MISSING_KEY" obrimCipherFailureInvalidKey = "FAILURE_INVALID_KEY" obrimCipherFailureMissingInput = "FAILURE_MISSING_INPUT" obrimCipherFailureInvalidInput = "FAILURE_INVALID_INPUT" obrimCipherFailureMissingOutput = "FAILURE_MISSING_OUTPUT" obrimCipherFailureInvalidOutput = "FAILURE_INVALID_OUTPUT" obrimCipherFailureExternalSalt = "FAILURE_EXTERNAL_SALT" obrimCipherFailureReadInput = "FAILURE_READ_INPUT" obrimCipherFailureWriteOutput = "FAILURE_WRITE_OUTPUT" obrimCipherFailureCipherInitialization = "FAILURE_CIPHER_INITIALIZATION" obrimCipherFailureNonceGeneration = "FAILURE_NONCE_GENERATION" obrimCipherFailureEncryption = "FAILURE_ENCRYPTION" obrimCipherFailureDecryption = "FAILURE_DECRYPTION" obrimCipherFailureSaltGeneration = "FAILURE_SALT_GENERATION" obrimCipherFailureHeaderBuild = "FAILURE_HEADER_BUILD" obrimCipherFailureSignature = "FAILURE_INVALID_SIGNATURE" obrimCipherFailureVersion = "FAILURE_INVALID_VERSION" obrimCipherFailureSaltExtraction = "FAILURE_SALT_EXTRACTION" obrimCipherFailurePayloadExtraction = "FAILURE_PAYLOAD_EXTRACTION" ) // Standardized utility output. type obrimCipherOutput struct { Status bool Code string Payload any } // Salted encrypted file header. type obrimCipherHeader struct { Signature []byte Version []byte Salt []byte } // Standardized operation payload. type obrimCipherPayload struct { Operation string Algorithm string Format string Version string InputPath string OutputPath string } // ObrimCipher executes encryption or decryption operations through a unified interface. func ObrimCipher(typeName string, config map[string]any) map[string]any { if code := obrimCipherValidateInput(typeName, config); code != "" { return obrimCipherBuildOutput(false, code, nil) } payload, code := obrimCipherRouteRequest(typeName, config) if code != "" { return obrimCipherBuildOutput(false, code, nil) } return obrimCipherBuildOutput(true, code, payload) } // obrimCipherValidateInput validates the requested operation and its configuration. func obrimCipherValidateInput(typeName string, config map[string]any) string { switch typeName { case obrimCipherTypeEncryptStandard, obrimCipherTypeEncryptSalted, obrimCipherTypeDecryptStandard, obrimCipherTypeDecryptSalted: default: return obrimCipherFailureInvalidType } if config == nil { return obrimCipherFailureInvalidConfig } key, ok := config["key"] if !ok || key == nil { return obrimCipherFailureMissingKey } if !obrimCipherValidKey(key) { return obrimCipherFailureInvalidKey } input, ok := config["input"] if !ok || input == nil { return obrimCipherFailureMissingInput } inputPath, ok := input.(string) if !ok || inputPath == "" { return obrimCipherFailureInvalidInput } output, ok := config["output"] if !ok || output == nil { return obrimCipherFailureMissingOutput } outputPath, ok := output.(string) if !ok || outputPath == "" { return obrimCipherFailureInvalidOutput } switch typeName { case obrimCipherTypeEncryptSalted, obrimCipherTypeDecryptSalted: if _, exists := config["salt"]; exists { return obrimCipherFailureExternalSalt } } return "" } // obrimCipherRouteRequest routes a validated request to its operation-specific implementation. func obrimCipherRouteRequest(typeName string, config map[string]any) (map[string]any, string) { switch typeName { case obrimCipherTypeEncryptStandard: return obrimCipherEncryptStandard(config) case obrimCipherTypeEncryptSalted: return obrimCipherEncryptSalted(config) case obrimCipherTypeDecryptStandard: return obrimCipherDecryptStandard(config) case obrimCipherTypeDecryptSalted: return obrimCipherDecryptSalted(config) default: return nil, obrimCipherFailureInvalidType } } // obrimCipherBuildOutput builds the standardized utility output. func obrimCipherBuildOutput(status bool, code string, payload map[string]any) map[string]any { return map[string]any{ "status": status, "code": code, "payload": payload, } } // obrimCipherEncryptStandard encrypts file content without salt metadata. func obrimCipherEncryptStandard(config map[string]any) (map[string]any, string) { key, ok := obrimCipherKey(config["key"]) if !ok { return nil, obrimCipherFailureInvalidKey } inputPath, outputPath, ok := obrimCipherPaths(config) if !ok { return nil, obrimCipherFailureInvalidConfig } plaintext, err := os.ReadFile(inputPath) if err != nil { return nil, obrimCipherFailureReadInput } ciphertext, code := obrimCipherEncrypt(key, plaintext, nil) if code != "" { return nil, code } if err := os.WriteFile(outputPath, ciphertext, 0600); err != nil { return nil, obrimCipherFailureWriteOutput } return map[string]any{ "operation": obrimCipherTypeEncryptStandard, "algorithm": obrimCipherAlgorithm, "inputPath": inputPath, "outputPath": outputPath, }, obrimCipherSuccessEncryptStandard } // obrimCipherGenerateSalt generates a cryptographically secure salt. func obrimCipherGenerateSalt() ([]byte, string) { salt := make([]byte, obrimCipherSaltSize) if _, err := rand.Read(salt); err != nil { return nil, obrimCipherFailureSaltGeneration } return salt, "" } // obrimCipherBuildHeader builds the salted cipher signature, version, and salt header. func obrimCipherBuildHeader(salt []byte) ([]byte, string) { if len(salt) != obrimCipherSaltSize { return nil, obrimCipherFailureHeaderBuild } signature := []byte(obrimCipherFormat) version := []byte(obrimCipherVersion) header := make([]byte, 0, len(signature)+len(version)+len(salt)) header = append(header, signature...) header = append(header, version...) header = append(header, salt...) return header, "" } // obrimCipherEncryptSalted encrypts file content with embedded salt metadata. func obrimCipherEncryptSalted(config map[string]any) (map[string]any, string) { key, ok := obrimCipherKey(config["key"]) if !ok { return nil, obrimCipherFailureInvalidKey } inputPath, outputPath, ok := obrimCipherPaths(config) if !ok { return nil, obrimCipherFailureInvalidConfig } plaintext, err := os.ReadFile(inputPath) if err != nil { return nil, obrimCipherFailureReadInput } salt, code := obrimCipherGenerateSalt() if code != "" { return nil, code } header, code := obrimCipherBuildHeader(salt) if code != "" { return nil, code } ciphertext, code := obrimCipherEncrypt(key, plaintext, salt) if code != "" { return nil, code } output := make([]byte, 0, len(header)+len(ciphertext)) output = append(output, header...) output = append(output, ciphertext...) if err := os.WriteFile(outputPath, output, 0600); err != nil { return nil, obrimCipherFailureWriteOutput } return map[string]any{ "operation": obrimCipherTypeEncryptSalted, "algorithm": obrimCipherAlgorithm, "format": obrimCipherFormat, "version": obrimCipherVersion, "inputPath": inputPath, "outputPath": outputPath, }, obrimCipherSuccessEncryptSalted } // obrimCipherDecryptStandard decrypts file content without salt metadata. func obrimCipherDecryptStandard(config map[string]any) (map[string]any, string) { key, ok := obrimCipherKey(config["key"]) if !ok { return nil, obrimCipherFailureInvalidKey } inputPath, outputPath, ok := obrimCipherPaths(config) if !ok { return nil, obrimCipherFailureInvalidConfig } ciphertext, err := os.ReadFile(inputPath) if err != nil { return nil, obrimCipherFailureReadInput } plaintext, code := obrimCipherDecrypt(key, ciphertext, nil) if code != "" { return nil, code } if err := os.WriteFile(outputPath, plaintext, 0600); err != nil { return nil, obrimCipherFailureWriteOutput } return map[string]any{ "operation": obrimCipherTypeDecryptStandard, "algorithm": obrimCipherAlgorithm, "inputPath": inputPath, "outputPath": outputPath, }, obrimCipherSuccessDecryptStandard } // obrimCipherParseHeader parses the signature and version metadata. func obrimCipherParseHeader(data []byte) (obrimCipherHeader, string) { signatureLength := len(obrimCipherFormat) versionLength := len(obrimCipherVersion) minimumLength := signatureLength + versionLength + obrimCipherSaltSize if len(data) < minimumLength { return obrimCipherHeader{}, obrimCipherFailureSignature } signature := data[:signatureLength] if string(signature) != obrimCipherFormat { return obrimCipherHeader{}, obrimCipherFailureSignature } versionStart := signatureLength versionEnd := versionStart + versionLength version := data[versionStart:versionEnd] if string(version) != obrimCipherVersion { return obrimCipherHeader{}, obrimCipherFailureVersion } saltStart := versionEnd saltEnd := saltStart + obrimCipherSaltSize return obrimCipherHeader{ Signature: append([]byte(nil), signature...), Version: append([]byte(nil), version...), Salt: append([]byte(nil), data[saltStart:saltEnd]...), }, "" } // obrimCipherExtractSalt extracts the embedded salt from a parsed salted file. func obrimCipherExtractSalt(data []byte, header obrimCipherHeader) ([]byte, string) { if len(header.Salt) != obrimCipherSaltSize { return nil, obrimCipherFailureSaltExtraction } signatureLength := len(header.Signature) versionLength := len(header.Version) saltStart := signatureLength + versionLength saltEnd := saltStart + obrimCipherSaltSize if saltStart < 0 || saltEnd > len(data) { return nil, obrimCipherFailureSaltExtraction } return append([]byte(nil), data[saltStart:saltEnd]...), "" } // obrimCipherExtractPayload extracts the encrypted payload after the salted header. func obrimCipherExtractPayload(data []byte, header obrimCipherHeader) ([]byte, string) { payloadStart := len(header.Signature) + len(header.Version) + len(header.Salt) if payloadStart >= len(data) { return nil, obrimCipherFailurePayloadExtraction } payload := data[payloadStart:] if len(payload) <= 0 { return nil, obrimCipherFailurePayloadExtraction } return append([]byte(nil), payload...), "" } // obrimCipherDecryptSalted decrypts file content using its embedded salt metadata. func obrimCipherDecryptSalted(config map[string]any) (map[string]any, string) { key, ok := obrimCipherKey(config["key"]) if !ok { return nil, obrimCipherFailureInvalidKey } inputPath, outputPath, ok := obrimCipherPaths(config) if !ok { return nil, obrimCipherFailureInvalidConfig } data, err := os.ReadFile(inputPath) if err != nil { return nil, obrimCipherFailureReadInput } header, code := obrimCipherParseHeader(data) if code != "" { return nil, code } salt, code := obrimCipherExtractSalt(data, header) if code != "" { return nil, code } payload, code := obrimCipherExtractPayload(data, header) if code != "" { return nil, code } plaintext, code := obrimCipherDecrypt(key, payload, salt) if code != "" { return nil, code } if err := os.WriteFile(outputPath, plaintext, 0600); err != nil { return nil, obrimCipherFailureWriteOutput } return map[string]any{ "operation": obrimCipherTypeDecryptSalted, "algorithm": obrimCipherAlgorithm, "format": string(header.Signature), "version": string(header.Version), "inputPath": inputPath, "outputPath": outputPath, }, obrimCipherSuccessDecryptSalted } // obrimCipherEncrypt performs AES-256-GCM encryption and prefixes the nonce to the ciphertext. func obrimCipherEncrypt(key []byte, plaintext []byte, salt []byte) ([]byte, string) { derivedKey := obrimCipherDeriveKey(key, salt) block, err := aes.NewCipher(derivedKey) if err != nil { return nil, obrimCipherFailureCipherInitialization } aead, err := cipher.NewGCM(block) if err != nil { return nil, obrimCipherFailureCipherInitialization } nonce := make([]byte, aead.NonceSize()) if _, err := rand.Read(nonce); err != nil { return nil, obrimCipherFailureNonceGeneration } ciphertext := aead.Seal(nil, nonce, plaintext, nil) output := make([]byte, 0, len(nonce)+len(ciphertext)) output = append(output, nonce...) output = append(output, ciphertext...) return output, "" } // obrimCipherDecrypt performs AES-256-GCM decryption after extracting the nonce. func obrimCipherDecrypt(key []byte, ciphertext []byte, salt []byte) ([]byte, string) { derivedKey := obrimCipherDeriveKey(key, salt) block, err := aes.NewCipher(derivedKey) if err != nil { return nil, obrimCipherFailureCipherInitialization } aead, err := cipher.NewGCM(block) if err != nil { return nil, obrimCipherFailureCipherInitialization } nonceSize := aead.NonceSize() if len(ciphertext) <= nonceSize { return nil, obrimCipherFailureDecryption } nonce := ciphertext[:nonceSize] payload := ciphertext[nonceSize:] plaintext, err := aead.Open(nil, nonce, payload, nil) if err != nil { return nil, obrimCipherFailureDecryption } return plaintext, "" } // obrimCipherDeriveKey derives the AES-256 key from the supplied key and optional salt. func obrimCipherDeriveKey(key []byte, salt []byte) []byte { if len(salt) == 0 { return append([]byte(nil), key...) } digest := sha256.New() _, _ = digest.Write(key) _, _ = digest.Write(salt) return digest.Sum(nil) } // obrimCipherKey normalizes a supported AES-256 key value. func obrimCipherKey(value any) ([]byte, bool) { switch key := value.(type) { case string: if len([]byte(key)) != 32 { return nil, false } return []byte(key), true case []byte: if len(key) != 32 { return nil, false } return append([]byte(nil), key...), true default: return nil, false } } // obrimCipherValidKey validates that a supplied key is AES-256 compatible. func obrimCipherValidKey(value any) bool { _, ok := obrimCipherKey(value) return ok } // obrimCipherPaths resolves and validates the configured input and output paths. func obrimCipherPaths(config map[string]any) (string, string, bool) { input, inputOK := config["input"].(string) output, outputOK := config["output"].(string) if !inputOK || !outputOK || input == "" || output == "" { return "", "", false } inputPath, err := filepath.Abs(input) if err != nil { return "", "", false } outputPath, err := filepath.Abs(output) if err != nil { return "", "", false } if inputPath == outputPath { return "", "", false } if _, err := os.Stat(inputPath); err != nil { return "", "", false } return inputPath, outputPath, true } // obrimCipherError provides a deterministic error representation for internal callers. func obrimCipherError(code string) error { return errors.New(fmt.Sprintf("cipher: %s", code)) }