Skip to content
This repository was archived by the owner on Oct 21, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
pip install --upgrade poetry
- name: Download Asherah binaries
run: |
./download-libasherah.sh
scripts/download-libasherah.sh
- name: Package and publish with Poetry
run: |
poetry config pypi-token.pypi $PYPI_TOKEN
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,8 @@ dmypy.json

# Pyre type checker
.pyre/

# Editors
.idea/
.vscode/
*.swp
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ Example code:
from asherah import Asherah, AsherahConfig

config = AsherahConfig(
kms_type='static',
kms='static',
metastore='memory',
service_name='TestService',
product_id='TestProduct',
verbose=True,
session_cache=True
enable_session_caching=True
)
crypt = Asherah()
crypt.setup(config)
Expand Down
75 changes: 20 additions & 55 deletions asherah/asherah.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Main Asherah class, for encrypting and decrypting of data"""
# pylint: disable=line-too-long, too-many-locals
# pylint: disable=line-too-long

from __future__ import annotations

import json
import os
from datetime import datetime, timezone
from typing import ByteString, Union

from cobhan import Cobhan
Expand All @@ -14,6 +15,7 @@
class Asherah:
"""The main class for providing encryption and decryption functionality"""

JSON_OVERHEAD = 256
KEY_SIZE = 64

def __init__(self):
Expand All @@ -22,9 +24,12 @@ def __init__(self):
os.path.join(os.path.dirname(__file__), "libasherah"),
"libasherah",
"""
void Shutdown();
int32_t SetupJson(void* configJson);
int32_t Decrypt(void* partitionIdPtr, void* encryptedDataPtr, void* encryptedKeyPtr, int64_t created, void* parentKeyIdPtr, int64_t parentKeyCreated, void* outputDecryptedDataPtr);
int32_t Encrypt(void* partitionIdPtr, void* dataPtr, void* outputEncryptedDataPtr, void* outputEncryptedKeyPtr, void* outputCreatedPtr, void* outputParentKeyIdPtr, void* outputParentKeyCreatedPtr);
int32_t EncryptToJson(void* partitionIdPtr, void* dataPtr, void* jsonPtr);
int32_t DecryptFromJson(void* partitionIdPtr, void* jsonPtr, void* dataPtr);
""",
)

Expand All @@ -38,6 +43,10 @@ def setup(self, config: types.AsherahConfig) -> None:
f"Setup failed with error number {result}"
)

def shutdown(self):
"""Shut down and clean up the Asherah instance"""
self.__libasherah.Shutdown()

def encrypt(self, partition_id: str, data: Union[ByteString, str]):
"""Encrypt a chunk of data"""
if isinstance(data, str):
Expand All @@ -46,71 +55,27 @@ def encrypt(self, partition_id: str, data: Union[ByteString, str]):
partition_id_buf = self.__cobhan.str_to_buf(partition_id)
data_buf = self.__cobhan.bytearray_to_buf(data)
# Outputs
encrypted_data_buf = self.__cobhan.allocate_buf(len(data) + self.KEY_SIZE)
encrypted_key_buf = self.__cobhan.allocate_buf(self.KEY_SIZE)
created_buf = self.__cobhan.int_to_buf(0)
parent_key_id_buf = self.__cobhan.allocate_buf(self.KEY_SIZE)
parent_key_created_buf = self.__cobhan.int_to_buf(0)
json_buf = self.__cobhan.allocate_buf(len(data_buf) + self.JSON_OVERHEAD)

result = self.__libasherah.Encrypt(
partition_id_buf,
data_buf,
encrypted_data_buf,
encrypted_key_buf,
created_buf,
parent_key_id_buf,
parent_key_created_buf,
)
result = self.__libasherah.EncryptToJson(partition_id_buf, data_buf, json_buf)
if result < 0:
raise exceptions.AsherahException(
f"Encrypt failed with error number {result}"
)
data_row_record = types.DataRowRecord(
data=self.__cobhan.buf_to_bytearray(encrypted_data_buf),
key=types.EnvelopeKeyRecord(
encrypted_key=self.__cobhan.buf_to_bytearray(encrypted_key_buf),
created=datetime.fromtimestamp(
self.__cobhan.buf_to_int(created_buf), tz=timezone.utc
),
parent_key_meta=types.KeyMeta(
id=self.__cobhan.buf_to_str(parent_key_id_buf),
created=datetime.fromtimestamp(
self.__cobhan.buf_to_int(parent_key_created_buf),
tz=timezone.utc,
),
),
),
)
return self.__cobhan.buf_to_str(json_buf)

return data_row_record

def decrypt(
self, partition_id: str, data_row_record: types.DataRowRecord
) -> bytearray:
def decrypt(self, partition_id: str, data_row_record: str) -> bytearray:
"""Decrypt data that was previously encrypted by Asherah"""
# Inputs
partition_id_buf = self.__cobhan.str_to_buf(partition_id)
encrypted_data_buf = self.__cobhan.bytearray_to_buf(data_row_record.data)
encrypted_key_buf = self.__cobhan.bytearray_to_buf(
data_row_record.key.encrypted_key
)
created = int(data_row_record.key.created.timestamp())
parent_key_id_buf = self.__cobhan.str_to_buf(
data_row_record.key.parent_key_meta.id
)
parent_key_created = int(
data_row_record.key.parent_key_meta.created.timestamp()
)
json_buf = self.__cobhan.str_to_buf(data_row_record)

# Output
data_buf = self.__cobhan.allocate_buf(len(encrypted_data_buf) + self.KEY_SIZE)
data_buf = self.__cobhan.allocate_buf(len(json_buf))

result = self.__libasherah.Decrypt(
result = self.__libasherah.DecryptFromJson(
partition_id_buf,
encrypted_data_buf,
encrypted_key_buf,
created,
parent_key_id_buf,
parent_key_created,
json_buf,
data_buf,
)

Expand Down
10 changes: 10 additions & 0 deletions asherah/scripts/download-libasherah.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash

rm -rf asherah/libasherah/

wget --content-disposition --directory-prefix asherah/libasherah/ \
https://github.com/godaddy/asherah-cobhan/releases/download/v0.3.1/libasherah-arm64.dylib \
https://github.com/godaddy/asherah-cobhan/releases/download/v0.3.1/libasherah-arm64.so \
https://github.com/godaddy/asherah-cobhan/releases/download/v0.3.1/libasherah-x64.dylib \
https://github.com/godaddy/asherah-cobhan/releases/download/v0.3.1/libasherah-x64.so \
|| exit 1
119 changes: 81 additions & 38 deletions asherah/types.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,103 @@
"""Type definitions for the Asherah library"""
# pylint: disable=too-many-instance-attributes,invalid-name
# pylint: disable=too-many-instance-attributes

from __future__ import annotations

from dataclasses import asdict, dataclass
from datetime import datetime
from typing import ByteString, Optional
from typing import Dict, Optional

from enum import Enum


class KMSType(Enum):
"""Supported types of KMS services"""

AWS = "aws"
STATIC = "static"


class MetastoreType(Enum):
"""Supported types of metastores"""

RDBMS = "rdbms"
DYNAMODB = "dynamodb"
MEMORY = "memory"


class ReadConsistencyType(Enum):
"""Supported read consistency types"""

EVENTUAL = "eventual"
GLOBAL = "global"
SESSION = "session"


@dataclass
class AsherahConfig:
"""Configuration options for Asherah setup"""
"""Configuration options for Asherah setup

