Skip to content
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
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# Encrypted Dataroom Management (eDRM)

A trustless, encrypted data room where access is enforced on-chain via FHE (Fully Homomorphic Encryption). Nobody can see who has access to what. Documents live on Filecoin via Storacha.
Trustless, encrypted filesharing where access is enforced via FHE (Fully Homomorphic Encryption). Nobody can see who has access to what. Documents live on Filecoin via Storacha.

## The Problem

Data rooms are essential to every investment deal, yet the market (\$3-4B TAM, led by Intralinks and Datasite) charges \$15-50k+ per deal for what is fundamentally access control on a file share.

Often we need to rely on trust assumptions of the vendors and the members that operate on these data rooms.
Often we need to rely on trust assumptions of the vendors and the members that operate on these data rooms.

**Alternatively, it's a way to securely share documents with your friends that no-one else can access!**

## The Solution

Expand Down Expand Up @@ -38,6 +40,7 @@ For the full encryption flow, key hierarchy, and contract interface, see [Techni

- [Technical Architecture](docs/ARCHITECTURE.md): encryption flow, key hierarchy, storage model, privacy guarantees
- [Feature Set](docs/FEATURES.md): planned and shipped features
- [Architecture Decision Records](docs/ADRs/): technical decisions and trade-offs

## Getting Started

Expand Down Expand Up @@ -80,12 +83,12 @@ This runs:
3. **TypeChain**: pulls ABIs from the build artifacts and generates typed bindings
4. **Vite**: starts the dapp dev server

#### Storacha setup (file storage)
If it's too much effort to connect storacha ask for keys from: petros@obolos.io
#### Storacha setup
If it's too much effort to setup Storacha ask for keys from: petros@obolos.io

The dapp encrypts files client-side and uploads them to Filecoin via [Storacha](https://storacha.network).

You need an agent key and a delegation proof:
Make an agent key and a delegation proof:

```bash
# 1. Install the CLI
Expand Down Expand Up @@ -119,8 +122,7 @@ VITE_STORACHA_PROOF=mAY…
#### Chain / RPC (optional)

```env
# 31337 = Anvil (default), 11155111 = Sepolia, 1 = Mainnet
VITE_CHAIN_ID=31337
VITE_CHAIN_ID=31337 # anvil, 11155111 = Sepolia
VITE_RPC_URL=http://127.0.0.1:8545
```

Expand Down
70 changes: 59 additions & 11 deletions contracts/src/EncryptedDataRoom.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ contract EncryptedDataRoom is ZamaEthereumConfig {
error NotParentRoom();
error IsParentRoom();
error CannotNestDeeper();
error BatchTooLarge();
error InvalidAddress();

// Types
struct Room {
Expand All @@ -39,6 +41,7 @@ contract EncryptedDataRoom is ZamaEthereumConfig {
}

uint256 public constant NO_PARENT = type(uint256).max;
uint256 public constant MAX_BATCH_SIZE = 100;
uint256 public roomCount;
mapping(uint256 => Room) public rooms;
mapping(uint256 => mapping(uint256 => Document)) internal _documents;
Expand Down Expand Up @@ -74,6 +77,11 @@ contract EncryptedDataRoom is ZamaEthereumConfig {
_;
}

modifier roomExists(uint256 roomId) {
if (rooms[roomId].owner == address(0)) revert RoomNotFound();
_;
}

// Room Management

/// @notice Create a new parent room
Expand Down Expand Up @@ -153,6 +161,7 @@ contract EncryptedDataRoom is ZamaEthereumConfig {
notParentRoom(roomId)
{
if (cids.length != names.length || cids.length != wrappedKeys.length) revert LengthMismatch();
if (cids.length > MAX_BATCH_SIZE) revert BatchTooLarge();
for (uint256 i = 0; i < cids.length; i++) {
uint256 docIndex = rooms[roomId].documentCount++;
_documents[roomId][docIndex] =
Expand All @@ -174,6 +183,7 @@ contract EncryptedDataRoom is ZamaEthereumConfig {
notParentRoom(roomId)
{
if (docIndices.length != newWrappedKeys.length) revert LengthMismatch();
if (docIndices.length > MAX_BATCH_SIZE) revert BatchTooLarge();
for (uint256 i = 0; i < docIndices.length; i++) {
_documents[roomId][docIndices[i]].wrappedKey = newWrappedKeys[i];
documentKeyVersion[roomId][docIndices[i]] = roomKeyVersion[roomId];
Expand All @@ -190,12 +200,24 @@ contract EncryptedDataRoom is ZamaEthereumConfig {
onlyRoomOwner(roomId)
notParentRoom(roomId)
{
if (users.length > MAX_BATCH_SIZE) revert BatchTooLarge();
for (uint256 i = 0; i < users.length; i++) {
address user = users[i];
if (user == address(0)) revert InvalidAddress();
if (_isMember[roomId][user]) revert AlreadyMember();

uint256 idx = rooms[roomId].memberCount++;
_members[roomId][idx] = user;
// Only allocate a new slot if user was never in this folder before
bool existingSlot = false;
uint256 count = rooms[roomId].memberCount;
for (uint256 j = 0; j < count; j++) {
if (_members[roomId][j] == user) {
existingSlot = true;
break;
}
}
if (!existingSlot) {
_members[roomId][rooms[roomId].memberCount++] = user;
}
_isMember[roomId][user] = true;

FHE.allow(_roomKey[roomId], user);
Expand All @@ -216,6 +238,7 @@ contract EncryptedDataRoom is ZamaEthereumConfig {
onlyRoomOwner(roomId)
notParentRoom(roomId)
{
if (users.length > MAX_BATCH_SIZE) revert BatchTooLarge();
for (uint256 i = 0; i < users.length; i++) {
address user = users[i];
if (!_isMember[roomId][user]) revert NotMember();
Expand All @@ -234,15 +257,25 @@ contract EncryptedDataRoom is ZamaEthereumConfig {
/// @param parentId The parent room ID.
/// @param user Address of the user to grant access.
function grantAccessToAllFolders(uint256 parentId, address user) external onlyRoomOwner(parentId) {
if (user == address(0)) revert InvalidAddress();
Room storage parent = rooms[parentId];
if (!parent.isParent) revert NotParentRoom();

for (uint256 i = 0; i < parent.childCount; i++) {
uint256 roomId = _children[parentId][i];
if (_isMember[roomId][user]) continue;

uint256 idx = rooms[roomId].memberCount++;
_members[roomId][idx] = user;
bool existingSlot = false;
uint256 count = rooms[roomId].memberCount;
for (uint256 j = 0; j < count; j++) {
if (_members[roomId][j] == user) {
existingSlot = true;
break;
}
}
if (!existingSlot) {
_members[roomId][rooms[roomId].memberCount++] = user;
}
_isMember[roomId][user] = true;

FHE.allow(_roomKey[roomId], user);
Expand Down Expand Up @@ -302,15 +335,21 @@ contract EncryptedDataRoom is ZamaEthereumConfig {

// Views

/// @notice Check if the caller has access to a folder
/// @param roomId The folder to check access for.
function hasAccess(uint256 roomId) external view roomExists(roomId) returns (bool) {
return _isMember[roomId][msg.sender];
}

/// @notice Get your encrypted access flag for a folder.
/// @param roomId The folder to check access for.
function validateAccess(uint256 roomId) external view returns (ebool) {
function validateAccess(uint256 roomId) external view roomExists(roomId) returns (ebool) {
return _access[roomId][msg.sender];
}

/// @notice Get the encrypted folder key handle. Only decryptable if granted FHE access.
/// @param roomId The folder to get the key for.
function getRoomKey(uint256 roomId) external view notParentRoom(roomId) returns (euint256) {
function getRoomKey(uint256 roomId) external view roomExists(roomId) notParentRoom(roomId) returns (euint256) {
if (!_isMember[roomId][msg.sender]) revert Unauthorized();
return _roomKey[roomId];
}
Expand All @@ -323,11 +362,12 @@ contract EncryptedDataRoom is ZamaEthereumConfig {
view
returns (string memory cid, string memory name, uint256 createdAt, uint256 keyVersion, bytes memory wrappedKey)
{
if (rooms[roomId].owner == address(0)) revert RoomNotFound();
Document storage doc = _documents[roomId][docIndex];
return (doc.cid, doc.name, doc.createdAt, documentKeyVersion[roomId][docIndex], doc.wrappedKey);
}

/// @notice Get room/folder info.
/// @notice Get room/folder info. memberCount reflects only active (non-revoked) members.
/// @param roomId The room or folder to query.
function getRoom(uint256 roomId)
external
Expand All @@ -342,14 +382,21 @@ contract EncryptedDataRoom is ZamaEthereumConfig {
uint256 childCount
)
{
if (rooms[roomId].owner == address(0)) revert RoomNotFound();
Room storage room = rooms[roomId];
return
(room.owner, room.name, room.documentCount, room.memberCount, room.isParent, room.parentId, room.childCount);
uint256 active = 0;
uint256 total = room.memberCount;
for (uint256 i = 0; i < total; i++) {
if (_isMember[roomId][_members[roomId][i]]) {
active++;
}
}
return (room.owner, room.name, room.documentCount, active, room.isParent, room.parentId, room.childCount);
}

/// @notice Get all folder IDs under a parent room.
/// @param parentId The parent room ID.
function getFolders(uint256 parentId) external view returns (uint256[] memory) {
function getFolders(uint256 parentId) external view roomExists(parentId) returns (uint256[] memory) {
Room storage parent = rooms[parentId];
if (!parent.isParent) revert NotParentRoom();

Expand All @@ -362,13 +409,14 @@ contract EncryptedDataRoom is ZamaEthereumConfig {

/// @notice Get the parent room ID for a folder.
/// @param roomId The folder ID.
function getParentRoom(uint256 roomId) external view returns (uint256) {
function getParentRoom(uint256 roomId) external view roomExists(roomId) returns (uint256) {
return rooms[roomId].parentId;
}

/// @notice Get active members of a folder.
/// @param roomId The folder to query.
function getMembers(uint256 roomId) external view returns (address[] memory) {
if (rooms[roomId].owner == address(0)) revert RoomNotFound();
if (rooms[roomId].owner != msg.sender) revert Unauthorized();

uint256 count = rooms[roomId].memberCount;
Expand Down
Loading
Loading