Skip to main content

Command Palette

Search for a command to run...

Self-Secured Folders with Electron

Updated
12 min read
Self-Secured Folders with Electron
K

A code-dependent life form.

You've got sensitive files on your laptop. Medical records, financial documents, private photos, confidential work files. Sure, you could password-protect them, encrypt them with traditional methods, or hide them in obscure folders. But here's the thing, these approaches all have the same fundamental flaw: they can't verify who you actually are.

And it's not just personal computers. Think about enterprise environments, NAS (Network-Attached Storage) devices, Active Directory file shares, departmental folders on shared drives. Multiple people have access, but how do you ensure only the right people with the right verified attributes can decrypt specific folders? You can't just rely on usernames and passwords when compliance requires proof of age, sanctions screening, or other identity attributes.

That's when it hit me. What if I could gate access to files not just with a password, but with actual identity verification? What if I could set rules like "only decrypt this if the user is over 18" or "only if they're not on any sanctions lists"? And most importantly, what if I could do all this without compromising privacy?

Want to follow along? All the code from this article is available on GitHub. Feel free to clone, experiment, and build upon it!

What Are We Building Here?

I created a desktop application that lets you:

  1. Encrypt entire folders with military-grade AES-256-GCM encryption

  2. Set custom access rules per folder (minimum age, gender, OFAC sanctions clearance)

  3. Verify your identity using the Self protocol before decrypting anything

  4. Keep everything local and private on your machine

The folders literally disappear from your file system (they get hidden and encrypted), and the only way to access them is through the app after proving you meet the specific requirements you set.

Why Self Protocol?

Traditional identity verification is broken. When you verify your age on a website, you typically upload your entire ID or passport. They see your name, address, date of birth, ID number, everything. You're giving away the farm just to prove you're 21.

Self flips this on its head with zero-knowledge proofs. Here's how it works:

  1. You scan your passport with your phone's NFC reader

  2. Self generates cryptographic proofs about you

  3. When an app needs verification, you share only what's required

  4. The app gets a mathematical proof that you're over 18 (for example) without ever seeing your actual birthdate

It's like proving you can unlock a door without showing the key itself. Mind-blowing stuff.

The Architecture

This application is split into three distinct parts that work together seamlessly:

The Electron Main Process

This is where the magic happens. The Electron main process handles all the heavy lifting:

  • File system operations

  • Encryption/decryption with AES-256-GCM

  • Storing folder configurations

  • Managing encryption keys

The Next.js Frontend

Built with Next.js and React, this provides a clean, modern interface for:

  • Browsing and managing encrypted folders

  • Configuring folder access rules

  • Displaying Self QR codes for verification

  • Showing verification status in real-time

The Express Backend

A lightweight Express server that:

  • Integrates with Self protocol's backend verifier

  • Manages per-folder verification configurations

  • Validates zero-knowledge proofs

  • Checks age, gender, and OFAC requirements

Checkout: kartikmehta8/self-secure-folders

How Encryption Actually Works

Let's peek under the hood. When you add a folder to encrypt, here's what happens:

Step 1: Generate a Random Encryption Key

const id = crypto.randomUUID();
const key = crypto.randomBytes(32); // 256-bit key for AES.
const keyBase64 = key.toString("base64");

Each folder gets its own unique 256-bit encryption key. This key never leaves your machine and is stored securely in Electron's user data directory.

Step 2: Recursively Encrypt Every File

The app walks through your entire folder structure and encrypts each file individually:

async function encryptFile(sourcePath, targetPath, key) {
  const iv = crypto.randomBytes(12); // 12-byte initialization vector.
  const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);

  const data = await fsPromises.readFile(sourcePath);
  const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
  const authTag = cipher.getAuthTag(); // For integrity verification.

  // Store: IV + Auth Tag + Encrypted Data.
  const payload = Buffer.concat([iv, authTag, encrypted]);
  await fsPromises.writeFile(targetPath, payload);
}

Why AES-256-GCM? Two reasons:

  1. AES-256 is the gold standard for symmetric encryption, the same encryption used by governments and militaries worldwide

  2. GCM (Galois/Counter Mode) provides authenticated encryption, meaning it prevents tampering. If someone modifies even a single bit of your encrypted file, decryption will fail

Step 3: Hide the Original Folder

On macOS and Linux, the app automatically renames your source folder with a leading dot (like .my-secret-folder), making it hidden from normal view:

