TreeMap
무료 오픈소스 디스크 사용량 시각화 도구로, 사각형 트리맵·중복 파일 탐색·디스크 가득 예측·정리 시 파일을 휴지통으로만 이동한다
브라우저에서 직접 암호화된 메모, 서버는 암호문만 저장하고 익명 로그인 가능
End-to-end encrypted notes in three security tiers. Encryption happens in your browser — the server only ever stores ciphertext, and you can sign in anonymously.
생산성TypeScript · SCSSGitHub @irondsd온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
SigNote is an end-to-end encrypted notes app with three security tiers.
Sign-in is passwordless — an emailed one-time code, Google OAuth, or Sign-In with Ethereum (SIWE), with passkeys planned — and higher-security notes are protected by a passphrase-derived key the server never sees. The goal is simple: let users pick how they identify themselves (privacy or convenience) while picking the right privacy level for each note.README 전문 보기 ↗SigNote is an end-to-end encrypted notes app with three security tiers.
Sign-in is passwordless — an emailed one-time code, Google OAuth, or Sign-In with Ethereum (SIWE), with passkeys planned — and higher-security notes are protected by a passphrase-derived key the server never sees. The goal is simple: let users pick how they identify themselves (privacy or convenience) while picking the right privacy level for each note.
SigNote organizes notes into three tiers:
title remains unencrypted for search and navigation.body is encrypted client-side before leaving the browser.title remains unencrypted.body is encrypted with a unique per-note key (NEK).For both Tier 2 and Tier 3, the server stores ciphertext only, and all encryption and decryption happens client-side using the Web Crypto API. The server never sees plaintext note bodies or any key material capable of decrypting them.
Encryption is unlocked with a user-chosen passphrase. The passphrase itself is never stored. Instead, during setup:
salt is generated client-side.deviceShare.serverShare is generated client-side and stored in Postgres (associated with the wallet address).deviceShare XOR serverShare. The MEK is never stored anywhere.After setup, deviceShare is kept in sessionStorage for the duration of the browser session. This means the MEK can be reconstructed silently on page reload without re-prompting for a passphrase — and is automatically discarded when the tab closes.
To unlock from a new tab or after sessionStorage is cleared, the user re-enters the passphrase. This re-derives deviceShare from the stored salt, fetches serverShare from the server, and reconstructs the MEK.
The MEK is split into two shares specifically to limit what either side alone can compromise:
serverShare but not deviceShare (which is derived from the passphrase and never stored server-side). Without the passphrase, they cannot reconstruct the MEK.sessionStorage gets deviceShare but not serverShare. Without the server, they cannot reconstruct the MEK. (Once sessionStorage is cleared — on tab close or explicit lock — there is nothing to steal.)The MEK itself only ever exists in memory, as a non-extractable CryptoKey object, for the duration of an unlocked session.
A keyCheck payload is stored with the encryption profile. It is an AES-GCM ciphertext of a known constant, encrypted with a key derived from the MEK using HKDF. On unlock, the app attempts to decrypt keyCheck — success means the passphrase was correct, failure means it was wrong. No oracle is needed on the server.
Once the MEK is in memory it is imported as an HKDF base key (non-extractable). All working keys are derived from it using HKDF (SHA-256) with distinct info strings to ensure domain separation:
| Derived key | HKDF info string | Used for |
|---|---|---|
secretBodyKey | signote-v1/secret-body | Encrypting and decrypting all Secrets bodies |
verifyKey | signote-v1/verify | Encrypting the keyCheck payload |
sealWrapKey(id) | signote-v1/seal-wrap/<noteId> | Wrapping the per-note NEK for each Seal |
Because the sealWrapKey derivation includes the note ID in the info string, each note gets a unique wrapping key even though they share the same MEK.
Each Secret body is encrypted with AES-GCM 256-bit using secretBodyKey. A fresh random 12-byte IV is generated per encryption operation. The ciphertext and IV are stored together in Postgres.
Each Seal uses a unique 32-byte Note Encryption Key (NEK):
sealWrapKey(noteId) — also AES-GCM, also with the note ID as AAD.To decrypt, the process is reversed: derive the seal wrapping key → unwrap the NEK → decrypt the body. If the note ID does not match the AAD used during encryption, decryption fails.
This design means that if a single seal's NEK were somehow recovered, it compromises only that note, not all Seals.
All cryptography uses the browser's built-in Web Crypto API (no third-party crypto libraries):
Keys are created as non-extractable CryptoKey objects where possible, preventing JavaScript from reading the raw key bytes.
SigNote supports two authentication methods:
SIWE is ideal for users who prioritize privacy and anonymity.
Google OAuth is ideal for users who prioritize convenience.
Users can link both SIWE and Google to the same account for flexibility. Either method can be used to sign in and access the same encrypted notes.
⚠️ Both methods validate against
NEXTAUTH_URL, which must exactly match the URL you use in the browser during local development and deployment.
| Tier | Searchable fields |
|---|---|
| Notes | Title + content |
| Secrets | Title only |
| Seals | Title only |
Titles are intentionally left unencrypted to enable full-text indexing while keeping bodies private.
bun install
cp .env.local.example .env.local
# Core configuration
NEXTAUTH_URL="http://localhost:5000"
NEXTAUTH_SECRET="replace-with-a-long-random-secret"
DATABASE_URL="postgres://<user>:<password>@<host>:5432/<database>"
# SIWE configuration
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=""
NEXT_PUBLIC_RPC_URL=""
# Google OAuth (optional)
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
# S3 file storage
AWS_S3_BUCKET=""
AWS_S3_REGION=""
AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY=""
AWS_S3_ENDPOINT="" # optional: S3-compatible provider endpoint
| Variable | Description | Required |
|---|---|---|
NEXTAUTH_URL | Exact public app origin (used for both SIWE and OAuth) | Yes |
NEXTAUTH_SECRET | Secret used by NextAuth to sign sessions | Yes |
DATABASE_URL | Postgres connection string | Yes |
PG_POOL_SIZE | Max pooled connections (default 10) | No |
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID | WalletConnect project ID used by RainbowKit/Wagmi | Yes |
NEXT_PUBLIC_RPC_URL | Ethereum RPC endpoint used by the app (for SIWE) | Yes |
GOOGLE_CLIENT_ID | Google OAuth 2.0 Client ID (from Google Cloud Console) | No* |
GOOGLE_CLIENT_SECRET | Google OAuth 2.0 Client Secret (from Google Cloud Console) | No* |
AWS_S3_BUCKET | S3 bucket name for file storage | Yes† |
AWS_S3_REGION | AWS region | Yes† |
AWS_ACCESS_KEY_ID | AWS access key ID | Yes† |
AWS_SECRET_ACCESS_KEY | AWS secret access key | Yes† |
AWS_S3_ENDPOINT | Custom endpoint for S3-compatible providers (e.g. R2) | No |
*Only required if you want to enable Google OAuth sign-in. SIWE works without Google credentials.
†Required to enable file attachments.
The local dev script runs on port
5000, soNEXTAUTH_URLshould behttp://localhost:5000unless you change the port.
bun run dev
Then open http://localhost:5000.
src/
├── app/ # App Router pages and API route handlers
│ ├── api/
│ │ ├── encryption/ # Profile and key material endpoints
│ │ ├── files/ # File upload and retrieval
│ │ ├── notes/ # Tier 1 CRUD
│ │ ├── secrets/ # Tier 2 CRUD
│ │ └── seals/ # Tier 3 CRUD
│ ├── secrets/ # Tier 2 pages
│ └── seals/ # Tier 3 pages
├── components/ # UI and feature components
├── config/ # Auth, wallet, and server configuration
├── contexts/ # EncryptionContext — MEK lifecycle management
├── controllers/ # Application logic over the database layer
├── db/ # Drizzle schema, client, and the shared note-tier repo
├── hooks/ # Client data and mutation hooks
├── lib/
│ └── crypto.ts # All client-side cryptographic operations
└── providers/ # App-wide React providers
serverShare stored in Postgres is useless without the user's passphrase.sessionStorage (which holds deviceShare) is tab-scoped and cleared automatically when the tab closes.sessionStorage entry immediately.As with any cryptography-heavy app, treat this as evolving software and review the implementation carefully before using it for sensitive real-world data.
SigNote is being opened up as a reference project for client-side encryption, note security tiers, and passwordless authentication in a modern Next.js app.
Issues, ideas, and contributions are welcome.
bun install
bun run test:e2e:prepare
bun run test:e2e
The explicit prepare command downloads Playwright Chromium and pinned PostgreSQL
18 binaries for your platform. It disables package install scripts and restores
the PostgreSQL symlinks explicitly. Nothing downloads PostgreSQL during ordinary
bun install or a test run. Re-run prepare after upgrading Playwright or the
PostgreSQL version in tests/setup/postgresRuntime.ts.
Each run starts a fresh temporary PostgreSQL cluster, pushes the Drizzle schema,
and starts the app and mock services. All six local Playwright workers share that
cluster, using separate test accounts. Teardown removes the cluster and stops the
processes, including on setup failure. No Docker, PostgreSQL service, or database
URL is required; incoming database URLs are ignored. Keep the app's test settings
in .env.test as usual.
Supported binary targets: macOS arm64/x64, Linux arm64/x64, and Windows x64.
On Linux CI, bun run test:e2e:prepare --with-deps also installs Chromium's system
libraries. Run the tests as a non-root user, as required by PostgreSQL. Binaries
are cached under .cache/e2e/postgres/; deleting that directory requires running
prepare again. Docker Compose remains available for the development database.