From eab2942af04139f179415f39c8e7f43e78a871de Mon Sep 17 00:00:00 2001 From: Natanael Mojica Date: Wed, 20 Nov 2024 20:19:59 +0100 Subject: [PATCH] Feat/m1 fixes (#3) * add address index * Throw if provided path is incorrect Comment requirement * Remove duplicated code by using our generic ledger js package to handle error response function and improve FVK type definition fix subarray offset --------- Co-authored-by: abenso --- src/app.ts | 147 ++++++++++++++++++++++---------------------------- src/consts.ts | 21 +++++--- src/helper.ts | 29 +++++----- src/types.ts | 30 +++++++---- 4 files changed, 111 insertions(+), 116 deletions(-) diff --git a/src/app.ts b/src/app.ts index c1a9abc..daa1519 100644 --- a/src/app.ts +++ b/src/app.ts @@ -14,27 +14,28 @@ * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************* */ -import { P2_VALUES, PREHASH_LEN, RANDOMIZER_LEN, SIGRSLEN } from "./consts"; -import { ResponseAddress, ResponseFvk, PenumbraIns, ResponseSign } from "./types"; - import BaseApp, { + BIP32Path, ConstructorParams, LedgerError, PAYLOAD_TYPE, - processErrorResponse, Transport, - BIP32Path, -} from "@zondax/ledger-js"; -import { processGetAddrResponse, processGetFvkResponse } from "./helper"; + processErrorResponse, + processResponse, +} from '@zondax/ledger-js' + +import { DEFAULT_PATH, P2_VALUES, PREHASH_LEN, RANDOMIZER_LEN, SIGRSLEN } from './consts' +import { processGetAddrResponse, processGetFvkResponse } from './helper' +import { AddressIndex, PenumbraIns, ResponseAddress, ResponseFvk, ResponseSign } from './types' // https://buf.build/penumbra-zone/penumbra/docs/main:penumbra.custody.v1#penumbra.custody.v1.ConfirmAddressRequest -export * from "./types"; +export * from './types' export class PenumbraApp extends BaseApp { - readonly INS!: PenumbraIns; + readonly INS!: PenumbraIns constructor(transport: Transport) { - if (transport == null) throw new Error("Transport has not been defined"); + if (transport == null) throw new Error('Transport has not been defined') const params: ConstructorParams = { cla: 0x80, @@ -48,43 +49,31 @@ export class PenumbraApp extends BaseApp { ONLY_RETRIEVE: 0x00, SHOW_ADDRESS_IN_DEVICE: 0x01, }, + // // Penumbra uses a fixed BIP44 path with exactly 3 levels: m/44'/6532'/0' acceptedPathLengths: [3], chunkSize: 250, - }; - super(transport, params); + } + super(transport, params) } - async getAddress( - path: string, - account: number, - randomizer: string | undefined = undefined, - ): Promise { - const data = this._prepareAddressData(path, account, randomizer); + async getAddress(path: string, addressIndex: AddressIndex): Promise { + const data = this._prepareAddressData(path, addressIndex) try { - const responseBuffer = await this.transport.send( - this.CLA, - this.INS.GET_ADDR, - this.P1_VALUES.ONLY_RETRIEVE, - P2_VALUES.DEFAULT, - data, - ); + const responseBuffer = await this.transport.send(this.CLA, this.INS.GET_ADDR, this.P1_VALUES.ONLY_RETRIEVE, P2_VALUES.DEFAULT, data) - const response = processGetAddrResponse(responseBuffer); + const payload = processResponse(responseBuffer) + const response = processGetAddrResponse(payload) return { address: response.address, - } as ResponseAddress; + } as ResponseAddress } catch (e) { - throw processErrorResponse(e); + throw processErrorResponse(e) } } - async showAddress( - path: string, - account: number, - randomizer: string | undefined = undefined, - ): Promise { - const data = this._prepareAddressData(path, account, randomizer); + async showAddress(path: string, addressIndex: AddressIndex): Promise { + const data = this._prepareAddressData(path, addressIndex) try { const responseBuffer = await this.transport.send( @@ -92,86 +81,80 @@ export class PenumbraApp extends BaseApp { this.INS.GET_ADDR, this.P1_VALUES.SHOW_ADDRESS_IN_DEVICE, P2_VALUES.DEFAULT, - data, - ); + data + ) - const response = processGetAddrResponse(responseBuffer); + const payload = processResponse(responseBuffer) + const response = processGetAddrResponse(payload) return { address: response.address, - } as ResponseAddress; + } as ResponseAddress } catch (e) { - throw processErrorResponse(e); + throw processErrorResponse(e) } } - async getFVK(path: string, account: number): Promise { - const updatedPath = this._updatePathWithAccount(path, account); - const serializedPath = this.serializePath(updatedPath); + async getFVK(path: string, addressIndex: AddressIndex): Promise { + const data = this._prepareAddressData(path, addressIndex) // Fvk can be retrieved without user confirmation try { - const responseBuffer = await this.transport.send( - this.CLA, - this.INS.FVK, - this.P1_VALUES.ONLY_RETRIEVE, - P2_VALUES.DEFAULT, - serializedPath, - ); + const responseBuffer = await this.transport.send(this.CLA, this.INS.FVK, this.P1_VALUES.ONLY_RETRIEVE, P2_VALUES.DEFAULT, data) - const response = processGetFvkResponse(responseBuffer); + const payload = processResponse(responseBuffer) + const response = processGetFvkResponse(payload) - return { - fvk: response.fvk, - } as ResponseFvk; + return response } catch (e) { - throw processErrorResponse(e); + throw processErrorResponse(e) } } - async sign(path: BIP32Path, account: number, blob: Buffer): Promise { - const updatedPath = this._updatePathWithAccount(path, account); - console.log("updatedPath!!!!!", blob.length, updatedPath); - const chunks = this.prepareChunks(updatedPath, blob); - console.log("chunks!!!!!", chunks.length, chunks[0]); + async sign(path: BIP32Path, addressIndex: AddressIndex, blob: Buffer): Promise { + const chunks = this.prepareChunks(path, blob) try { - let signatureResponse = await this.signSendChunk(this.INS.SIGN, 1, chunks.length, chunks[0]); + let signatureResponse = await this.signSendChunk(this.INS.SIGN, 1, chunks.length, chunks[0]) for (let i = 1; i < chunks.length; i += 1) { - signatureResponse = await this.signSendChunk(this.INS.SIGN, 1 + i, chunks.length, chunks[i]); + signatureResponse = await this.signSendChunk(this.INS.SIGN, 1 + i, chunks.length, chunks[i]) } return { signature: signatureResponse.readBytes(signatureResponse.length()), - }; + } } catch (e) { - throw processErrorResponse(e); + throw processErrorResponse(e) } } - private _prepareAddressData(path: string, account: number, randomizer: string | undefined = undefined): Buffer { - // Add /x' to the end of the path where x is the number in account - const updatedPath = this._updatePathWithAccount(path, account); - const serializedPath = this.serializePath(updatedPath); - // Serialize account - const accountBuffer = Buffer.alloc(4); - accountBuffer.writeUInt32BE(account); - const randomizerBuffer = randomizer ? Buffer.from(randomizer, "hex") : undefined; - // Ensure randomizerBuffer does not exceed 12 bytes - if (randomizerBuffer && randomizerBuffer.length > RANDOMIZER_LEN) { - throw new Error("randomizerBuffer exceeds the maximum allowed length of 12 bytes"); + private _prepareAddressData(path: string, addressIndex: AddressIndex): Buffer { + // Path must always be this + // according to penumbra team + if (path !== DEFAULT_PATH) { + throw new Error("Invalid derivation path. Must be m/44'/6532'/0'") } + // Enforce exactly 3 levels + // this was set in our class constructor [3] + const serializedPath = this.serializePath(path) + const accountBuffer = this.serializeAccountIndex(addressIndex) // concatenate data - const concatenatedBuffer: Buffer = Buffer.concat([ - serializedPath, - accountBuffer, - ...(randomizerBuffer ? [randomizerBuffer] : []), - ]); + const concatenatedBuffer: Buffer = Buffer.concat([serializedPath, accountBuffer]) - return concatenatedBuffer; + return concatenatedBuffer } - private _updatePathWithAccount(path: string, account: number): string { - return `${path}/${account}'`; + private serializeAccountIndex(accountIndex: AddressIndex): Buffer { + const accountBuffer = Buffer.alloc(4) + accountBuffer.writeUInt32LE(accountIndex.account) + + const hasRandomizerBuffer = Buffer.from([accountIndex.randomizer ? 1 : 0]) + const randomizerBuffer = accountIndex.randomizer ?? Buffer.alloc(RANDOMIZER_LEN) + // Ensure randomizerBuffer does not exceed 12 bytes + if (randomizerBuffer && randomizerBuffer.length > RANDOMIZER_LEN) { + throw new Error('randomizerBuffer exceeds the maximum allowed length of 12 bytes') + } + + return Buffer.concat([accountBuffer, hasRandomizerBuffer, randomizerBuffer]) } } diff --git a/src/consts.ts b/src/consts.ts index cbb8073..2ab4589 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -1,12 +1,17 @@ -export const APP_KEY = "PENUMBRA"; +export const APP_KEY = 'PENUMBRA' +export const DEFAULT_PATH = "m/44'/6532'/0'" export const P2_VALUES = { DEFAULT: 0x00, -}; +} -export const PKLEN = 65; -export const ADDRLEN = 80; -export const FVKLEN = 64; -export const SIGRSLEN = 64; -export const PREHASH_LEN = 32; -export const RANDOMIZER_LEN = 12; +export const PKLEN = 65 +export const ADDRLEN = 80 +// Verification key length +export const AK_LEN = 32 +// Nullifier key length +export const NK_LEN = 32 +export const FVKLEN = AK_LEN + NK_LEN +export const SIGRSLEN = 64 +export const PREHASH_LEN = 32 +export const RANDOMIZER_LEN = 12 diff --git a/src/helper.ts b/src/helper.ts index 977957a..5442b43 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,26 +1,25 @@ -import { ADDRLEN, FVKLEN } from "./consts"; -import { ResponseAddress, ResponseFvk } from "./types"; +import { ResponsePayload } from '@zondax/ledger-js/dist/payload' -export function processGetAddrResponse(response: Buffer): ResponseAddress { - const errorCodeData = response.subarray(-2); - const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; +import { ADDRLEN, AK_LEN, FVKLEN, NK_LEN } from './consts' +import { ResponseAddress, ResponseFvk } from './types' - const address = Buffer.from(response.subarray(0, ADDRLEN)); - response = response.subarray(ADDRLEN); +export function processGetAddrResponse(response: ResponsePayload): ResponseAddress { + const address = response.readBytes(ADDRLEN) return { address, - }; + } } -export function processGetFvkResponse(response: Buffer): ResponseFvk { - const errorCodeData = response.subarray(-2); - const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; +export function processGetFvkResponse(response: ResponsePayload): ResponseFvk { + const keys = response.readBytes(FVKLEN) - const fvk = Buffer.from(response.subarray(0, FVKLEN)); - response = response.subarray(FVKLEN); + // Extract ak and nullifier_key + const ak = Buffer.from(keys.subarray(0, AK_LEN)) + const nk = Buffer.from(keys.subarray(32, FVKLEN)) return { - fvk, - }; + ak, + nk, + } } diff --git a/src/types.ts b/src/types.ts index 21116ca..16523f6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,23 +1,31 @@ -import { INSGeneric } from "@zondax/ledger-js"; +import { INSGeneric } from '@zondax/ledger-js' export interface PenumbraIns extends INSGeneric { - GET_VERSION: 0x00; - GET_ADDR: 0x01; - SIGN: 0x02; - FVK: 0x03; + GET_VERSION: 0x00 + GET_ADDR: 0x01 + SIGN: 0x02 + FVK: 0x03 +} + +export interface AddressIndex { + account: number + randomizer?: Buffer } export interface ResponseAddress { - // publicKey?: Buffer; - // principal?: Buffer; - address?: Buffer; - // principalText?: string; + address?: Buffer } +// The full viewing key consists of two components: +// +// - ak +// - nk +// export interface ResponseFvk { - fvk?: Buffer; + ak: Buffer + nk: Buffer } export interface ResponseSign { - signature: Buffer; + signature: Buffer }