-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
637 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import type {Version} from '../../types/public' | ||
import {MessageAddressFieldType} from '../../types/public' | ||
import type {ParsedMessageData, Uint32_t, Uint8_t} from '../../types/internal' | ||
import {path_to_buf, uint32_to_buf, uint8_to_buf} from '../../utils/serialize' | ||
import {serializeAddressParams} from './addressParams' | ||
|
||
export function serializeMessageDataInit( | ||
version: Version, | ||
msgData: ParsedMessageData, | ||
): Buffer { | ||
const msgLengthBuffer = uint32_to_buf( | ||
(msgData.messageHex.length / 2) as Uint32_t, | ||
) | ||
|
||
const hashPayloadBuffer = msgData.hashPayload | ||
? uint8_to_buf(1 as Uint8_t) | ||
: uint8_to_buf(0 as Uint8_t) | ||
|
||
const isAsciiBuffer = msgData.isAscii | ||
? uint8_to_buf(1 as Uint8_t) | ||
: uint8_to_buf(0 as Uint8_t) | ||
|
||
const addressFieldTypeEncoding = { | ||
[MessageAddressFieldType.ADDRESS]: 0x01, | ||
[MessageAddressFieldType.KEYHASH]: 0x02, | ||
} as const | ||
const addressFieldTypeBuffer = uint8_to_buf( | ||
addressFieldTypeEncoding[msgData.addressFieldType] as Uint8_t, | ||
) | ||
|
||
const addressBuffer = | ||
msgData.addressFieldType === MessageAddressFieldType.ADDRESS | ||
? serializeAddressParams(msgData.address, version) | ||
: Buffer.concat([]) | ||
|
||
return Buffer.concat([ | ||
msgLengthBuffer, | ||
path_to_buf(msgData.signingPath), | ||
hashPayloadBuffer, | ||
isAsciiBuffer, | ||
addressFieldTypeBuffer, | ||
addressBuffer, | ||
]) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import {buf_to_uint32, hex_to_buf, uint32_to_buf} from '../utils/serialize' | ||
import {DeviceVersionUnsupported, InvalidDataReason} from '../errors' | ||
import type {ParsedMessageData} from '../types/internal' | ||
import { | ||
ED25519_SIGNATURE_LENGTH, | ||
PUBLIC_KEY_LENGTH, | ||
Uint32_t, | ||
} from '../types/internal' | ||
import type {SignedMessageData, Version} from '../types/public' | ||
import {getVersionString} from '../utils' | ||
import {INS} from './common/ins' | ||
import type {Interaction, SendParams} from './common/types' | ||
import {getCompatibility} from './getVersion' | ||
import {serializeMessageDataInit} from './serialization/messageData' | ||
import {validate} from '../utils/parse' | ||
|
||
const send = (params: { | ||
p1: number | ||
p2: number | ||
data: Buffer | ||
expectedResponseLength?: number | ||
}): SendParams => ({ins: INS.SIGN_MESSAGE, ...params}) | ||
|
||
export function* signMessage( | ||
version: Version, | ||
msgData: ParsedMessageData, | ||
): Interaction<SignedMessageData> { | ||
if (!getCompatibility(version).supportsMessageSigning) { | ||
throw new DeviceVersionUnsupported( | ||
`CIP-8 message signing not supported by Ledger app version ${getVersionString( | ||
version, | ||
)}.`, | ||
) | ||
} | ||
|
||
const enum P1 { | ||
STAGE_INIT = 0x01, | ||
STAGE_CHUNK = 0x02, | ||
STAGE_CONFIRM = 0x03, | ||
} | ||
const enum P2 { | ||
UNUSED = 0x00, | ||
} | ||
|
||
// INIT | ||
yield send({ | ||
p1: P1.STAGE_INIT, | ||
p2: P2.UNUSED, | ||
data: serializeMessageDataInit(version, msgData), | ||
expectedResponseLength: 0, | ||
}) | ||
|
||
// CHUNK | ||
const MAX_CIP8_MSG_FIRST_CHUNK_ASCII_SIZE = 198 | ||
const MAX_CIP8_MSG_FIRST_CHUNK_HEX_SIZE = 99 | ||
const MAX_CIP8_MSG_HIDDEN_CHUNK_SIZE = 250 | ||
|
||
const msgBytes = hex_to_buf(msgData.messageHex) | ||
|
||
const getChunkData = (start: number, end: number) => { | ||
const chunk = msgBytes.slice(start, end) | ||
return Buffer.concat([uint32_to_buf(chunk.length as Uint32_t), chunk]) | ||
} | ||
|
||
const firstChunkSize = msgData.isAscii | ||
? MAX_CIP8_MSG_FIRST_CHUNK_ASCII_SIZE | ||
: MAX_CIP8_MSG_FIRST_CHUNK_HEX_SIZE | ||
|
||
let start = 0 | ||
let end = Math.min(msgBytes.length, firstChunkSize) | ||
|
||
yield send({ | ||
p1: P1.STAGE_CHUNK, | ||
p2: P2.UNUSED, | ||
data: getChunkData(start, end), | ||
expectedResponseLength: 0, | ||
}) | ||
start = end | ||
|
||
if (start < msgBytes.length) { | ||
// non-hashed messages must be processed in a single APDU | ||
validate( | ||
msgData.hashPayload, | ||
InvalidDataReason.MESSAGE_DATA_LONG_NON_HASHED_MSG, | ||
) | ||
} | ||
while (start < msgBytes.length) { | ||
end = Math.min(msgBytes.length, start + MAX_CIP8_MSG_HIDDEN_CHUNK_SIZE) | ||
|
||
yield send({ | ||
p1: P1.STAGE_CHUNK, | ||
p2: P2.UNUSED, | ||
data: getChunkData(start, end), | ||
expectedResponseLength: 0, | ||
}) | ||
|
||
start = end | ||
} | ||
|
||
// CONFIRM | ||
const MAX_ADDRESS_SIZE = 128 | ||
|
||
const confirmResponse = yield send({ | ||
p1: P1.STAGE_CONFIRM, | ||
p2: P2.UNUSED, | ||
data: Buffer.concat([]), | ||
expectedResponseLength: | ||
ED25519_SIGNATURE_LENGTH + PUBLIC_KEY_LENGTH + 4 + MAX_ADDRESS_SIZE, | ||
}) | ||
|
||
let s = 0 | ||
const signatureHex = confirmResponse | ||
.slice(s, s + ED25519_SIGNATURE_LENGTH) | ||
.toString('hex') | ||
s += ED25519_SIGNATURE_LENGTH | ||
|
||
const signingPublicKeyHex = confirmResponse | ||
.slice(s, s + PUBLIC_KEY_LENGTH) | ||
.toString('hex') | ||
s += PUBLIC_KEY_LENGTH | ||
|
||
const addressFieldSizeBuf = confirmResponse.slice(s, s + 4) | ||
s += 4 | ||
const addressFieldSize = buf_to_uint32(addressFieldSizeBuf) | ||
const addressFieldHex = confirmResponse | ||
.slice(s, s + addressFieldSize) | ||
.toString('hex') | ||
|
||
return { | ||
signatureHex, | ||
signingPublicKeyHex, | ||
addressFieldHex, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import {unreachable} from '../utils/assert' | ||
import {InvalidDataReason} from '../errors/invalidDataReason' | ||
import type {ParsedMessageData} from '../types/internal' | ||
import type {MessageData} from '../types/public' | ||
import {MessageAddressFieldType} from '../types/public' | ||
import {parseBIP32Path, parseHexString} from '../utils/parse' | ||
import {parseAddress} from './address' | ||
|
||
// check if a non-null-terminated buffer contains printable ASCII between 32 and 126 (inclusive) | ||
// copied from Ledger app | ||
function isPrintableAscii(buffer: Buffer): boolean { | ||
for (let i = 0; i < buffer.length; i++) { | ||
if (buffer[i] > 126) return false | ||
if (buffer[i] < 32) return false | ||
} | ||
|
||
return true | ||
} | ||
|
||
// check if the string can be unambiguously displayed to the user | ||
// copied from Ledger app | ||
function isAscii(msg: string): boolean { | ||
const buffer = Buffer.from(msg, 'hex') | ||
|
||
// must not be empty | ||
if (buffer.length === 0) return false | ||
|
||
// no non-printable characters except spaces | ||
if (!isPrintableAscii(buffer)) return false | ||
|
||
const space = ' '.charCodeAt(0) | ||
|
||
// no leading spaces | ||
if (buffer[0] === space) return false | ||
|
||
// no trailing spaces | ||
if (buffer[buffer.length - 1] === space) return false | ||
|
||
// only single spaces | ||
for (let i = 0; i + 1 < buffer.length; i++) { | ||
if (buffer[i] === space && buffer[i + 1] === space) return false | ||
} | ||
|
||
return true | ||
} | ||
|
||
export function parseMessageData(data: MessageData): ParsedMessageData { | ||
const preferHexDisplay = data.preferHexDisplay || false | ||
const common = { | ||
signingPath: parseBIP32Path( | ||
data.signingPath, | ||
InvalidDataReason.MESSAGE_DATA_INVALID_WITNESS_PATH, | ||
), | ||
isAscii: isAscii(data.messageHex) && !preferHexDisplay, | ||
hashPayload: data.hashPayload, | ||
messageHex: parseHexString( | ||
data.messageHex, | ||
InvalidDataReason.MESSAGE_DATA_INVALID_MESSAGE_HEX, | ||
), | ||
} | ||
|
||
switch (data.addressFieldType) { | ||
case MessageAddressFieldType.ADDRESS: | ||
return { | ||
...common, | ||
addressFieldType: MessageAddressFieldType.ADDRESS, | ||
address: parseAddress(data.network, data.address), | ||
} | ||
case MessageAddressFieldType.KEYHASH: | ||
return { | ||
...common, | ||
addressFieldType: MessageAddressFieldType.KEYHASH, | ||
} | ||
default: | ||
unreachable(data) | ||
} | ||
} |
Oops, something went wrong.