Skip to content

Commit

Permalink
Feat/m1 fixes (#3)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
neithanmo and abenso authored Nov 20, 2024
1 parent 4bde87b commit eab2942
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 116 deletions.
147 changes: 65 additions & 82 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -48,130 +49,112 @@ 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<ResponseAddress> {
const data = this._prepareAddressData(path, account, randomizer);
async getAddress(path: string, addressIndex: AddressIndex): Promise<ResponseAddress> {
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<ResponseAddress> {
const data = this._prepareAddressData(path, account, randomizer);
async showAddress(path: string, addressIndex: AddressIndex): Promise<ResponseAddress> {
const data = this._prepareAddressData(path, addressIndex)

try {
const responseBuffer = await this.transport.send(
this.CLA,
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<ResponseFvk> {
const updatedPath = this._updatePathWithAccount(path, account);
const serializedPath = this.serializePath(updatedPath);
async getFVK(path: string, addressIndex: AddressIndex): Promise<ResponseFvk> {
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<ResponseSign> {
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<ResponseSign> {
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])
}
}
21 changes: 13 additions & 8 deletions src/consts.ts
Original file line number Diff line number Diff line change
@@ -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
29 changes: 14 additions & 15 deletions src/helper.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
30 changes: 19 additions & 11 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit eab2942

Please sign in to comment.