# @l8d/viem-account-yubikey

Use YubiKey hardware security keys for signing Ethereum transactions and messages with [viem](https://viem.sh/).

This package enables you to create viem accounts that use YubiKey's OpenPGP card applet for signing operations, providing hardware-based security for your Ethereum interactions.

## Features

- 🔐 **Hardware Security**: Sign transactions using YubiKey's secure element
- 🔌 **Viem Integration**: Works seamlessly with viem's wallet client
- 🎯 **OpenPGP Slots**: Support for all three OpenPGP key slots (Signature, Authentication, Decryption)
- 🔍 **Auto-Discovery**: Automatically find which slot controls your Ethereum address
- ⚡ **Type-Safe**: Full TypeScript support with type definitions
- 📦 **Zero Config**: Works out of the box with sensible defaults

## Installation

```bash
npm install @l8d/viem-account-yubikey viem@^2.0.0 pcsclite@^1.0.0
```

### System Requirements

- **Node.js**: 18 or higher
- **PC/SC Daemon**:
  - **macOS**: Built-in, works out of the box
  - **Linux**: Install `pcscd` (`sudo apt install pcscd` on Debian/Ubuntu)
  - **Windows**: Built-in smart card service
- **YubiKey**: With secp256k1 keys configured in OpenPGP applet

## Quick Start

```typescript
import { yubikeyToAccount } from '@l8d/viem-account-yubikey';
import { createWalletClient, http } from 'viem';
import { mainnet } from 'viem/chains';

// Create YubiKey account
const account = await yubikeyToAccount({
  address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
  // Optional: specify PIN (defaults to '123456')
  pin: '123456',
  // Optional: slot will be auto-discovered if not specified
});

// Create wallet client
const client = createWalletClient({
  account,
  chain: mainnet,
  transport: http(),
});

// Send transaction
const hash = await client.sendTransaction({
  to: '0x...',
  value: 1000000000000000000n, // 1 ETH
});

console.log('Transaction hash:', hash);
```

## Usage

### Basic Account Creation

```typescript
import { yubikeyToAccount } from '@l8d/viem-account-yubikey';

// Minimal configuration (slot will be auto-discovered)
const account = await yubikeyToAccount({
  address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
});
```

### Explicit Slot Specification

For better performance, you can explicitly specify which OpenPGP slot to use:

```typescript
import { yubikeyToAccount, OpenPGPSlot } from '@l8d/viem-account-yubikey';

const account = await yubikeyToAccount({
  address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
  slot: OpenPGPSlot.Signature, // or Authentication, or Decryption
  pin: '123456',
});
```

### Available OpenPGP Slots

YubiKey's OpenPGP applet has three key slots:

```typescript
enum OpenPGPSlot {
  Signature = 0xb6,      // Primary signing slot
  Authentication = 0xa4,  // Authentication operations
  Decryption = 0xb8,      // Encryption/decryption operations
}
```

All three slots can hold secp256k1 keys and be used for Ethereum signing.

### Signing Messages

```typescript
// Sign a personal message (EIP-191)
const signature = await account.signMessage({
  message: 'Hello, Ethereum!',
});
```

### Signing Typed Data

```typescript
// Sign EIP-712 typed data
const signature = await account.signTypedData({
  domain: {
    name: 'Ether Mail',
    version: '1',
    chainId: 1,
    verifyingContract: '0x...',
  },
  types: {
    Person: [
      { name: 'name', type: 'string' },
      { name: 'wallet', type: 'address' },
    ],
  },
  primaryType: 'Person',
  message: {
    name: 'Bob',
    wallet: '0x...',
  },
});
```

### Direct Signing (Without Client)

You can also use the account directly without creating a wallet client:

```typescript
const account = await yubikeyToAccount({
  address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
});

// Sign transaction
const signedTx = await account.signTransaction({
  to: '0x...',
  value: 1000000000000000000n,
  chainId: 1,
  nonce: 42,
  maxFeePerGas: 100n,
  maxPriorityFeePerGas: 100n,
});

// Sign message
const signature = await account.signMessage({
  message: 'Hello!',
});
```

## Configuration Options

```typescript
interface YubikeyAccountConfig {
  /**
   * The Ethereum address controlled by the YubiKey
   */
  address: `0x${string}`;

  /**
   * Optional: Explicitly specify which OpenPGP slot to use
   * If not provided, the slot will be auto-discovered
   */
  slot?: OpenPGPSlot;

  /**
   * Optional: OpenPGP card PIN (default: "123456")
   * Required for signing operations
   */
  pin?: string;

  /**
   * Optional: Timeout for signing operations in milliseconds (default: 30000)
   */
  timeout?: number;

  /**
   * Optional: Enable debug logging
   */
  debug?: boolean;
}
```

## YubiKey Setup

Before using this package, you need to:

1. **Configure secp256k1 keys** in your YubiKey's OpenPGP applet
2. **Know the Ethereum address** derived from your YubiKey's public key

### Configuring Your YubiKey

You can use the YubiKey Manager (`ykman`) or GPG to configure OpenPGP keys:

```bash
# Install YubiKey Manager
brew install ykman  # macOS
# or
sudo apt install yubikey-manager  # Linux

# View current OpenPGP configuration
ykman openpgp info
```

For importing existing secp256k1 keys or generating new ones, refer to the [YubiKey OpenPGP documentation](https://developers.yubico.com/PGP/).

### Finding Your Ethereum Address

If you already have a secp256k1 key on your YubiKey but don't know the corresponding Ethereum address, you can use utilities from the parent `@l8d/hardhat-yubikey` package to discover it.

## Transaction Types Support

This package supports all Ethereum transaction types:

- ✅ **Legacy Transactions** (Type 0)
- ✅ **EIP-2930** Access List Transactions (Type 1)
- ✅ **EIP-1559** Dynamic Fee Transactions (Type 2)

The `v` value is automatically calculated based on the transaction type and chain ID.

## Error Handling

```typescript
try {
  const account = await yubikeyToAccount({
    address: '0x...',
  });
} catch (error) {
  if (error.message.includes('PC/SC')) {
    console.error('YubiKey connection failed. Is it plugged in?');
  } else if (error.message.includes('PIN verification failed')) {
    console.error('Incorrect PIN provided');
  } else if (error.message.includes('Could not find OpenPGP slot')) {
    console.error('Address not found on any YubiKey slot');
  } else {
    console.error('Unexpected error:', error);
  }
}
```

## Debug Logging

Enable debug logging to troubleshoot issues:

```typescript
// Enable for specific account
const account = await yubikeyToAccount({
  address: '0x...',
  debug: true,
});

// Or enable via environment variable
DEBUG=viem-account-yubikey:* node your-script.js
```

## How It Works

1. **Connection**: Uses PC/SC (Personal Computer/Smart Card) interface to communicate with YubiKey
2. **OpenPGP Applet**: Sends APDU commands to YubiKey's OpenPGP card application
3. **ECDSA Signing**: YubiKey performs ECDSA signing using secp256k1 curve
4. **Signature Recovery**: Recovers the Ethereum `v` value by trying both recovery IDs
5. **Format Conversion**: Converts OpenPGP signature format to Ethereum signature format

## Security Considerations

- 🔐 **Private keys never leave the YubiKey**: All signing happens on the secure element
- 🔑 **PIN Protection**: YubiKey requires PIN verification before signing
- 🛡️ **Physical Confirmation**: Some YubiKey models require physical touch for signing
- ⚠️ **Default PIN**: Change the default PIN (`123456`) after initial setup!

## Limitations

- **Slot Auto-Discovery**: First-time slot discovery requires trying all three slots, which may take a few seconds
- **Single Device**: Currently supports one YubiKey at a time
- **PC/SC Required**: Requires PC/SC daemon to be running on the system

## Related Packages

- [`@l8d/hardhat-yubikey`](https://github.com/mezosphere/hardhat-yubikey) - Hardhat plugin for YubiKey integration
- [`viem`](https://viem.sh/) - Lightweight Ethereum library
- [`@celo/viem-account-ledger`](https://www.npmjs.com/package/@celo/viem-account-ledger) - Similar package for Ledger devices

## Troubleshooting

### "Failed to initialize PC/SC"

- **macOS**: PC/SC should work out of the box
- **Linux**: Install and start pcscd: `sudo apt install pcscd && sudo systemctl start pcscd`
- **Windows**: Ensure Smart Card service is running

### "Could not find OpenPGP slot"

- Verify your YubiKey has secp256k1 keys configured
- Check that the address matches the public key on your YubiKey
- Try explicitly specifying the slot

### "PIN verification failed"

- Default PIN is `123456`
- After 3 failed attempts, PIN will be blocked
- Use PUK to unlock if blocked

## Contributing

Contributions are welcome! Please see the [main repository](https://github.com/mezosphere/hardhat-yubikey) for contribution guidelines.

## License

MIT

## Author

Tenor Biel ([@l8d](https://github.com/l8d))

## Acknowledgments

This package is inspired by:
- [`@celo/viem-account-ledger`](https://github.com/celo-org/developer-tooling) by cLabs
- [`@nomicfoundation/hardhat-ledger`](https://github.com/NomicFoundation/hardhat) by Nomic Foundation

---

**Need help?** [Open an issue](https://github.com/mezosphere/hardhat-yubikey/issues) on GitHub.
# viem-account-yubikey