:param kms: Configures the master key management service (aws or static)
:param metastore: Determines the type of metastore to use for persisting
types
:param service_name: The name of the service
:param product_id: The name of the product that owns this service
:param connection_string: The database connection string (Required if
metastore is rdbms)
:param dynamo_db_endpoint: An optional endpoint URL (hostname only or fully
qualified URI) (only supported by metastore = dynamodb)
:param dynamo_db_region: The AWS region for DynamoDB requests (defaults to
globally configured region) (only supported by metastore = dynamodb)
:param dynamo_db_table_name: The table name for DynamoDB (only supported by
metastore = dynamodb)
:param enable_region_suffix: Configure the metastore to use regional
suffixes (only supported by metastore = dynamodb)
:param preferred_region: The preferred AWS region (required if kms is aws)
:param region_map: Dictionary of REGION: ARN (required if kms is aws)
:param verbose: Enable verbose logging output
:param enable_session_caching: Enable shared session caching
:param expire_after: The amount of time a key is considered valid
:param check_interval: The amount of time before cached keys are considered
stale
:param replica_read_consistency: Required for Aurora sessions using write
forwarding (eventual, global, session)
:param session_cache_max_size: Define the maximum number of sessions to
cache (default 1000)
:param session_cache_max_duration: The amount of time a session will remain
cached (default 2h)
"""

kms_type: str
metastore: str
kms: KMSType
metastore: MetastoreType
service_name: str
product_id: str
rdbms_connection_string: Optional[str] = None
connection_string: Optional[str] = None
dynamo_db_endpoint: Optional[str] = None
dynamo_db_region: Optional[str] = None
dynamo_db_table_name: Optional[str] = None
enable_region_suffix: bool = False
preferred_region: Optional[str] = None
region_map: Optional[str] = None
region_map: Optional[Dict[str, str]] = None
verbose: bool = False
session_cache: bool = False
debug_output: bool = False
enable_session_caching: bool = False
expire_after: Optional[int] = None
check_interval: Optional[int] = None
replica_read_consistency: Optional[ReadConsistencyType] = None
session_cache_max_size: Optional[int] = None
session_cache_duration: Optional[int] = None

def to_json(self):
def camel_case(key):
"""Produce a JSON dictionary in a form expected by Asherah"""

def translate_key(key):
"""Translate snake_case into camelCase."""
parts = key.split("_")
parts = [parts[0]] + [part.capitalize() for part in parts[1:]]
parts = [
part.capitalize()
.replace("Db", "DB")
.replace("Id", "ID")
.replace("Kms", "KMS")
for part in parts
]
return "".join(parts)

return {camel_case(key): val for key, val in asdict(self).items()}


@dataclass
class KeyMeta:
"""Metadata about an encryption key"""

id: str
created: datetime


@dataclass
class EnvelopeKeyRecord:
"""Information about an encryption envelope"""

encrypted_key: ByteString
created: datetime
parent_key_meta: KeyMeta


@dataclass
class DataRowRecord:
"""Encrypted data and its related information"""

data: ByteString
key: EnvelopeKeyRecord
return {translate_key(key): val for key, val in asdict(self).items()}
4 changes: 2 additions & 2 deletions benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
from asherah import Asherah, AsherahConfig

config = AsherahConfig(
kms_type="static",
kms="static",
metastore="memory",
service_name="TestService",
product_id="TestProduct",
session_cache=True,
enable_session_caching=True,
)
crypt = Asherah()
crypt.setup(config)
Expand Down
10 changes: 0 additions & 10 deletions download-libasherah.sh

This file was deleted.

Loading