obrimbaseapi/essential/visible/service/helper/cipher/cipher.go

809 lines
17 KiB
Go

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