730 lines
14 KiB
Go
730 lines
14 KiB
Go
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)
|
|
}
|