Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 0.4.0
- Implement `showMnemonic()`, `changePassword()`, and `bip85AppBip39()` with the Rust/WASM firmware requirements

## 0.3.0
- Add Bitcoin APIs, sandbox actions, and simulator transaction-vector coverage
- Validate Bitcoin and Ethereum ECDSA signatures in Anti-Klepto and direct signing flows
Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ The package is intended as a drop-in TypeScript replacement for the current
taxonomy unless there is an explicit reason to extend it.
- Bitcoin xpub, address, script config, PSBT, and message-signing support is
implemented; keep it aligned with the Rust/WASM reference and simulator
vectors. BIP85 methods remain compatibility stubs.
vectors.
- Recovery-word display, password changes, and BIP85-BIP39 derivation are
implemented.
- Cardano xpub, address, and transaction-signing support is implemented; keep
it aligned with the Rust/WASM reference behavior and simulator vectors.

Expand Down
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ applications.
## Status

- **Implemented:** WebHID and BitBoxBridge transports, Noise XX pairing,
device metadata helpers, Bitcoin xpub/address/PSBT/message signing methods,
device metadata helpers, recovery-word display, password changes, BIP85-BIP39
mnemonic derivation, Bitcoin xpub/address/PSBT/message signing methods,
script config registration, Ethereum xpub/address/signing methods,
antiklepto, transaction data streaming, EIP-712 typed messages,
`ethIdentifyCase()`, and Cardano xpub/address/signing methods.
- **Stubbed with `code: 'unsupported'`:** BIP85 methods.
- **Stubbed with `code: 'not-implemented'`:** `showMnemonic()` and
`changePassword()`.

## Installation

Expand Down Expand Up @@ -93,6 +91,19 @@ closed. Reconnect before retrying.
Call `bb02.close()` when your app is done with the device. `close()` is
idempotent and invokes the `onClose` callback supplied to the connect function.

## Device Workflows

These workflows run on the device and resolve when complete:

```ts
await bb02.showMnemonic(); // Display the recovery words on the device.
await bb02.changePassword(); // Change the password (firmware >=9.25.0).
await bb02.bip85AppBip39(); // Derive and display a BIP39 mnemonic (firmware >=9.17.0).
```

For BIP85, the user selects the word count (12, 18, or 24) and derivation index

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: only 12 or 24 words right?

on the device. Recovery words and derived mnemonics are not returned to the host.

## Bitcoin Usage

Bitcoin-family methods accept coin, keypath, script config, and xpub type
Expand Down Expand Up @@ -318,7 +329,6 @@ Common client-facing codes include:
- `communication`, `noise`, `noise-config`, `pairing-rejected`: transport,
pairing, or encrypted-channel failures.
- `version`: the connected firmware is too old for the requested method.
- `unsupported` / `not-implemented`: methods that are not currently available.

## Sandbox and Development

Expand All @@ -332,6 +342,9 @@ make sandbox-dev

Open the printed Vite URL, usually `http://localhost:5173`.

The General accordion covers device info, the root fingerprint, recovery-word
display, password changes, and BIP85-BIP39 derivation.

The Bitcoin accordion covers xpubs, addresses, script config registration,
PSBT signing, and message signing. Bitcoin unit tests are part of
`npm run build && npm test`; the firmware transaction-vector suite runs against
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bitboxswiss/bitbox-api",
"version": "0.3.0",
"version": "0.4.0",
"description": "A pure TypeScript library to interact with BitBox hardware wallets.",
"license": "Apache-2.0",
"type": "module",
Expand Down
99 changes: 99 additions & 0 deletions sandbox/src/General.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,96 @@ function DeviceInfo({ bb02 }: Props) {
);
}

