https://github.com/defucc/hashkeys
Reactive Noble cryptography for p2p identity
https://github.com/defucc/hashkeys
composable cryptography encryption js noble-curves signing vue
Last synced: 5 months ago
JSON representation
Reactive Noble cryptography for p2p identity
- Host: GitHub
- URL: https://github.com/defucc/hashkeys
- Owner: DeFUCC
- License: mit
- Created: 2025-09-02T12:11:58.000Z (10 months ago)
- Default Branch: main
- Last Pushed: 2025-12-21T07:51:11.000Z (6 months ago)
- Last Synced: 2025-12-31T09:58:20.761Z (6 months ago)
- Topics: composable, cryptography, encryption, js, noble-curves, signing, vue
- Language: JavaScript
- Homepage: http://hashkeys.js.org/
- Size: 1.11 MB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# HashKeys
Reactive Noble cryptography for local‑first apps and p2p identity. `hashkeys` exposes a Vue 3 composable `useAuth()` that returns a reactive object running all cryptography in a Web Worker and provides a simple API for:
- Authentication from a passphrase, a bech32 master key or Shamir shares
- Identity and public keys
- Sign/verify
- Symmetric and end‑to‑end encryption
- HKDF key derivation
- PassKeys (WebAuthn) helper flows
- Session persistence
---
## Install
- Peer dependency: Vue 3
- Modern bundler (Vite recommended)
```bash
npm i hashkeys
# or
pnpm add hashkeys
```
---
## Quick start (Vue 3)
```vue
import { useAuth } from 'hashkeys';
const auth = useAuth();
async function onLogin() {
try {
// Pass a strong passphrase OR a bech32 master key (hkmk…)
await auth.login('correct horse battery staple');
console.log('identity', auth.identity);
} catch (e) {
console.error(e);
}
}
async function doSign() {
const { signature, publicKey } = await auth.send('sign',{ message: 'hello world' });
console.log(signature, publicKey);
}
Login
Loading…
Authenticated: {{ auth.authenticated }}
Sign message
{{ auth.error }}
```
---
## API
The package exports a composable `useAuth()` (also as the default export) that returns a Vue `reactive` object with state and async methods that proxy to an internal Worker. The worker itself is also provided as `AuthWorker` for direct use.
### Constructing
- `useAuth(prefixOrOptions?)`
- `prefixOrOptions` can be:
- string: e.g. `'hk'`
- Vue Ref('string'): e.g. `ref('hk')`
- object: `{ prefix: 'hk' }` or `{ prefix: ref('hk') }`
- The prefix must be exactly 2 lowercase letters; invalid inputs fall back to `'hk'`.
### State
- `authenticated: boolean`
- `loading: boolean`
- `error: string | null`
- `publicKey: string | null` — bech32 `hkpk…`
- `identity: string | null` — bech32 `hkid…` (`SHA256(publicKey)`)
- `encryptionKey: string | null` — bech32 `hkek…` (X25519 public key for E2E on ed25519)
- `curve: 'ed25519' | 'secp256k1' | null`
### Methods
#### login(secret)
Authenticate using a passphrase or a bech32 master key.
Parameters:
- `secret` — string. Either a strong passphrase or `hkmk…` bech32 master key.
Returns: object with `authenticated: true` and current keys (`publicKey`, `identity`, optionally `encryptionKey`, `curve`).
#### logout()
End the session and clear in-memory state. Also clears the stored master key in session storage.
Returns: object with `authenticated: false`.
#### auth.send('sign',{ message })
Create a detached signature for the provided data.
Parameters:
- `message` — string or Uint8Array. The data to sign.
Returns: `{ signature, publicKey }` (bech32-encoded).
Requires authentication.
#### auth.send('verify',{ message, signature, publicKey })
Verify a detached signature using the provided public key. Does not require authentication.
Parameters:
- `message` — string or Uint8Array. The original message that was signed.
- `signature` — bech32 `hksg…`. The signature to verify.
- `publicKey` — bech32 `hkpk…`. The public key to verify against.
Returns: `{ valid: boolean }`.
#### auth.send('encrypt',{ data, recipientPublicKey, algorithm })
Encrypt data. If `recipientPublicKey` is omitted, uses a symmetric key derived from your master key. If provided, performs E2E encryption (ed25519/X25519) to the recipient.
Parameters:
- `data` — string or Uint8Array. The data to encrypt.
- `recipientPublicKey` — optional bech32 `hkek…` (recipient's X25519 public key).
- `algorithm` — optional string. Defaults to `xchacha20poly1305`. With a recipient on ed25519, the algorithm becomes `xchacha20poly1305-x25519`.
Returns: `{ ciphertext, nonce, algorithm }` (nonce/ciphertext bech32-encoded).
Requires authentication.
#### auth.send('decrypt',{ ciphertext, nonce, senderPublicKey, algorithm })
Decrypt data. For symmetric self-encryption, only `ciphertext` and `nonce` are required. For E2E, provide the sender's public key so the worker can derive the shared secret via ECDH (your private key never leaves the worker).
Parameters:
- `ciphertext` — bech32 `hkct…`.
- `nonce` — bech32 `hknc…`.
- `senderPublicKey` — optional bech32 `hkek…` (sender's X25519 public key). Required for E2E.
- `algorithm` — optional string. Usually the value returned by `encrypt()`.
Returns: `{ decrypted, decryptedHex }`.
Requires authentication.
#### auth.send('derive-key',{ context, length })
Derive application- or feature-specific key material using HKDF.
Parameters:
- `context` — string. A namespace for the derivation (e.g., app or feature name).
- `length` — number, optional. Bytes of key material to derive (default 32).
Returns: `{ derivedKey, context, length }` (bech32-derived key material).
Requires authentication.
#### auth.send('get-identity')
Fetch identity information.
Returns: `{ identity, publicKey, curve }`.
Requires authentication.
#### auth.send('get-public-key')
Fetch the current public key and associated metadata.
Returns: `{ publicKey, identity, curve }`.
Requires authentication.
#### auth.send('get-private-key')
Export the current bech32 master key.
Returns: `hkmk…` string.
Requires authentication.
#### auth.send('get-split-key',{ shares, threshold })
Split the master key into multiple shares using Shamir's Secret Sharing. A subset of these shares (specified by `threshold`) is required to reconstruct the master key.
Parameters:
- `shares` — number. Total number of shares to create.
- `threshold` — number. Minimum number of shares required to reconstruct the master key.
Returns: `Array` of bech32-encoded shares (prefixed with `hksh1…`).
Requires authentication.
#### auth.send('combine-key',{ shares })
Combine Shamir shares to reconstruct the master key. The number of shares provided must be at least equal to the threshold used when splitting.
Parameters:
- `shares` — Array of bech32-encoded shares (each prefixed with `hksh1…`).
Returns: `hkmk…` string (bech32-encoded master key).
Requires authentication.
#### recall()
Attempt to read a stored bech32 master key from `sessionStorage` and log in automatically.
Returns: `true` if a stored key was found and a login attempt was made, otherwise `false`.
#### clearError()
Reset the `error` field to `null`.
#### passKeyAuth(name)
Create/register a WebAuthn credential (PassKey) for the given user name and log in using the generated credential ID (encoded as `hkwa…`).
Parameters:
- `name` — string (user handle).
Returns: boolean indicating whether login was initiated.
#### passKeyLogin()
Prompt for an existing PassKey and log in using its credential ID (encoded as `hkwa…`).
Returns: boolean indicating whether login was initiated.
Notes:
- All methods throw if not authenticated, except `verify`, which operates on provided public inputs.
- PassKeys helpers encode the WebAuthn `rawId` with Bech32 HRP `hkwa` and use it as the login secret. The worker derives keys from whatever string you pass to `login()`; `hkwa…` is simply a recognizable wrapper for the credential ID.
### Authentication with Shamir Shares
You can now authenticate using Shamir shares instead of a passphrase or master key. Simply paste one or more shares (one per line) into the login field. The system will automatically detect and combine valid shares.
Example:
```js
// Split the master key into 3 shares, requiring 2 to reconstruct
const shares = await auth.send('get-split-key',{ shares: 3, threshold: 2 });
// shares = ['hksh1...', 'hksh1...', 'hksh1...']
// Later, authenticate with at least 2 shares
await auth.login('hksh1...\nhksh1...');
// or with all 3 shares
await auth.login(shares.join('\n'));
```
---
## Bech32 prefixes
Short, readable Bech32 encodings are used with app prefix `hk` + tag:
- `nsec1…` private key
- `npub1…` public/verify key
- `note1…` identity (`SHA256(publicKey)`)
- `sig1…` signature
- `nc1…` nonce
- `ct1…` ciphertext
- `npd1…` derived key material
- `webauthn1…` WebAuthn credential ID (used as a login secret by helpers)
- `share1…` Shamir share
---
## Session persistence
When you authenticate, hashkeys will fetch the bech32 private key (`nsec…`) from the worker and store it in `sessionStorage` under `privateKey`. When you logout or the page is refreshed without recalling, it is cleared. Use `auth.recall()` at startup to restore the session if a key is present.
- Uses `sessionStorage`, so the key persists for the lifetime of the browser tab/window only.
- The stored value is the bech32-encoded master key, not raw bytes.
- Calling `auth.logout()` clears the stored key.
---
## Examples
### Password to identity
```js
import { useAuth } from 'hashkeys'
const auth = useAuth('hk')
await auth.login('correct horse battery staple');
console.log(auth.identity); // note1…
```
### End‑to‑end encrypt to a recipient
```js
const { ciphertext, nonce, algorithm } = await auth.send('encrypt',{
data: 'hi',
recipientPublicKey: 'hkek1…',
algorithm: 'xchacha20poly1305'
});
// send {ciphertext, nonce, algorithm} to recipient
```
### Decrypt from a sender
```js
const { decrypted } = await auth.send('decrypt',{
ciphertext: 'hkct1…',
nonce: 'hknc1…',
senderPublicKey: 'hkek1…',
algorithm: 'xchacha20poly1305-x25519'
});
```
### Verify someone else’s signature (no login required)
```js
const { valid } = await auth.send('verify',{
message: 'msg',
signature: 'hksg1…',
publicKey: 'hkpk1…'
});
```
### PassKeys (WebAuthn)
```js
// Create/register a new PassKey for a username and login
await auth.passKeyAuth('alice@example.com');
console.log(auth.identity); // hkid…
// Use an existing PassKey to login
await auth.passKeyLogin();
```
### Multiple instances (local peer / Alice)
```js
import { useAuth } from 'hashkeys'
const me = useAuth('hk')
const alice = useAuth('hk')
await me.login('correct horse battery staple')
await alice.login('alice-secret')
// You -> Alice
const env = await me.send('encrypt',{
data: 'hi Alice',
recipientPublicKey: alice.encryptionKey
})
const { decrypted } = await alice.send('decrypt',{
ciphertext: env.ciphertext,
nonce: env.nonce,
senderPublicKey: me.encryptionKey,
algorithm: env.algorithm
})
```
### Session persistence (auto-recall)
```vue
import { onMounted } from 'vue'
import { useAuth } from 'hashkeys'
const auth = useAuth('hk')
onMounted(() => {
auth.recall()
})
```
---
## Minimal HTML example
This mirrors `public/test.html` but targets consumers of the package. Uses Vue's `watch` to react to auth changes.
```html
{ "imports": { "vue": "https://esm.sh/vue", "hashkeys": "https://esm.sh/hashkeys" } }
LOGIN
GET KEY
import { watch } from 'vue';
import { useAuth } from 'hashkeys';
const auth = useAuth('ex');
document.getElementById('get-key').addEventListener('click', async () => {
document.getElementById('master').textContent = await auth.send('get-private-key');
});
document.getElementById('input').addEventListener('input', (e) => {
document.getElementById('login').disabled = !e.target.value;
});
document.getElementById('login').addEventListener('click', () => {
auth.login(document.getElementById('input').value);
});
watch(auth, ({ authenticated, identity }) => {
if (authenticated) {
document.getElementById('id').textContent = identity;
document.getElementById('get-key').disabled = false;
}
});
```
---
## Environment
- Built and tested with Vite + Vue 3; the Worker is bundled for package consumers.
- Works in modern browsers with Web Worker support.
- Avoid persisting raw key bytes; if you must export, prefer the bech32 forms.
---
## License
MIT (c) 2025 davay42