if (process.platform !== "win32") {
  const parentDir = path.dirname(input.sourcePath);
  const baseName = path.basename(input.sourcePath);

  if (!baseName.startsWith(".")) {
    const hiddenPath = path.join(parentDir, `.${baseName}`);
    await fsPromises.rename(input.sourcePath, hiddenPath);
  }
}

Your encrypted version lives in Electron's user data directory, completely separate from your normal file system.

The Self Integration

This is where things get really interesting. When you click "Open" on an encrypted folder, you're not just entering a password, you're proving your identity.

Setting Up Verification Rules

First, you configure what requirements must be met to access a specific folder:

const conditions: FolderConditions = {
  minAge: 18,              // Must be at least 18 years old.
  gender: "female",        // Optional: specific gender requirement.
  requireOfacClear: true   // Must not be on OFAC sanctions list.
};

These rules get sent to the Express backend and stored in memory:

app.post("/api/config", async (req, res) => {
  const { configId, minimumAge, excludedCountries, ofac } = req.body;

  const config = {
    minimumAge: minimumAge || 18,
    excludedCountries: excludedCountries || [],
    ofac: Boolean(ofac),
  };

  await configStore.setConfig(configId, config);
});

Generating the QR Code

When you attempt to open a folder, the app generates a Self verification session:

const app = new SelfAppBuilder({
  version: 2,
  appName: "Self Secure Folders",
  scope: process.env.NEXT_PUBLIC_SELF_SCOPE,
  endpoint: process.env.NEXT_PUBLIC_SELF_ENDPOINT,
  userId: ethers.ZeroAddress,
  endpointType: "staging_https",
  userDefinedData: configId, // Links verification to specific folder.
  disclosures: {
    minimumAge: folder.conditions.minAge,
    ofac: folder.conditions.requireOfacClear,
    nationality: true,
    gender: true,
  },
}).build();

setUniversalLink(getUniversalLink(app));

This creates a QR code that encodes all the verification requirements. When you scan it with the Self mobile app, it knows exactly what to prove.

The Verification Flow

Here's what happens when you scan that QR code:

Backend Verification

The Express server receives the proof and validates it:

app.post("/api/verify", async (req, res) => {
  const { attestationId, proof, publicSignals, userContextData } = req.body;

  // Verify the zero-knowledge proof.
  const result = await selfBackendVerifier.verify(
    attestationId,
    proof,
    publicSignals,
    userContextData
  );

  const { isValid, isMinimumAgeValid, isOfacValid } = result.isValidDetails;

  // Check all requirements.
  if (!isValid || !isMinimumAgeValid || isOfacValid) {
    return res.json({
      status: "error",
      result: false,
      reason: "Verification failed"
    });
  }

  return res.json({
    status: "success",
    result: true,
    credentialSubject: result.discloseOutput
  });
});

The backend sees that you're over 18 (or whatever the requirement is), but it never sees your actual birthdate. It sees that you're not on OFAC sanctions lists, but it doesn't need your full identity document.

Frontend Requirement Checking

Once verification succeeds, the frontend does a final check against the folder's specific requirements:

const handleSuccessfulVerification = async () => {
  const response = await fetch(debugEndpoint);
  const data = await response.json();
  const result = data.verificationResult;

  // Check age requirement.
  if (folder.conditions.minAge && !result.isValidDetails.isMinimumAgeValid) {
    setStatus("Minimum age requirement not satisfied");
    return;
  }

  // Check gender requirement (if specified).
  if (folder.conditions.gender) {
    const genderFromProof = result.discloseOutput.gender.toLowerCase();
    const required = folder.conditions.gender.toLowerCase();

    if (!matchesGender(genderFromProof, required)) {
      setStatus("Gender requirement not satisfied");
      return;
    }
  }

  // Check OFAC requirement.
  if (folder.conditions.requireOfacClear && result.isValidDetails.isOfacValid) {
    setStatus("OFAC check failed. Access denied.");
    return;
  }

  // All checks passed - decrypt the folder!
  await window.electronAPI.openFolder(folder.id);
};

The Decryption Process

Once all verification checks pass, it's time to decrypt. The app creates a temporary decrypted copy of your folder:

async function decryptFile(sourcePath, targetPath, key) {
  const payload = await fsPromises.readFile(sourcePath);

  // Extract components from stored payload.
  const iv = payload.subarray(0, 12);
  const authTag = payload.subarray(12, 28);
  const ciphertext = payload.subarray(28);

  // Decrypt.
  const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
  decipher.setAuthTag(authTag);

  const decrypted = Buffer.concat([
    decipher.update(ciphertext),
    decipher.final()  // This will throw if the auth tag doesn't match.
  ]);

  await fsPromises.writeFile(targetPath, decrypted);
}

