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
51 changes: 45 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ USAGE
* [`proton key:lock`](#proton-keylock)
* [`proton key:remove [PRIVATEKEY]`](#proton-keyremove-privatekey)
* [`proton key:reset`](#proton-keyreset)
* [`proton key:reveal-disable`](#proton-keyreveal-disable)
* [`proton key:reveal-setup`](#proton-keyreveal-setup)
* [`proton key:unlock [PASSWORD]`](#proton-keyunlock-password)
* [`proton msig:approve PROPOSER PROPOSAL AUTH`](#proton-msigapprove-proposer-proposal-auth)
* [`proton msig:cancel PROPOSALNAME AUTH`](#proton-msigcancel-proposalname-auth)
Expand Down Expand Up @@ -675,28 +677,36 @@ _See code: [lib/commands/key/generate.js](https://github.com/ProtonProtocol/prot

## `proton key:get PUBLICKEY`

Find private key for public key
Reveal the private key for a saved public key (gated by the reveal password if one is set)

```
USAGE
$ proton key:get [PUBLICKEY]
$ proton key:get [PUBLICKEY] [-f]

FLAGS
-f, --force Skip the typed confirmation and TTY check. Intended for non-interactive scripts. Does NOT skip the reveal password if one is set.

DESCRIPTION
Find private key for public key
Reveal the private key for a saved public key (gated by the reveal password
if one is set)
```

_See code: [lib/commands/key/get.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.97/lib/commands/key/get.js)_

## `proton key:list`

List All Key
List saved keys. Shows public keys and associated accounts by default; pass --reveal-private to include private keys (gated by the reveal password if one is set).

```
USAGE
$ proton key:list
$ proton key:list [-r] [-f]

FLAGS
-r, --reveal-private Include private keys in the output (requires the reveal password if set, or a typed confirmation otherwise)
-f, --force Skip the typed confirmation and TTY check when used with --reveal-private. Intended for non-interactive scripts. Does NOT skip the reveal password if one is set.

DESCRIPTION
List All Key
List saved keys. Shows public keys and associated accounts by default; pass --reveal-private to include private keys (gated by the reveal password if one is set).
```

_See code: [lib/commands/key/list.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.97/lib/commands/key/list.js)_
Expand Down Expand Up @@ -743,6 +753,35 @@ DESCRIPTION

_See code: [lib/commands/key/reset.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.97/lib/commands/key/reset.js)_

## `proton key:reveal-disable`

Remove the reveal password (requires entering the current one)

```
USAGE
$ proton key:reveal-disable

DESCRIPTION
Remove the reveal password (requires entering the current one)
```

_See code: [lib/commands/key/reveal-disable.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.97/lib/commands/key/reveal-disable.js)_

## `proton key:reveal-setup`

Set or change the reveal password required to view private keys via key:get or key:list --reveal-private

```
USAGE
$ proton key:reveal-setup

DESCRIPTION
Set or change the reveal password required to view private keys via key:get
or key:list --reveal-private
```

_See code: [lib/commands/key/reveal-setup.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.97/lib/commands/key/reveal-setup.js)_

## `proton key:unlock [PASSWORD]`

Unlock all keys (Caution: Your keys will be stored in plaintext on disk)
Expand Down
47 changes: 39 additions & 8 deletions src/commands/key/get.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,53 @@
import { Command } from '@oclif/command'
import { Command, flags } from '@oclif/command'
import { CliUx } from '@oclif/core'
import { green, red } from 'colors'
import { green, red, yellow } from 'colors'
import passwordManager from '../../storage/passwordManager'
import { isRevealPasswordSet, requireRevealPassword } from '../../storage/revealPassword'
import { CONFIRMATION_PHRASE } from '../../storage/confirmation'

export default class GetPrivateKey extends Command {
static description = 'Find private key for public key'
static description = 'Reveal the private key for a saved public key (gated by the reveal password if one is set)'

static args = [
{name: 'publicKey', required: true},
{ name: 'publicKey', required: true },
]

static flags = {
force: flags.boolean({
char: 'f',
description: 'Skip the typed confirmation and TTY check. Intended for non-interactive scripts. Does NOT skip the reveal password if one is set.',
default: false,
}),
}

async run() {
const { args } = this.parse(GetPrivateKey)
const { args, flags: parsedFlags } = this.parse(GetPrivateKey)

const privateKey = await passwordManager.getPrivateKey(args.publicKey)
if (privateKey) {
CliUx.ux.log(`${green('Success:')} ${privateKey}`)
} else {
if (!privateKey) {
CliUx.ux.log(`${red('Failure:')} No matching private key found in saved keys`)
return
}

if (!parsedFlags.force) {
if (!process.stdout.isTTY || !process.stdin.isTTY) {
CliUx.ux.error('Refusing to print a private key to a non-TTY stream. Run this in an interactive terminal, or pass --force in a trusted script.')
}
}

if (isRevealPasswordSet()) {
// Reveal password is required regardless of --force.
await requireRevealPassword()
} else if (!parsedFlags.force) {
CliUx.ux.log(yellow('No reveal password is set. Run `proton key:reveal-setup` to protect private-key reveals behind a password that AI agents running on this machine cannot bypass.'))
CliUx.ux.log(red('WARNING: This will print a PRIVATE KEY to the terminal.'))
CliUx.ux.log(red('Anyone with this key can control the associated account. Make sure no one is watching and your terminal is not being recorded.'))
const confirmation = await CliUx.ux.prompt(`Type "${CONFIRMATION_PHRASE}" to continue`)
if (confirmation !== CONFIRMATION_PHRASE) {
CliUx.ux.error('Confirmation phrase did not match. Aborting.')
}
}

CliUx.ux.log(`${green('Success:')} ${privateKey}`)
}
}
90 changes: 82 additions & 8 deletions src/commands/key/list.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,95 @@
import { Command } from '@oclif/command'
import {CliUx} from '@oclif/core'
import { Command, flags } from '@oclif/command'
import { CliUx } from '@oclif/core'
import { Key } from '@proton/js'
import { red, yellow } from 'colors'
import passwordManager from '../../storage/passwordManager'
import { network } from '../../storage/networks'
import { isRevealPasswordSet, requireRevealPassword } from '../../storage/revealPassword'
import { CONFIRMATION_PHRASE } from '../../storage/confirmation'

export default class ListAllKeys extends Command {
static description = 'List All Key'
static description = 'List saved keys. Shows public keys and associated accounts by default; pass --reveal-private to include private keys (gated by the reveal password if one is set).'

static flags = {
'reveal-private': flags.boolean({
char: 'r',
description: 'Include private keys in the output (requires the reveal password if set, or a typed confirmation otherwise)',
default: false,
}),
force: flags.boolean({
char: 'f',
description: 'Skip the typed confirmation and TTY check when used with --reveal-private. Intended for non-interactive scripts. Does NOT skip the reveal password if one is set.',
default: false,
}),
}

async run() {
const { flags: parsedFlags } = this.parse(ListAllKeys)
const revealPrivate = parsedFlags['reveal-private']

const privateKeys = await passwordManager.getPrivateKeys()
const displayKeys = privateKeys.map(privateKey => {
const parsedPrivateKey = Key.PrivateKey.fromString(privateKey)
if (privateKeys.length === 0) {
CliUx.ux.log('No keys saved.')
return
}

const publicKeys = privateKeys.map(pk => Key.PrivateKey.fromString(pk).getPublicKey().toString())

let accountsByPubkey: Record<string, Array<{ account: string; permission: string }>> = {}
try {
const res = await network.rpc.get_accounts_by_authorizers([], publicKeys)
for (const entry of res.accounts) {
if (!entry.authorizing_key) continue
// RPC may return keys in legacy EOS format; normalize to modern PUB_K1_... form.
const canonical = Key.PublicKey.fromString(entry.authorizing_key).toString()
const list = accountsByPubkey[canonical] || (accountsByPubkey[canonical] = [])
list.push({ account: entry.account_name, permission: entry.permission_name })
}
} catch (err) {
CliUx.ux.warn(`Could not resolve accounts for keys: ${(err as Error).message}`)
}

if (!revealPrivate) {
const display = privateKeys.map(pk => {
const publicKey = Key.PrivateKey.fromString(pk).getPublicKey().toString()
return {
publicKey,
accounts: accountsByPubkey[publicKey] || [],
}
})
CliUx.ux.styledJSON(display)
CliUx.ux.log(yellow('\nPrivate keys hidden. Use --reveal-private to include them.'))
return
}

if (!parsedFlags.force) {
if (!process.stdout.isTTY || !process.stdin.isTTY) {
CliUx.ux.error('Refusing to print private keys to a non-TTY stream. Run this in an interactive terminal, or pass --force in a trusted script.')
}
}

if (isRevealPasswordSet()) {
// Reveal password is required regardless of --force.
await requireRevealPassword()
} else if (!parsedFlags.force) {
CliUx.ux.log(yellow('No reveal password is set. Run `proton key:reveal-setup` to protect private-key reveals behind a password that AI agents running on this machine cannot bypass.'))
CliUx.ux.log(red('WARNING: This will print your PRIVATE KEYS to the terminal.'))
CliUx.ux.log(red('Anyone with these keys can control your accounts. Make sure no one is watching and your terminal is not being recorded.'))
const confirmation = await CliUx.ux.prompt(`Type "${CONFIRMATION_PHRASE}" to continue`)
if (confirmation !== CONFIRMATION_PHRASE) {
CliUx.ux.error('Confirmation phrase did not match. Aborting.')
}
}

const display = privateKeys.map(pk => {
const parsed = Key.PrivateKey.fromString(pk)
const publicKey = parsed.getPublicKey().toString()
return {
publicKey: parsedPrivateKey.getPublicKey().toString(),
privateKey: parsedPrivateKey.toString()
publicKey,
privateKey: parsed.toString(),
accounts: accountsByPubkey[publicKey] || [],
}
})
CliUx.ux.styledJSON(displayKeys);
CliUx.ux.styledJSON(display)
}
}
36 changes: 36 additions & 0 deletions src/commands/key/reveal-disable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Command } from '@oclif/command'
import { CliUx } from '@oclif/core'
import { green, red } from 'colors'
import {
clearRevealPasswordHash,
getStoredRevealPasswordHash,
isRevealPasswordSet,
verifyRevealPassword,
} from '../../storage/revealPassword'

export default class KeyRevealDisable extends Command {
static description = 'Remove the reveal password (requires entering the current one)'

async run() {
if (!isRevealPasswordSet()) {
CliUx.ux.log('No reveal password is set.')
return
}
if (!process.stdin.isTTY || !process.stdout.isTTY) {
CliUx.ux.error('This command must be run in an interactive terminal.')
}

const current = await CliUx.ux.prompt('Current reveal password', { type: 'hide' })
const stored = getStoredRevealPasswordHash()!
if (!verifyRevealPassword(current, stored)) {
CliUx.ux.error('Current reveal password is incorrect.')
}

clearRevealPasswordHash()
CliUx.ux.log(green('Success: reveal password removed. key:get and key:list --reveal-private will no longer prompt for it.'))
}

async catch(e: Error) {
CliUx.ux.error(red(e.message))
}
}
50 changes: 50 additions & 0 deletions src/commands/key/reveal-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Command } from '@oclif/command'
import { CliUx } from '@oclif/core'
import { green, red, yellow } from 'colors'
import {
hashRevealPassword,
isRevealPasswordSet,
setRevealPasswordHash,
verifyRevealPassword,
getStoredRevealPasswordHash,
} from '../../storage/revealPassword'

const MIN_LENGTH = 12

export default class KeyRevealSetup extends Command {
static description = 'Set or change the reveal password required to view private keys via key:get or key:list --reveal-private'

async run() {
if (!process.stdin.isTTY || !process.stdout.isTTY) {
CliUx.ux.error('This command must be run in an interactive terminal.')
}

if (isRevealPasswordSet()) {
CliUx.ux.log(yellow('A reveal password is already set. Enter the current one to change it.'))
const current = await CliUx.ux.prompt('Current reveal password', { type: 'hide' })
const stored = getStoredRevealPasswordHash()!
if (!verifyRevealPassword(current, stored)) {
CliUx.ux.error('Current reveal password is incorrect.')
}
} else {
CliUx.ux.log('Setting a reveal password. This will be required whenever a private key is revealed via key:get or key:list --reveal-private.')
CliUx.ux.log(yellow('Use a password you keep only in your head. Do not share it with AI agents, pair programmers, or scripts.'))
}

const pw1 = await CliUx.ux.prompt(`New reveal password (min ${MIN_LENGTH} chars)`, { type: 'hide' })
if (pw1.length < MIN_LENGTH) {
CliUx.ux.error(`Password must be at least ${MIN_LENGTH} characters.`)
}
const pw2 = await CliUx.ux.prompt('Confirm reveal password', { type: 'hide' })
if (pw1 !== pw2) {
CliUx.ux.error('Passwords do not match.')
}

setRevealPasswordHash(hashRevealPassword(pw1))
CliUx.ux.log(green('Success: reveal password set. key:get and key:list --reveal-private will now prompt for it.'))
}

async catch(e: Error) {
CliUx.ux.error(red(e.message))
}
}
11 changes: 8 additions & 3 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
export const networks = [
{
chain: "proton",
endpoints: ["https://proton.greymass.com"],
endpoints: [
"https://rpc.api.mainnet.metalx.com",
"https://proton.cryptolions.io",
"https://proton.eosusa.io",
],
},
{
chain: "proton-test",
endpoints: [
"https://protontestnet.ledgerwise.io",
"https://proton-testnet.eosphere.io",
"https://rpc.api.testnet.metalx.com",
"https://proton-testnet.eoscafeblock.com",
"https://test.proton.eosusa.io",
],
},
];
Expand Down
Loading