function ShowMnemonic({ bb02 }: Props) {
const [running, setRunning] = useState(false);
const [err, setErr] = useState<bitbox.Error>();

const actionShowMnemonic = async (e: FormEvent) => {
e.preventDefault();
setRunning(true);
setErr(undefined);
try {
await bb02.showMnemonic();
} catch (err) {
setErr(bitbox.ensureError(err));
} finally {
setRunning(false);
}
};

return (
<>
<h4>Recovery Words</h4>
<form className="verticalForm" onSubmit={actionShowMnemonic}>
<button type="submit" disabled={running}>Show recovery words</button>
{err !== undefined && (
<ErrorNotification message={err.message} code={err.code} onClose={() => setErr(undefined)} />
)}
</form>
</>
);
}

function ChangePassword({ bb02 }: Props) {
const [running, setRunning] = useState(false);
const [err, setErr] = useState<bitbox.Error>();

const actionChangePassword = async (e: FormEvent) => {
e.preventDefault();
setRunning(true);
setErr(undefined);
try {
await bb02.changePassword();
} catch (err) {
setErr(bitbox.ensureError(err));
} finally {
setRunning(false);
}
};

return (
<>
<h4>Change Password</h4>
<form className="verticalForm" onSubmit={actionChangePassword}>
<button type="submit" disabled={running}>Change password</button>
{err !== undefined && (
<ErrorNotification message={err.message} code={err.code} onClose={() => setErr(undefined)} />
)}
</form>
</>
);
}

function Bip85AppBip39({ bb02 }: Props) {
const [running, setRunning] = useState(false);
const [err, setErr] = useState<bitbox.Error>();

const actionBip85 = async (e: FormEvent) => {
e.preventDefault();
setRunning(true);
setErr(undefined);
try {
await bb02.bip85AppBip39();
} catch (err) {
setErr(bitbox.ensureError(err));
} finally {
setRunning(false);
}
};

return (
<>
<h4>BIP-85</h4>
<form className="verticalForm" onSubmit={actionBip85}>
<button type="submit" disabled={running}>Invoke BIP-85 (BIP-39 app)</button>
{err !== undefined && (
<ErrorNotification message={err.message} code={err.code} onClose={() => setErr(undefined)} />
)}
</form>
</>
);
}

