Encryption Adapter and API

I would like to implement some transparent encryption/decryption layer between the filesystem and the display that would be pretty transparent for obsidian. For that purpose, I should be able to intercept obsidian reads/write to and from the filesystem in order to encrypt/decrypt the arraybuffers.

Most of the read/write method I find are on the Vault API, and they seems to be deferred to a DataAdapter object who will leverage the filesystem’s API. I guess from the API that the default Adapter used is FileSystemAdapter, which has many read/write methods. I would prefer that the API supports the notion of decorators or custom chain of adapter but would it work (albeit a bit ugly) to make an EncryptionAdapter deferring to the FileSystemAdapter and registering that one in the Vault instead of the default one? If feels to me that duplicating any behaviour existing in the FileSystemAdapter would just be fragile, errorprone and undesirable.

Maybe I also miss some parts of the API where read/write occur that make it unworkable? I also don’t know when Vault will call read/readBinary/append/…

Also the replacement should happen very early before Obsidian starts to read the vault content with the default adapter, do plugin get some very early event hook to make changes to Vault before it starts reading files?

2 Likes

I’m also curious how to make a custom DataAdapter. I would like to just filter out some folders based on .gitignore and other patterns.

Is there any update to this problem? I tried to achieve the same thing and eventually settled on using a code block processor, but a markdown Pre-Processor would be nicer.

From what I see, Obsidian API doesn’t allow to hook into the file system events.

However, we can try to patch it in some way

import { DataAdapter, DataWriteOptions, ListedFiles, Plugin, Stat } from 'obsidian';

export class EncryptionPlugin extends Plugin {
    override onload(): void {
        this.app.vault.adapter = new EncryptionAdapter(this.app.vault.adapter);
    }

    override onunload(): void {
        this.app.vault.adapter = (this.app.vault.adapter as EncryptionAdapter).internalAdapter;
    }
}

class EncryptionAdapter extends DataAdapterDecorator {
    override async read(normalizedPath: string): Promise<string> {
        const encrypted = await super.read(normalizedPath);
        return await this.decrypt(encrypted);
    }

    override async readBinary(normalizedPath: string): Promise<ArrayBuffer> {
        const encryptedBinary = await super.readBinary(normalizedPath);
        return await this.decryptBinary(encryptedBinary);
    }

    override async write(normalizedPath: string, data: string, options?: DataWriteOptions | undefined): Promise<void> {
        const encryptedData = await this.encrypt(data);
        await super.write(normalizedPath, encryptedData, options);
    }

    override async writeBinary(normalizedPath: string, data: ArrayBuffer, options?: DataWriteOptions | undefined): Promise<void> {
        const encryptedData = await this.encryptBinary(data);
        await super.writeBinary(normalizedPath, encryptedData, options);
    }

    override async append(normalizedPath: string, data: string, options?: DataWriteOptions | undefined): Promise<void> {
        await this.process(normalizedPath, (content) => content + data, options);
    }

    override async process(normalizedPath: string, fn: (data: string) => string, options?: DataWriteOptions | undefined): Promise<string> {
        const content = await this.read(normalizedPath);
        const newContent = fn(content);
        await this.write(normalizedPath, newContent, options);
        return newContent;
    }

    async decrypt(encrypted: string): Promise<string> {
        // TODO
        return encrypted;
    }

    async decryptBinary(encryptedBinary: ArrayBuffer): Promise<ArrayBuffer> {
        // TODO
        return encryptedBinary;
    }

    async encrypt(decrypted: string): Promise<string> {
        // TODO
        return decrypted;
    }

    async encryptBinary(decryptedBinary: ArrayBuffer): Promise<ArrayBuffer> {
        // TODO
        return decryptedBinary;
    }
}

class DataAdapterDecorator implements DataAdapter {
    internalAdapter: DataAdapter;

    constructor (internalAdapter: DataAdapter) {
        this.internalAdapter = internalAdapter;
    }

    getName(): string {
        return this.internalAdapter.getName();
    }
    exists(normalizedPath: string, sensitive?: boolean | undefined): Promise<boolean> {
        return this.internalAdapter.exists(normalizedPath, sensitive);
    }
    stat(normalizedPath: string): Promise<Stat | null> {
        return this.internalAdapter.stat(normalizedPath);
    }
    list(normalizedPath: string): Promise<ListedFiles> {
        return this.internalAdapter.list(normalizedPath);
    }
    read(normalizedPath: string): Promise<string> {
        return this.internalAdapter.read(normalizedPath);
    }
    readBinary(normalizedPath: string): Promise<ArrayBuffer> {
        return this.internalAdapter.readBinary(normalizedPath);
    }
    write(normalizedPath: string, data: string, options?: DataWriteOptions | undefined): Promise<void> {
        return this.internalAdapter.write(normalizedPath, data, options);
    }
    writeBinary(normalizedPath: string, data: ArrayBuffer, options?: DataWriteOptions | undefined): Promise<void> {
        return this.internalAdapter.writeBinary(normalizedPath, data, options);
    }
    append(normalizedPath: string, data: string, options?: DataWriteOptions | undefined): Promise<void> {
        return this.internalAdapter.append(normalizedPath, data, options);
    }
    process(normalizedPath: string, fn: (data: string) => string, options?: DataWriteOptions | undefined): Promise<string> {
        return this.internalAdapter.process(normalizedPath, fn, options);
    }
    getResourcePath(normalizedPath: string): string {
        return this.internalAdapter.getResourcePath(normalizedPath);
    }
    mkdir(normalizedPath: string): Promise<void> {
        return this.internalAdapter.mkdir(normalizedPath);
    }
    trashSystem(normalizedPath: string): Promise<boolean> {
        return this.internalAdapter.trashSystem(normalizedPath);
    }
    trashLocal(normalizedPath: string): Promise<void> {
        return this.internalAdapter.trashLocal(normalizedPath);
    }
    rmdir(normalizedPath: string, recursive: boolean): Promise<void> {
        return this.internalAdapter.rmdir(normalizedPath, recursive);
    }
    remove(normalizedPath: string): Promise<void> {
        return this.internalAdapter.remove(normalizedPath);
    }
    rename(normalizedPath: string, normalizedNewPath: string): Promise<void> {
        return this.internalAdapter.rename(normalizedPath, normalizedNewPath);
    }
    copy(normalizedPath: string, normalizedNewPath: string): Promise<void> {
        return this.internalAdapter.copy(normalizedPath, normalizedNewPath);
    }
}
2 Likes

Thanks a lot for the inspiration @mnaoumov.
Unfortunately, it did not work for me with your suggestion but I found a different way to hook into the DataAdapter and Vault methods.

WIth this, I developed a transparent encryption plugin: GitHub - tejado/obsidian-gpgCrypt: Seamlessly encrypts your notes using GPG. Supports smartcards for enhanced security! 🔒📝📎 (let’s see if this will be accepted…).
It only encrypt notes as other data like PDF, audio and image files are loaded differently and are harder to intercept.

3 Likes