The decrypted folder is placed in a temporary location, and your system's file explorer automatically opens it. You can work with your files normally.

The IPC Bridge

Electron apps have a unique challenge: the frontend (renderer process) and backend (main process) run in separate contexts for security. They communicate through IPC (Inter-Process Communication).

The Preload Script

This secure bridge exposes only specific functionality to the frontend:

// electron/preload.js.
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("electronAPI", {
  listFolders: () => ipcRenderer.invoke("folders:list"),
  selectSourceFolder: () => ipcRenderer.invoke("folders:selectSource"),
  createFolder: (input) => ipcRenderer.invoke("folders:create", input),
  updateFolder: (id, updates) =>
    ipcRenderer.invoke("folders:update", { id, updates }),
  deleteFolder: (id) => ipcRenderer.invoke("folders:delete", { id }),
  openFolder: (id) => ipcRenderer.invoke("folders:open", { id }),
});

Using It in React

In your React components, you can now call these functions naturally:

const handleCreateFolder = async () => {
  const created = await window.electronAPI.createFolder({
    label: "My Secret Files",
    sourcePath: "/Users/me/Documents/secret",
    conditions: {
      minAge: 21,
      gender: null,
      requireOfacClear: true
    }
  });

  setFolders(prev => [...prev, created]);
};

This pattern keeps your Electron app secure while maintaining a clean, React-friendly API.

Performance Considerations

Handling Large Folders

Encrypting thousands of files can take time. I implemented several optimizations:

1. File Count Limiting

const MAX_ENCRYPTED_FILES = 5000;
let encryptedFileCount = 0;

async function encryptFile(sourcePath, targetPath, key) {
  if (encryptedFileCount >= MAX_ENCRYPTED_FILES) {
    throw new Error("Folder is too large to encrypt");
  }
  encryptedFileCount += 1;
  // ... encryption logic.
}

2. Smart Directory Skipping

Common large directories that don't need encryption are automatically skipped:

if (entry.isDirectory() && (
  entry.name === "node_modules" ||
  entry.name === ".git" ||
  entry.name === ".next" ||
  entry.name === "dist" ||
  entry.name === "build"
)) {
  continue;
}

3. Async File Operations

All file operations use Node's promise-based APIs for better performance:

const fsPromises = fs.promises;

// Instead of synchronous:
// fs.readFileSync(path).

// We use:
await fsPromises.readFile(path)

Real-World Use Cases

Now that we understand how it works, let's talk about where this could actually be useful.

1. Healthcare Records Management

Imagine a medical practice that needs to ensure only adults can access certain medical files:

{
  label: "Adult Patient Records",
  conditions: {
    minAge: 18,
    gender: null,
    requireOfacClear: false
  }
}

2. Financial Compliance

A financial advisor could use this to gate access to client files, ensuring anyone accessing them isn't on sanctions lists:

{
  label: "Client Portfolio Files",
  conditions: {
    minAge: 21,
    gender: null,
    requireOfacClear: true  // Must not be on OFAC list.
  }
}

3. Age-Gated Content

Content creators could distribute files that self-enforce age restrictions:

{
  label: "Age-Restricted Content",
  conditions: {
    minAge: 21,
    gender: null,
    requireOfacClear: false
  }
}

4. Multi-User Shared Workstations

In environments where multiple people share computers, this ensures only authorized individuals can decrypt sensitive folders based on their verified identity attributes.

5. Enterprise NAS and Network Shares

This pattern extends beautifully to enterprise environments with Network-Attached Storage (NAS) devices:

{
  label: "HR Confidential Documents",
  conditions: {
    minAge: 21,
    gender: null,
    requireOfacClear: true
  }
}

Imagine a NAS device where encrypted folders are stored, but decryption only happens after Self verification. Multiple employees can have physical access to the network share, but only those who verify their identity attributes can decrypt specific folders. This works great for:

  • HR departments restricting access to employee records based on age verification

  • Finance teams ensuring OFAC compliance before accessing client data

  • Legal departments gating access to sensitive case files

  • Research labs controlling access to confidential study data

The same approach could integrate with Active Directory or LDAP systems, users authenticate with their domain credentials, but decryption requires additional Self verification. It's defense-in-depth with privacy preservation.

Why This Is Different?