export function General({ bb02 }: Props) {
return (
<>
Expand All @@ -99,6 +189,15 @@ export function General({ bb02 }: Props) {
<div className="action">
<DeviceInfo bb02={bb02} />
</div>
<div className="action">
<ShowMnemonic bb02={bb02} />
</div>
<div className="action">
<Bip85AppBip39 bb02={bb02} />
</div>
<div className="action">
<ChangePassword bb02={bb02} />
</div>
</>
);
}
34 changes: 9 additions & 25 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@ import {
} from './internal/connect.js';
import {
CODE_INVALID_STATE,
CODE_NOT_IMPLEMENTED,
CODE_UNSUPPORTED,
CODE_USER_ABORT,
CODE_BITBOX_USER_ABORT,
ensureTyped,
toPublicError,
} from './internal/errors.js';
import {
bip85AppBip39 as bip85AppBip39Impl,
changePassword as changePasswordImpl,
deviceInfo as deviceInfoImpl,
rootFingerprint as rootFingerprintImpl,
showMnemonic as showMnemonicImpl,
} from './internal/device.js';
import {
cardanoAddress as cardanoAddressImpl,
Expand Down Expand Up @@ -50,20 +51,6 @@ import {
type PairingState,
} from './internal/pairing.js';

function unsupportedError(method: string): Error {
return {
code: CODE_UNSUPPORTED,
message: `${method} is not supported in @bitboxswiss/bitbox-api`,
};
}

function notImplementedError(method: string): Error {
return {
code: CODE_NOT_IMPLEMENTED,
message: `${method} is not yet implemented in @bitboxswiss/bitbox-api`,
};
}

function invalidStateError(method: string): Error {
return {
code: CODE_INVALID_STATE,
Expand Down Expand Up @@ -594,16 +581,14 @@ export class PairedBitBox {
return this.#runExclusive('rootFingerprint', open => rootFingerprintImpl(open.channel));
}

/** Not implemented in this TypeScript iteration. */
/** Show recovery words on the BitBox. */
async showMnemonic(): Promise<void> {
this.#requireOpen('showMnemonic');
throw notImplementedError('showMnemonic');
return this.#runExclusive('showMnemonic', open => showMnemonicImpl(open.channel));
}

/** Not implemented in this TypeScript iteration. */
/** Invokes the password change workflow on the device. Requires firmware >=9.25.0. */
async changePassword(): Promise<void> {
this.#requireOpen('changePassword');
throw notImplementedError('changePassword');
return this.#runExclusive('changePassword', open => changePasswordImpl(open.channel, open.info));
}

/** Retrieves a Bitcoin-family account xpub. */
Expand Down Expand Up @@ -851,11 +836,10 @@ export class PairedBitBox {
* Invokes the BIP85-BIP39 workflow on the device, letting the user select the number of words
* (12, 18, 24) and an index and display a derived BIP-39 mnemonic.
*
* Compatibility stub: BIP85 support is not implemented in this TypeScript iteration.
* Requires firmware >=9.17.0.
*/
async bip85AppBip39(): Promise<void> {
this.#requireOpen('bip85AppBip39');
throw unsupportedError('bip85AppBip39');
return this.#runExclusive('bip85AppBip39', open => bip85AppBip39Impl(open.channel, open.info));
}
}

Expand Down
53 changes: 52 additions & 1 deletion src/internal/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@
import { create } from '@bufbuild/protobuf';
import { bytesToHex } from '@noble/hashes/utils';
import type { DeviceInfo } from '../index.js';
import { DeviceInfoRequestSchema } from '../proto/gen/bitbox02_system_pb.js';
import {
ChangePasswordRequestSchema,
DeviceInfoRequestSchema,
} from '../proto/gen/bitbox02_system_pb.js';
import { RootFingerprintRequestSchema } from '../proto/gen/common_pb.js';
import { RequestSchema } from '../proto/gen/hww_pb.js';
import { BIP85RequestSchema } from '../proto/gen/keystore_pb.js';
import { ShowMnemonicRequestSchema } from '../proto/gen/mnemonic_pb.js';
import type { Info } from './hww.js';
import type { EncryptedChannel } from './pairing.js';
import { query, unexpectedResponse } from './proto-query.js';
import { requireVersion } from './version.js';

export async function deviceInfo(channel: EncryptedChannel): Promise<DeviceInfo> {
const response = await query(channel, create(RequestSchema, {
Expand Down Expand Up @@ -42,3 +49,47 @@ export async function rootFingerprint(channel: EncryptedChannel): Promise<string
}
return bytesToHex(response.response.value.fingerprint);
}

/** Show recovery words on the BitBox. */
export async function showMnemonic(channel: EncryptedChannel): Promise<void> {
const response = await query(channel, create(RequestSchema, {
request: {
case: 'showMnemonic',
value: create(ShowMnemonicRequestSchema),
},
}));
if (response.response.case !== 'success') {
throw unexpectedResponse();
}
}

/** Invokes the password change workflow on the device. Requires firmware >=9.25.0. */
export async function changePassword(channel: EncryptedChannel, info: Info): Promise<void> {
requireVersion(info, { major: 9, minor: 25, patch: 0 });
const response = await query(channel, create(RequestSchema, {
request: {
case: 'changePassword',
value: create(ChangePasswordRequestSchema),
},
}));
if (response.response.case !== 'success') {
throw unexpectedResponse();
}
}

/**
* Invokes the BIP85-BIP39 workflow on the device, letting the user select the number of words
* (12, 18, 24) and an index and display a derived BIP-39 mnemonic.
*/
export async function bip85AppBip39(channel: EncryptedChannel, info: Info): Promise<void> {
requireVersion(info, { major: 9, minor: 17, patch: 0 });
const response = await query(channel, create(RequestSchema, {
request: {
case: 'bip85',
value: create(BIP85RequestSchema, { app: { case: 'bip39', value: {} } }),
},
}));
if (response.response.case !== 'bip85' || response.response.value.app.case !== 'bip39') {
throw unexpectedResponse();
}
}
Loading
Loading