An open API service indexing awesome lists of open source software.

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

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