/* |-------------------------------------------------------------------------- | Metadata |-------------------------------------------------------------------------- | | Name: | - Cipher | | Purpose: | - Provide reusable AES-256 encryption and decryption capabilities | for file-based data processing. | | Guideline: | - Use encrypt-standard for AES-256 encryption without salt metadata. | - Use encrypt-salted for AES-256 encryption with embedded salt, | signature, and version metadata. | - Use decrypt-standard only for files produced by | encrypt-standard. | - Use decrypt-salted only for files produced by | encrypt-salted. | - Always provide a valid AES-256 compatible key. | - Always provide valid input and output file paths. | - Do not manually modify salted file headers, version | information, or embedded salt metadata. | - Treat generated salt as non-secret metadata required for | successful decryption. | | Example: | - ObrimCipher( | "encrypt-standard", | map[string]any{ | "key": "<32-byte-key>", | "input": "./plain.txt", | "output": "./encrypted.bin", | }, | ) | | - ObrimCipher( | "encrypt-salted", | map[string]any{ | "key": "<32-byte-key>", | "input": "./plain.txt", | "output": "./encrypted.obc", | }, | ) | | - ObrimCipher( | "decrypt-standard", | map[string]any{ | "key": "<32-byte-key>", | "input": "./encrypted.bin", | "output": "./plain.txt", | }, | ) | | - ObrimCipher( | "decrypt-salted", | map[string]any{ | "key": "<32-byte-key>", | "input": "./encrypted.obc", | "output": "./plain.txt", | }, | ) | |-------------------------------------------------------------------------- */ /* |-------------------------------------------------------------------------- | Credit |-------------------------------------------------------------------------- | | Contributor: | - Rajon Ahmed | - Blockonite | |-------------------------------------------------------------------------- */ package cipher import ( "bytes" "crypto/aes" cryptocipher "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/json" "errors" "io" "os" "path/filepath" ) const ( obrimCipherTypeEncryptStandard = "encrypt-standard" obrimCipherTypeEncryptSalted = "encrypt-salted" obrimCipherTypeDecryptStandard = "decrypt-standard" obrimCipherTypeDecryptSalted = "decrypt-salted" obrimCipherAlgorithm = "AES-256" obrimCipherFormatSignature = "OBRIMCIPHER" obrimCipherFormatVersion = "1" obrimCipherSaltLength = 32 obrimCipherNonceLength = 12 ObrimCipherCodeSuccessEncryptStandard = "SUCCESS_ENCRYPT_STANDARD" ObrimCipherCodeSuccessEncryptSalted = "SUCCESS_ENCRYPT_SALTED" ObrimCipherCodeSuccessDecryptStandard = "SUCCESS_DECRYPT_STANDARD" ObrimCipherCodeSuccessDecryptSalted = "SUCCESS_DECRYPT_SALTED" ObrimCipherCodeFailedInvalidType = "FAILED_INVALID_TYPE" ObrimCipherCodeFailedInvalidConfig = "FAILED_INVALID_CONFIGURATION" ObrimCipherCodeFailedInvalidKey = "FAILED_INVALID_KEY" ObrimCipherCodeFailedInvalidInputPath = "FAILED_INVALID_INPUT_PATH" ObrimCipherCodeFailedInvalidOutputPath = "FAILED_INVALID_OUTPUT_PATH" ObrimCipherCodeFailedInputAccess = "FAILED_INPUT_ACCESS" ObrimCipherCodeFailedOutputWrite = "FAILED_OUTPUT_WRITE" ObrimCipherCodeFailedEncryption = "FAILED_ENCRYPTION" ObrimCipherCodeFailedDecryption = "FAILED_DECRYPTION" ObrimCipherCodeFailedSaltGeneration = "FAILED_SALT_GENERATION" ObrimCipherCodeFailedSignatureValidation = "FAILED_SIGNATURE_VALIDATION" ObrimCipherCodeFailedVersionValidation = "FAILED_VERSION_VALIDATION" ObrimCipherCodeFailedSaltExtraction = "FAILED_SALT_EXTRACTION" ObrimCipherCodeFailedPayloadExtraction = "FAILED_PAYLOAD_EXTRACTION" ) // ObrimCipherConfig defines utility configuration. type ObrimCipherConfig struct { Key string `json:"key"` Input string `json:"input"` Output string `json:"output"` } // ObrimCipherResponse defines standardized utility response. type ObrimCipherResponse struct { Status bool `json:"status"` Code string `json:"code"` Payload map[string]any `json:"payload"` } // obrimCipherHeader defines encrypted file metadata. type obrimCipherHeader struct { Signature string Version string } // ObrimCipher executes encryption and decryption workflows. func ObrimCipher( cipherType string, config map[string]any, ) map[string]any { if !obrimCipherValidateType(cipherType) { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedInvalidType, nil, ) } if !obrimCipherValidateConfig(config) { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedInvalidConfig, nil, ) } key := config["key"].(string) if !obrimCipherValidateKey(key) { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedInvalidKey, nil, ) } inputPath, inputOk := obrimCipherResolvePath( config["input"].(string), ) if !inputOk { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedInvalidInputPath, nil, ) } outputPath, outputOk := obrimCipherResolvePath( config["output"].(string), ) if !outputOk { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedInvalidOutputPath, nil, ) } inputData, readErr := obrimCipherReadInput(inputPath) if readErr != nil { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedInputAccess, nil, ) } switch cipherType { case obrimCipherTypeEncryptStandard: return obrimCipherEncryptStandard( key, inputPath, outputPath, inputData, ) case obrimCipherTypeEncryptSalted: return obrimCipherEncryptSalted( key, inputPath, outputPath, inputData, ) case obrimCipherTypeDecryptStandard: return obrimCipherDecryptStandard( key, inputPath, outputPath, inputData, ) case obrimCipherTypeDecryptSalted: return obrimCipherDecryptSalted( key, inputPath, outputPath, inputData, ) } return obrimCipherBuildResponse( false, ObrimCipherCodeFailedInvalidType, nil, ) } // obrimCipherValidateType validates operation type. func obrimCipherValidateType(cipherType string) bool { switch cipherType { case obrimCipherTypeEncryptStandard: return true case obrimCipherTypeEncryptSalted: return true case obrimCipherTypeDecryptStandard: return true case obrimCipherTypeDecryptSalted: return true default: return false } } // obrimCipherValidateConfig validates configuration. func obrimCipherValidateConfig(config map[string]any) bool { if config == nil { return false } keyValue, keyExists := config["key"] if !keyExists { return false } inputValue, inputExists := config["input"] if !inputExists { return false } outputValue, outputExists := config["output"] if !outputExists { return false } key, keyOk := keyValue.(string) if !keyOk || key == "" { return false } input, inputOk := inputValue.(string) if !inputOk || input == "" { return false } output, outputOk := outputValue.(string) if !outputOk || output == "" { return false } return true } // obrimCipherValidateKey validates AES-256 key requirements. func obrimCipherValidateKey(key string) bool { return len([]byte(key)) == 32 } // obrimCipherResolvePath resolves a path to absolute form. func obrimCipherResolvePath(path string) (string, bool) { resolvedPath, err := filepath.Abs(path) if err != nil { return "", false } return resolvedPath, true } // obrimCipherReadInput reads source file content. func obrimCipherReadInput(path string) ([]byte, error) { return os.ReadFile(path) } // obrimCipherWriteOutput writes processed output safely. func obrimCipherWriteOutput( path string, data []byte, ) error { var directory = filepath.Dir(path) if err := os.MkdirAll(directory, 0755); err != nil { return err } var temporaryFile, createErr = os.CreateTemp( directory, ".obrim-cipher-*", ) if createErr != nil { return createErr } var temporaryPath = temporaryFile.Name() defer func() { _ = temporaryFile.Close() }() if _, err := temporaryFile.Write(data); err != nil { _ = os.Remove(temporaryPath) return err } if err := temporaryFile.Sync(); err != nil { _ = os.Remove(temporaryPath) return err } if err := temporaryFile.Close(); err != nil { _ = os.Remove(temporaryPath) return err } return os.Rename(temporaryPath, path) } // obrimCipherBuildResponse builds standardized responses. func obrimCipherBuildResponse( status bool, code string, payload map[string]any, ) map[string]any { return map[string]any{ "status": status, "code": code, "payload": payload, } } // obrimCipherEncryptStandard encrypts data without salt metadata. func obrimCipherEncryptStandard( key string, inputPath string, outputPath string, inputData []byte, ) map[string]any { encryptedData, err := obrimCipherEncryptAES( []byte(key), inputData, ) if err != nil { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedEncryption, nil, ) } if err := obrimCipherWriteOutput( outputPath, encryptedData, ); err != nil { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedOutputWrite, nil, ) } return obrimCipherBuildResponse( true, ObrimCipherCodeSuccessEncryptStandard, map[string]any{ "operation": obrimCipherTypeEncryptStandard, "algorithm": obrimCipherAlgorithm, "inputPath": inputPath, "outputPath": outputPath, }, ) } // obrimCipherGenerateSalt generates a cryptographic salt. func obrimCipherGenerateSalt() ([]byte, error) { var salt = make([]byte, obrimCipherSaltLength) _, err := io.ReadFull(rand.Reader, salt) return salt, err } // obrimCipherBuildHeader builds encrypted file header. func obrimCipherBuildHeader() []byte { var buffer bytes.Buffer buffer.WriteString(obrimCipherFormatSignature) buffer.WriteByte(0) buffer.WriteString(obrimCipherFormatVersion) buffer.WriteByte(0) return buffer.Bytes() } // obrimCipherEncryptSalted encrypts data with embedded salt metadata. func obrimCipherEncryptSalted( key string, inputPath string, outputPath string, inputData []byte, ) map[string]any { salt, saltErr := obrimCipherGenerateSalt() if saltErr != nil { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedSaltGeneration, nil, ) } var derivedKey = sha256.Sum256( append([]byte(key), salt...), ) encryptedData, err := obrimCipherEncryptAES( derivedKey[:], inputData, ) if err != nil { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedEncryption, nil, ) } var header = obrimCipherBuildHeader() var outputData []byte outputData = append(outputData, header...) outputData = append(outputData, salt...) outputData = append(outputData, encryptedData...) if err := obrimCipherWriteOutput( outputPath, outputData, ); err != nil { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedOutputWrite, nil, ) } return obrimCipherBuildResponse( true, ObrimCipherCodeSuccessEncryptSalted, map[string]any{ "operation": obrimCipherTypeEncryptSalted, "algorithm": obrimCipherAlgorithm, "format": obrimCipherFormatSignature, "version": obrimCipherFormatVersion, "inputPath": inputPath, "outputPath": outputPath, }, ) } // obrimCipherDecryptStandard decrypts AES-256 encrypted data. func obrimCipherDecryptStandard( key string, inputPath string, outputPath string, inputData []byte, ) map[string]any { decryptedData, err := obrimCipherDecryptAES( []byte(key), inputData, ) if err != nil { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedDecryption, nil, ) } if err := obrimCipherWriteOutput( outputPath, decryptedData, ); err != nil { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedOutputWrite, nil, ) } return obrimCipherBuildResponse( true, ObrimCipherCodeSuccessDecryptStandard, map[string]any{ "operation": obrimCipherTypeDecryptStandard, "algorithm": obrimCipherAlgorithm, "inputPath": inputPath, "outputPath": outputPath, }, ) } // obrimCipherParseHeader parses encrypted metadata. func obrimCipherParseHeader( data []byte, ) (obrimCipherHeader, int, error) { var signatureBytes = []byte(obrimCipherFormatSignature) if len(data) < len(signatureBytes)+1 { return obrimCipherHeader{}, 0, errors.New("signature") } if !bytes.Equal( data[:len(signatureBytes)], signatureBytes, ) { return obrimCipherHeader{}, 0, errors.New("signature") } var offset = len(signatureBytes) if data[offset] != 0 { return obrimCipherHeader{}, 0, errors.New("signature") } offset++ var versionBytes = []byte(obrimCipherFormatVersion) if len(data) < offset+len(versionBytes)+1 { return obrimCipherHeader{}, 0, errors.New("version") } if !bytes.Equal( data[offset:offset+len(versionBytes)], versionBytes, ) { return obrimCipherHeader{}, 0, errors.New("version") } offset += len(versionBytes) if data[offset] != 0 { return obrimCipherHeader{}, 0, errors.New("version") } offset++ return obrimCipherHeader{ Signature: obrimCipherFormatSignature, Version: obrimCipherFormatVersion, }, offset, nil } // obrimCipherExtractSalt extracts embedded salt. func obrimCipherExtractSalt( data []byte, offset int, ) ([]byte, error) { if len(data) < offset+obrimCipherSaltLength { return nil, errors.New("salt") } return data[offset : offset+obrimCipherSaltLength], nil } // obrimCipherExtractPayload extracts encrypted payload. func obrimCipherExtractPayload( data []byte, offset int, ) ([]byte, error) { var payloadOffset = offset + obrimCipherSaltLength if len(data) <= payloadOffset { return nil, errors.New("payload") } return data[payloadOffset:], nil } // obrimCipherDecryptSalted decrypts AES-256 encrypted data using extracted salt. func obrimCipherDecryptSalted( key string, inputPath string, outputPath string, inputData []byte, ) map[string]any { header, offset, headerErr := obrimCipherParseHeader( inputData, ) if headerErr != nil { if headerErr.Error() == "signature" { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedSignatureValidation, nil, ) } return obrimCipherBuildResponse( false, ObrimCipherCodeFailedVersionValidation, nil, ) } salt, saltErr := obrimCipherExtractSalt( inputData, offset, ) if saltErr != nil { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedSaltExtraction, nil, ) } payload, payloadErr := obrimCipherExtractPayload( inputData, offset, ) if payloadErr != nil { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedPayloadExtraction, nil, ) } var derivedKey = sha256.Sum256( append([]byte(key), salt...), ) decryptedData, decryptErr := obrimCipherDecryptAES( derivedKey[:], payload, ) if decryptErr != nil { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedDecryption, nil, ) } if err := obrimCipherWriteOutput( outputPath, decryptedData, ); err != nil { return obrimCipherBuildResponse( false, ObrimCipherCodeFailedOutputWrite, nil, ) } return obrimCipherBuildResponse( true, ObrimCipherCodeSuccessDecryptSalted, map[string]any{ "operation": obrimCipherTypeDecryptSalted, "algorithm": obrimCipherAlgorithm, "format": header.Signature, "version": header.Version, "inputPath": inputPath, "outputPath": outputPath, }, ) } // obrimCipherEncryptAES performs AES-256 encryption. func obrimCipherEncryptAES( key []byte, data []byte, ) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cryptocipher.NewGCM(block) if err != nil { return nil, err } var nonce = make([]byte, obrimCipherNonceLength) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return nil, err } var encryptedData = gcm.Seal( nil, nonce, data, nil, ) var result []byte result = append(result, nonce...) result = append(result, encryptedData...) return result, nil } // obrimCipherDecryptAES performs AES-256 decryption. func obrimCipherDecryptAES( key []byte, data []byte, ) ([]byte, error) { if len(data) < obrimCipherNonceLength { return nil, errors.New("invalid payload") } block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cryptocipher.NewGCM(block) if err != nil { return nil, err } var nonce = data[:obrimCipherNonceLength] var payload = data[obrimCipherNonceLength:] return gcm.Open( nil, nonce, payload, nil, ) } // obrimCipherMarshalResponse marshals response data. func obrimCipherMarshalResponse( response ObrimCipherResponse, ) ([]byte, error) { return json.Marshal(response) }