-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
295 additions
and
104 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import type { EventTemplate, SignedEvent } from "blossom-client"; | ||
import Drive, { emptyMetadata, type DriveMetadata } from "./Drive"; | ||
import { base64 } from "@scure/base"; | ||
import { decrypt, encrypt } from "./crypto"; | ||
import TreeFolder from "./FileTree/TreeFolder"; | ||
|
||
export const ENCRYPTED_DRIVE_KIND = 30564; | ||
|
||
const drivePassword = new WeakMap<EncryptedDrive, string>(); | ||
export class EncryptedDrive extends Drive { | ||
private encoder = new TextEncoder(); | ||
private decoder = new TextDecoder(); | ||
|
||
unlocked = false; | ||
|
||
async unlock(password: string) { | ||
if (!this.event) throw new Error("No Event"); | ||
if (this.unlocked) return; | ||
try { | ||
drivePassword.set(this, password); | ||
this.unlocked = false; | ||
await this.resetFromEvent(); | ||
} catch (e) { | ||
drivePassword.delete(this); | ||
this.unlocked = true; | ||
throw e; | ||
} | ||
} | ||
lock() { | ||
if (this.unlocked) { | ||
drivePassword.delete(this); | ||
this.unlocked = true; | ||
this._metadata = emptyMetadata; | ||
this.tree = new TreeFolder(""); | ||
} | ||
} | ||
|
||
/** used to set the password on new drives */ | ||
setPassword(password: string) { | ||
if (!this.unlocked && !drivePassword.has(this)) { | ||
drivePassword.set(this, password); | ||
} | ||
} | ||
|
||
protected readEvent(event: EventTemplate | SignedEvent): DriveMetadata { | ||
const password = drivePassword.get(this); | ||
if (!password) throw new Error("No password provided"); | ||
|
||
const data = decrypt(base64.decode(event.content), password); | ||
const plaintext = this.decoder.decode(data); | ||
const tags = JSON.parse(plaintext); | ||
|
||
this.unlocked = true; | ||
|
||
return super.readEvent({ ...event, content: "", tags, created_at: 0 }); | ||
} | ||
|
||
protected createEventTemplate(): EventTemplate { | ||
const password = drivePassword.get(this); | ||
if (!password) throw new Error("No password set"); | ||
|
||
const template = super.createEventTemplate(); | ||
const plaintext = this.encoder.encode(JSON.stringify(template.tags)); | ||
const data = encrypt(plaintext, password); | ||
const ciphertext = base64.encode(data); | ||
template.kind = ENCRYPTED_DRIVE_KIND; | ||
template.content = ciphertext; | ||
template.tags = []; | ||
return template; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { scrypt } from "@noble/hashes/scrypt"; | ||
import { xchacha20poly1305 } from "@noble/ciphers/chacha"; | ||
import { concatBytes, randomBytes } from "@noble/hashes/utils"; | ||
|
||
export function encrypt(data: Uint8Array, password: string, logn: number = 16, ksb: 0x00 | 0x01 | 0x02 = 0x02) { | ||
let salt = randomBytes(16); | ||
let n = 2 ** logn; | ||
let key = scrypt(password.normalize("NFKC"), salt, { N: n, r: 8, p: 1, dkLen: 32 }); | ||
let nonce = randomBytes(24); | ||
let aad = Uint8Array.from([ksb]); | ||
let xc2p1 = xchacha20poly1305(key, nonce, aad); | ||
let ciphertext = xc2p1.encrypt(data); | ||
let b = concatBytes(Uint8Array.from([0x02]), Uint8Array.from([logn]), salt, nonce, aad, ciphertext); | ||
return b; | ||
} | ||
|
||
export function decrypt(b: Uint8Array, password: string) { | ||
let version = b[0]; | ||
if (version !== 0x02) { | ||
throw new Error(`invalid version ${version}, expected 0x02`); | ||
} | ||
|
||
let logn = b[1]; | ||
let n = 2 ** logn; | ||
|
||
let salt = b.slice(2, 2 + 16); | ||
let nonce = b.slice(2 + 16, 2 + 16 + 24); | ||
let ksb = b[2 + 16 + 24]; | ||
let aad = Uint8Array.from([ksb]); | ||
let ciphertext = b.slice(2 + 16 + 24 + 1); | ||
|
||
let key = scrypt(password.normalize("NFKC"), salt, { N: n, r: 8, p: 1, dkLen: 32 }); | ||
let xc2p1 = xchacha20poly1305(key, nonce, aad); | ||
let plaintext = xc2p1.decrypt(ciphertext); | ||
|
||
return plaintext; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
<script lang="ts"> | ||
import { FolderOutline } from "flowbite-svelte-icons"; | ||
import { Badge } from "flowbite-svelte"; | ||
import type Drive from "../blossom-drive-client/Drive"; | ||
import { EncryptedDrive } from "../blossom-drive-client/EncryptedDrive"; | ||
export let drive: Drive; | ||
$: isEncrypted = drive instanceof EncryptedDrive; | ||
</script> | ||
|
||
<a | ||
href={`#/drive/${drive.address}`} | ||
class="flex w-full max-w-sm flex-row divide-gray-200 rounded-lg border border-gray-200 bg-white text-gray-500 shadow-md hover:bg-gray-100 dark:divide-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700" | ||
> | ||
<div class="flex aspect-square h-full w-32 items-center justify-center p-4"> | ||
<FolderOutline class="h-full w-full text-purple-500" /> | ||
</div> | ||
<div class="relative flex-1 py-4 pb-4 pr-4"> | ||
<h5 class="mb-2 text-lg font-bold tracking-tight text-gray-900 dark:text-white"> | ||
{drive.name} | ||
</h5> | ||
|
||
<p class="font-normal leading-tight text-gray-700 dark:text-gray-400"> | ||
{drive.description} | ||
</p> | ||
{#if isEncrypted} | ||
<Badge color="green" class="absolute bottom-2 right-2">Encrypted</Badge> | ||
{:else} | ||
<Badge color="purple" class="absolute bottom-2 right-2">Public</Badge> | ||
{/if} | ||
</div> | ||
</a> |
Oops, something went wrong.