Let's look at how this stacks up against traditional solutions:

ApproachHow It WorksPrivacyIdentity VerificationGranular Rules
OS Encryption (FileVault, BitLocker)Full disk encryptionGoodNone (password only)No
Password-Protected ArchivesZIP/RAR with passwordGoodNoneNo
Cloud Storage with 2FAFiles on cloud with MFAPoor (cloud provider sees all)Basic (phone/email)Limited
Traditional Document VerificationUpload ID copiesTerribleYes, but over-sharesLimited
Self + Encryption (This App)Local encryption + ZK proofsExcellentYes, privacy-preservingYes, per-folder

Building with Self

From a developer perspective, integrating Self was surprisingly straightforward.

Frontend Integration

import { SelfAppBuilder } from "@selfxyz/qrcode";

const app = new SelfAppBuilder({
  version: 2,
  appName: "My App",
  scope: "my-scope",
  endpoint: "https://my-server.com/verify",
  userId: userAddress,
  endpointType: "staging_https",
  disclosures: {
    minimumAge: 18,
    ofac: true,
  },
}).build();

Backend Integration

import { SelfBackendVerifier, InMemoryConfigStore } from "@selfxyz/core";

const configStore = new InMemoryConfigStore();
const verifier = new SelfBackendVerifier(
  scope,
  endpoint,
  true,
  AllIds,
  configStore,
  "hex"
);

app.post("/verify", async (req, res) => {
  const result = await verifier.verify(
    req.body.attestationId,
    req.body.proof,
    req.body.publicSignals,
    req.body.userContextData
  );

  res.json({ success: result.isValidDetails.isValid });
});

That's it. Self handles all the complex cryptography, proof verification, and passport validation.

Development Setup

Here's how to get this running on your machine:

1. Install Dependencies

# Root workspace.
npm install

# Backend.
cd server
npm install

# Frontend.
cd client
npm install

2. Configure Environment Variables

Backend (.env):

PORT=3001
SELF_SCOPE=your-scope-name
SELF_ENDPOINT=https://your-ngrok-url.ngrok-free.app/api/verify

Frontend (.env.local):

NEXT_PUBLIC_SELF_APP_NAME=Self Secure Folders
NEXT_PUBLIC_SELF_SCOPE=your-scope-name
NEXT_PUBLIC_SELF_ENDPOINT=https://your-ngrok-url.ngrok-free.app/api/verify
NEXT_PUBLIC_SELF_DEBUG_ENDPOINT=http://localhost:3001/debug/last-result
NEXT_PUBLIC_SELF_CONFIG_ENDPOINT=http://localhost:3001/api/config

3. Start Everything

# Terminal 1: Backend.
cd server
npm run dev

# Terminal 2: Expose backend via ngrok.
ngrok http 3001

# Terminal 3: Frontend.
cd client
npm run dev

# Terminal 4: Electron.
npm run dev:electron

4. Use the Self Mobile App

Download the Self mobile app, create mock passports, and you're ready to test the verification flow.

The Bigger Picture

This proof of concept demonstrates something important: identity verification doesn't have to compromise privacy.

We live in a world where showing your ID means revealing everything about yourself. Want to prove you're 21? Here's your full name, address, date of birth, ID number, and photo. It's absurd.

Zero-knowledge proofs change this paradigm entirely. You can prove:

  • You're over 18 without revealing your birthdate

  • You're not on a sanctions list without showing your full identity

  • You're a resident of a country without showing your address

  • You're unique without revealing who you are

When combined with strong encryption and native apps, this opens up entirely new possibilities:

Compliance Without Surveillance: Companies can meet regulatory requirements (age verification, sanctions screening) without collecting personal data.

Privacy-First Access Control: You can gate access to resources based on verified attributes while respecting user privacy.

Decentralized Identity: Users maintain control of their identity data instead of fragmenting it across dozens of services.

Wrapping Up

Building this project opened my eyes to what's possible when we combine modern cryptography with user-friendly applications. The code is open source, the protocol is transparent, and the possibilities are endless. Whether you're building compliance tools, privacy applications, or just want to experiment with zero-knowledge proofs, I hope this walkthrough inspires you to explore what's possible.

Feel free to fork the repo, build on it, break it, improve it. That's what POCs are for.

This project is a proof of concept demonstrating Self protocol integration with Electron. It is not intended for production use without proper security hardening and auditing.

If you ever need help or just want to chat, DM me on Twitter / X or LinkedIn.

Kartik Mehta

X / LinkedIn