431 lines
10 KiB
Go
431 lines
10 KiB
Go
package cipher
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
const (
|
|
obrimCipherAlgorithm = "AES-256"
|
|
obrimCipherSignature = "OBRIMCIPHER"
|
|
obrimCipherVersion = "1"
|
|
obrimCipherSaltSize = 32
|
|
obrimCipherNonceSize = 12
|
|
)
|
|
|
|
type ObrimCipherConfig struct {
|
|
Key string `json:"key"`
|
|
Input string `json:"input"`
|
|
Output string `json:"output"`
|
|
}
|
|
|
|
type obrimCipherResponse struct {
|
|
Status bool `json:"status"`
|
|
Code string `json:"code"`
|
|
Payload interface{} `json:"payload"`
|
|
}
|
|
|
|
type obrimCipherStandardPayload struct {
|
|
Operation string `json:"operation"`
|
|
Algorithm string `json:"algorithm"`
|
|
InputPath string `json:"inputPath"`
|
|
OutputPath string `json:"outputPath"`
|
|
}
|
|
|
|
type obrimCipherSaltedPayload struct {
|
|
Operation string `json:"operation"`
|
|
Algorithm string `json:"algorithm"`
|
|
Format string `json:"format"`
|
|
Version string `json:"version"`
|
|
InputPath string `json:"inputPath"`
|
|
OutputPath string `json:"outputPath"`
|
|
}
|
|
|
|
// ObrimCipher executes encryption or decryption workflows.
|
|
func ObrimCipher(operationType string, config ObrimCipherConfig) ([]byte, error) {
|
|
if err := obrimCipherValidateType(operationType); err != nil {
|
|
return obrimCipherBuildResponse(false, "FAILED_INVALID_TYPE", nil)
|
|
}
|
|
|
|
if err := obrimCipherValidateConfig(config); err != nil {
|
|
return obrimCipherBuildResponse(false, "FAILED_INVALID_CONFIG", nil)
|
|
}
|
|
|
|
if err := obrimCipherValidateKey(config.Key); err != nil {
|
|
return obrimCipherBuildResponse(false, "FAILED_INVALID_KEY", nil)
|
|
}
|
|
|
|
inputPath, err := obrimCipherResolvePath(config.Input)
|
|
if err != nil {
|
|
return obrimCipherBuildResponse(false, "FAILED_INVALID_INPUT_PATH", nil)
|
|
}
|
|
|
|
outputPath, err := obrimCipherResolvePath(config.Output)
|
|
if err != nil {
|
|
return obrimCipherBuildResponse(false, "FAILED_INVALID_OUTPUT_PATH", nil)
|
|
}
|
|
|
|
inputData, err := obrimCipherReadInput(inputPath)
|
|
if err != nil {
|
|
return obrimCipherBuildResponse(false, "FAILED_INPUT_ACCESS", nil)
|
|
}
|
|
|
|
var outputData []byte
|
|
|
|
switch operationType {
|
|
case "encrypt-standard":
|
|
outputData, err = obrimCipherEncryptStandard(inputData, config.Key)
|
|
|
|
case "encrypt-salted":
|
|
outputData, err = obrimCipherEncryptSalted(inputData, config.Key)
|
|
|
|
case "decrypt-standard":
|
|
outputData, err = obrimCipherDecryptStandard(inputData, config.Key)
|
|
|
|
case "decrypt-salted":
|
|
outputData, err = obrimCipherDecryptSalted(inputData, config.Key)
|
|
}
|
|
|
|
if err != nil {
|
|
switch operationType {
|
|
case "encrypt-standard", "encrypt-salted":
|
|
return obrimCipherBuildResponse(false, "FAILED_ENCRYPTION", nil)
|
|
case "decrypt-standard", "decrypt-salted":
|
|
return obrimCipherBuildResponse(false, "FAILED_DECRYPTION", nil)
|
|
}
|
|
}
|
|
|
|
if err := obrimCipherWriteOutput(outputPath, outputData); err != nil {
|
|
return obrimCipherBuildResponse(false, "FAILED_OUTPUT_WRITE", nil)
|
|
}
|
|
|
|
switch operationType {
|
|
case "encrypt-standard":
|
|
return obrimCipherBuildResponse(
|
|
true,
|
|
"SUCCESS_ENCRYPT_STANDARD",
|
|
obrimCipherStandardPayload{
|
|
Operation: operationType,
|
|
Algorithm: obrimCipherAlgorithm,
|
|
InputPath: inputPath,
|
|
OutputPath: outputPath,
|
|
},
|
|
)
|
|
|
|
case "decrypt-standard":
|
|
return obrimCipherBuildResponse(
|
|
true,
|
|
"SUCCESS_DECRYPT_STANDARD",
|
|
obrimCipherStandardPayload{
|
|
Operation: operationType,
|
|
Algorithm: obrimCipherAlgorithm,
|
|
InputPath: inputPath,
|
|
OutputPath: outputPath,
|
|
},
|
|
)
|
|
|
|
case "encrypt-salted":
|
|
return obrimCipherBuildResponse(
|
|
true,
|
|
"SUCCESS_ENCRYPT_SALTED",
|
|
obrimCipherSaltedPayload{
|
|
Operation: operationType,
|
|
Algorithm: obrimCipherAlgorithm,
|
|
Format: obrimCipherSignature,
|
|
Version: obrimCipherVersion,
|
|
InputPath: inputPath,
|
|
OutputPath: outputPath,
|
|
},
|
|
)
|
|
|
|
case "decrypt-salted":
|
|
return obrimCipherBuildResponse(
|
|
true,
|
|
"SUCCESS_DECRYPT_SALTED",
|
|
obrimCipherSaltedPayload{
|
|
Operation: operationType,
|
|
Algorithm: obrimCipherAlgorithm,
|
|
Format: obrimCipherSignature,
|
|
Version: obrimCipherVersion,
|
|
InputPath: inputPath,
|
|
OutputPath: outputPath,
|
|
},
|
|
)
|
|
}
|
|
|
|
return obrimCipherBuildResponse(false, "FAILED_UNKNOWN_OPERATION", nil)
|
|
}
|
|
|
|
// obrimCipherValidateType validates operation type.
|
|
func obrimCipherValidateType(operationType string) error {
|
|
switch operationType {
|
|
case "encrypt-standard",
|
|
"encrypt-salted",
|
|
"decrypt-standard",
|
|
"decrypt-salted":
|
|
return nil
|
|
default:
|
|
return errors.New("invalid operation type")
|
|
}
|
|
}
|
|
|
|
// obrimCipherValidateConfig validates configuration values.
|
|
func obrimCipherValidateConfig(config ObrimCipherConfig) error {
|
|
if config.Key == "" {
|
|
return errors.New("missing key")
|
|
}
|
|
|
|
if config.Input == "" {
|
|
return errors.New("missing input")
|
|
}
|
|
|
|
if config.Output == "" {
|
|
return errors.New("missing output")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// obrimCipherValidateKey validates AES-256 key requirements.
|
|
func obrimCipherValidateKey(key string) error {
|
|
if len([]byte(key)) != 32 {
|
|
return errors.New("invalid aes-256 key")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// obrimCipherResolvePath resolves paths to absolute paths.
|
|
func obrimCipherResolvePath(path string) (string, error) {
|
|
return filepath.Abs(path)
|
|
}
|
|
|
|
// obrimCipherReadInput reads source file content.
|
|
func obrimCipherReadInput(path string) ([]byte, error) {
|
|
return os.ReadFile(path)
|
|
}
|
|
|
|
// obrimCipherWriteOutput writes processed output.
|
|
func obrimCipherWriteOutput(path string, data []byte) error {
|
|
directory := filepath.Dir(path)
|
|
|
|
if err := os.MkdirAll(directory, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
tempFile, err := os.CreateTemp(directory, ".obrim-cipher-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tempPath := tempFile.Name()
|
|
|
|
defer func() {
|
|
_ = tempFile.Close()
|
|
}()
|
|
|
|
if _, err := tempFile.Write(data); err != nil {
|
|
_ = os.Remove(tempPath)
|
|
return err
|
|
}
|
|
|
|
if err := tempFile.Sync(); err != nil {
|
|
_ = os.Remove(tempPath)
|
|
return err
|
|
}
|
|
|
|
if err := tempFile.Close(); err != nil {
|
|
_ = os.Remove(tempPath)
|
|
return err
|
|
}
|
|
|
|
return os.Rename(tempPath, path)
|
|
}
|
|
|
|
// obrimCipherBuildResponse builds standardized responses.
|
|
func obrimCipherBuildResponse(status bool, code string, payload interface{}) ([]byte, error) {
|
|
response := obrimCipherResponse{
|
|
Status: status,
|
|
Code: code,
|
|
Payload: payload,
|
|
}
|
|
|
|
result, err := json.Marshal(response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// obrimCipherEncryptStandard encrypts data without salt metadata.
|
|
func obrimCipherEncryptStandard(data []byte, key string) ([]byte, error) {
|
|
return obrimCipherEncryptAES(data, []byte(key))
|
|
}
|
|
|
|
// obrimCipherGenerateSalt generates cryptographically secure salt.
|
|
func obrimCipherGenerateSalt() ([]byte, error) {
|
|
salt := make([]byte, obrimCipherSaltSize)
|
|
|
|
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return salt, nil
|
|
}
|
|
|
|
// obrimCipherBuildHeader builds encrypted file header.
|
|
func obrimCipherBuildHeader() []byte {
|
|
var buffer bytes.Buffer
|
|
|
|
buffer.WriteString(obrimCipherSignature)
|
|
buffer.WriteByte('|')
|
|
buffer.WriteString(obrimCipherVersion)
|
|
buffer.WriteByte('|')
|
|
|
|
return buffer.Bytes()
|
|
}
|
|
|
|
// obrimCipherEncryptSalted encrypts data with embedded salt metadata.
|
|
func obrimCipherEncryptSalted(data []byte, key string) ([]byte, error) {
|
|
salt, err := obrimCipherGenerateSalt()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
derivedKey := sha256.Sum256(append([]byte(key), salt...))
|
|
|
|
encrypted, err := obrimCipherEncryptAES(data, derivedKey[:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
header := obrimCipherBuildHeader()
|
|
|
|
result := make([]byte, 0, len(header)+len(salt)+len(encrypted))
|
|
result = append(result, header...)
|
|
result = append(result, salt...)
|
|
result = append(result, encrypted...)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// obrimCipherDecryptStandard decrypts AES-256 encrypted data.
|
|
func obrimCipherDecryptStandard(data []byte, key string) ([]byte, error) {
|
|
return obrimCipherDecryptAES(data, []byte(key))
|
|
}
|
|
|
|
// obrimCipherParseHeader parses encrypted metadata.
|
|
func obrimCipherParseHeader(data []byte) (string, string, int, error) {
|
|
header := []byte(obrimCipherSignature + "|" + obrimCipherVersion + "|")
|
|
|
|
if len(data) < len(header) {
|
|
return "", "", 0, errors.New("invalid header")
|
|
}
|
|
|
|
if !bytes.Equal(data[:len(header)], header) {
|
|
return "", "", 0, errors.New("invalid header")
|
|
}
|
|
|
|
return obrimCipherSignature, obrimCipherVersion, len(header), nil
|
|
}
|
|
|
|
// obrimCipherExtractSalt extracts embedded salt.
|
|
func obrimCipherExtractSalt(data []byte, offset int) ([]byte, error) {
|
|
if len(data) < offset+obrimCipherSaltSize {
|
|
return nil, errors.New("salt extraction failed")
|
|
}
|
|
|
|
return data[offset : offset+obrimCipherSaltSize], nil
|
|
}
|
|
|
|
// obrimCipherExtractPayload extracts encrypted payload.
|
|
func obrimCipherExtractPayload(data []byte, offset int) ([]byte, error) {
|
|
start := offset + obrimCipherSaltSize
|
|
|
|
if len(data) <= start {
|
|
return nil, errors.New("payload extraction failed")
|
|
}
|
|
|
|
return data[start:], nil
|
|
}
|
|
|
|
// obrimCipherDecryptSalted decrypts AES-256 encrypted data with salt metadata.
|
|
func obrimCipherDecryptSalted(data []byte, key string) ([]byte, error) {
|
|
_, _, offset, err := obrimCipherParseHeader(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
salt, err := obrimCipherExtractSalt(data, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
payload, err := obrimCipherExtractPayload(data, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
derivedKey := sha256.Sum256(append([]byte(key), salt...))
|
|
|
|
return obrimCipherDecryptAES(payload, derivedKey[:])
|
|
}
|
|
|
|
// obrimCipherEncryptAES performs AES-256 GCM encryption.
|
|
func obrimCipherEncryptAES(data []byte, key []byte) ([]byte, error) {
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nonce := make([]byte, obrimCipherNonceSize)
|
|
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
encrypted := gcm.Seal(nil, nonce, data, nil)
|
|
|
|
result := make([]byte, 0, len(nonce)+len(encrypted))
|
|
result = append(result, nonce...)
|
|
result = append(result, encrypted...)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// obrimCipherDecryptAES performs AES-256 GCM decryption.
|
|
func obrimCipherDecryptAES(data []byte, key []byte) ([]byte, error) {
|
|
if len(data) < obrimCipherNonceSize {
|
|
return nil, errors.New("invalid encrypted payload")
|
|
}
|
|
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nonce := data[:obrimCipherNonceSize]
|
|
payload := data[obrimCipherNonceSize:]
|
|
|
|
return gcm.Open(nil, nonce, payload, nil)
|
|
}
|