diff --git a/modules/hdwallet b/modules/hdwallet index eccc3fa6..a8551cce 160000 --- a/modules/hdwallet +++ b/modules/hdwallet @@ -1 +1 @@ -Subproject commit eccc3fa680d6ad79d1f4799f3860679697631551 +Subproject commit a8551cce637a04e3a082403a77104cc0eac5b3c7 diff --git a/modules/keepkey-firmware b/modules/keepkey-firmware index f322fc40..ddade55d 160000 --- a/modules/keepkey-firmware +++ b/modules/keepkey-firmware @@ -1 +1 @@ -Subproject commit f322fc4093de1631b21d8698e575a99654c71e66 +Subproject commit ddade55d97a1c2252cd3e132d99459a1b19907bd diff --git a/projects/keepkey-vault/src/bun/rest-api.ts b/projects/keepkey-vault/src/bun/rest-api.ts index dd6c3635..952e20c4 100644 --- a/projects/keepkey-vault/src/bun/rest-api.ts +++ b/projects/keepkey-vault/src/bun/rest-api.ts @@ -2067,6 +2067,44 @@ export function startRestApi(engine: EngineController, auth: AuthStore, port = 1 return json({ address }) } + if (path === '/hive/sign-message' && method === 'POST') { + auth.requireAuth(req) + // Same gates as /addresses/hive: feature flag + firmware ≥ 7.15.0 + if (getSetting('hive_enabled') !== '1') return json({ error: 'Hive is disabled' }, 403) + const fwBlock = requireChainSupport('hive') + if (fwBlock) return fwBlock + const wallet = requireWallet(engine) + const body = await parseRequest(req, S.HiveSignMessageRequest) + const messageBytes = body.is_text === false + ? Buffer.from(body.message.replace(/^0x/, ''), 'hex') + : Buffer.from(body.message, 'utf8') + if (messageBytes.length === 0 || messageBytes.length > 1024) { + throw new HttpError(400, 'Hive message must be 1–1024 bytes') + } + // Default to the posting role — Keychain signBuffer is overwhelmingly + // dApp login, which verifies against the account's posting authority. + const addressNList = body.addressNList || body.address_n || hiveRolePath('posting', 0) + const result = await emuWrap(() => (wallet as any).hiveSignMessage({ + addressNList, + message: new Uint8Array(messageBytes), + }), { operation: 'hiveSignMessage', chain: 'HIVE' }) + if (!result?.signature) throw new HttpError(500, 'Hive sign-message: device returned no signature') + const sigBytes = result.signature instanceof Uint8Array ? Buffer.from(result.signature) : Buffer.from(String(result.signature), 'hex') + const pubBytes = result.publicKey instanceof Uint8Array ? Buffer.from(result.publicKey) : Buffer.from(String(result.publicKey), 'hex') + // STM encoding: 'STM' + base58(pub33 || ripemd160(pub33)[0:4]) + let stm = '' + if (pubBytes.length === 33) { + const { ripemd160 } = await import('@noble/hashes/ripemd160') + const bs58 = (await import('bs58')).default + const checksum = Buffer.from(ripemd160(pubBytes)).subarray(0, 4) + stm = 'STM' + bs58.encode(Buffer.concat([pubBytes, checksum])) + } + return json({ + signature: sigBytes.toString('hex'), + public_key: stm, + }) + } + if (path === '/hive/sign-transfer' && method === 'POST') { auth.requireAuth(req) // Same gates as /addresses/hive: feature flag + firmware ≥ 7.15.0 diff --git a/projects/keepkey-vault/src/bun/schemas.ts b/projects/keepkey-vault/src/bun/schemas.ts index 3d572779..54f5ae77 100644 --- a/projects/keepkey-vault/src/bun/schemas.ts +++ b/projects/keepkey-vault/src/bun/schemas.ts @@ -184,6 +184,16 @@ export const TonSignRequest = z.object({ amount: z.string().optional(), // amount in nanoTON — enables clear-sign on device }).strip() +/** POST /hive/sign-message — Keychain signBuffer: sign SHA256(message bytes) on-device (fw 7.15.0+) */ +export const HiveSignMessageRequest = z.object({ + /** SLIP-0048 path; defaults to posting role m/48'/13'/4'/0'/0' (dApp login) */ + address_n: z.array(z.number().int()).optional(), + addressNList: z.array(z.number().int()).optional(), + /** Message payload. Default: UTF-8 text. If is_text=false, hex (optional 0x). */ + message: z.string().min(1), + is_text: z.boolean().optional(), +}).strip() + /** POST /hive/sign-transfer — Graphene transfer op, serialized + signed on-device (fw 7.15.0+) */ export const HiveSignTransferRequest = z.object({ /** SLIP-0048 path; defaults to active role m/48'/13'/1'/0'/0' */ diff --git a/projects/keepkey-vault/src/bun/signing-routes.ts b/projects/keepkey-vault/src/bun/signing-routes.ts index 279ffc64..0294be28 100644 --- a/projects/keepkey-vault/src/bun/signing-routes.ts +++ b/projects/keepkey-vault/src/bun/signing-routes.ts @@ -15,7 +15,7 @@ export const SIGNING_ROUTES = new Set([ '/eth/sign-transaction', '/eth/sign-typed-data', '/eth/sign', '/utxo/sign-transaction', '/xrp/sign-transaction', '/solana/sign-transaction', '/solana/sign-message', '/tron/sign-transaction', '/ton/sign-transaction', - '/hive/sign-transfer', + '/hive/sign-transfer', '/hive/sign-message', // Message / off-chain signing. The firmware always confirms these on its // OLED, but that is the device surface only — without these in the set a // REST caller reaches the device with no in-Vault review. Gate them so the @@ -69,6 +69,7 @@ export function requiredSigningFields(path: string): string[] | null { '/tron/sign-transaction': ['raw_tx', 'rawTx', 'to_address', 'amount'], '/ton/sign-transaction': ['raw_tx', 'rawTx', 'to_address', 'amount'], '/hive/sign-transfer': ['from', 'to', 'amount'], + '/hive/sign-message': ['message'], '/tron/sign-message': ['message'], '/tron/sign-typed-hash': ['domain_separator_hash'], '/ton/sign-message': ['message'],