Skip to content

Commit

Permalink
Merge pull request #2 from ehanoc/revert-1-wallet-api-context
Browse files Browse the repository at this point in the history
Revert "1-1 feature parity with Kotlin"
  • Loading branch information
ehanoc committed Mar 26, 2024
2 parents 4319a82 + 6e8337c commit 8b4de1d
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 228 deletions.
121 changes: 46 additions & 75 deletions assets/arc-0052/contextual.api.crypto.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_scalarmult_ed25519_base_noclamp, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, ready, to_base64 } from "libsodium-wrappers-sumo"
import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_scalarmult_ed25519_base_noclamp, crypto_secretbox_NONCEBYTES, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, ready, to_base64 } from "libsodium-wrappers-sumo"
import * as bip39 from "bip39"
import { randomBytes } from "crypto"
import { ContextualCryptoApi, ERROR_BAD_DATA, ERROR_TAGS_FOUND, Encoding, KeyContext, SignMetadata, harden } from "./contextual.api.crypto"
Expand All @@ -9,10 +9,17 @@ import base32 from "hi-base32"
import { JSONSchemaType } from "ajv"
import { readFileSync } from "fs"
import path from "path"
import { decodeUnsignedTransaction, encodeAddress } from "algosdk"
import nacl from "tweetnacl"
const libBip32Ed25519 = require('bip32-ed25519')

function encodeAddress(publicKey: Buffer): string {
const keyHash: string = sha512_256.create().update(publicKey).hex()

// last 4 bytes of the hash
const checksum: string = keyHash.slice(-8)

return base32.encode(ConcatArrays(publicKey, Buffer.from(checksum, "hex"))).slice(0, 58)
}

function ConcatArrays(...arrs: ArrayLike<number>[]) {
const size = arrs.reduce((sum, arr) => sum + arr.length, 0)
const c = new Uint8Array(size)
Expand Down Expand Up @@ -204,7 +211,7 @@ describe("Contextual Derivation & Signing", () => {
})

describe("Signing Typed Data", () => {
it("\(OK) Sign authentication challenge of 32 bytes, encoded base64", async () => {
it("\(OK) Sign authentication challenge of 32 bytes, encoded base 64", async () => {
const challenge: Uint8Array = new Uint8Array(randomBytes(32))

// read auth schema file for authentication. 32 bytes challenge to sign
Expand All @@ -221,21 +228,6 @@ describe("Contextual Derivation & Signing", () => {
expect(isValid).toBe(true)
})

it("\(OK) Sign authentication challenge of 32 bytes, encoded msgpack", async () => {
const challenge: Uint8Array = new Uint8Array(randomBytes(32))

// read auth schema file for authentication. 32 bytes challenge to sign
const authSchema: JSONSchemaType<any> = JSON.parse(readFileSync(path.resolve(__dirname, "schemas/auth.request.json"), "utf8"))
const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: authSchema }
const encoded: Uint8Array = msgpack.encode(challenge)

const signature: Uint8Array = await cryptoService.signData(KeyContext.Address,0, 0, encoded, metadata)
expect(signature).toHaveLength(64)

const isValid: boolean = await cryptoService.verifyWithPublicKey(signature, encoded, await cryptoService.keyGen(KeyContext.Address, 0, 0))
expect(isValid).toBe(true)
})

it ("\(OK) Sign authentication challenge of 32 bytes, no encoding", async () => {
const challenge: Uint8Array = new Uint8Array(randomBytes(32))

Expand All @@ -250,7 +242,7 @@ describe("Contextual Derivation & Signing", () => {
expect(isValid).toBe(true)
})

it("\(OK) Sign Arbitrary Message against Schema, encoded base64", async () => {
it("\(OK) Sign Arbitrary Message against Schem", async () => {
const firstKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0)

const message = {
Expand Down Expand Up @@ -278,35 +270,7 @@ describe("Contextual Derivation & Signing", () => {
expect(isValid).toBe(true)
})

it("\(OK) Sign Arbitrary Message against Schema, encoded msgpack", async () => {
const firstKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0)

const message = {
letter: "Hello World"
}

const encoded: Buffer = Buffer.from(msgpack.encode(message))

// Schema of what we are signing
const jsonSchema = {
type: "object",
properties: {
letter: {
type: "string"
}
}
}

const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: jsonSchema }

const signature: Uint8Array = await cryptoService.signData(KeyContext.Address,0, 0, encoded, metadata)
expect(signature).toHaveLength(64)

const isValid: boolean = await cryptoService.verifyWithPublicKey(signature, encoded, firstKey)
expect(isValid).toBe(true)
})

it("\(FAIL) Signing attempt fails because of invalid data against Schema, encoded base64", async () => {
it("\(FAIL) Signing attempt fails because of invalid data against Schema", async () => {
const message = {
letter: "Hello World"
}
Expand All @@ -322,23 +286,41 @@ describe("Contextual Derivation & Signing", () => {
expect(cryptoService.signData(KeyContext.Identity,0, 0, encoded, metadata)).rejects.toThrowError()
})

it("\(FAIL) Signing attempt fails because of invalid data against Schema, encoded msgpack", async () => {
const message = {
letter: "Hello World"
}

const encoded: Buffer = Buffer.from(msgpack.encode(message))
describe("Reject Regular Transaction Signing. IF TAG Prexies are present signing must fail", () => {
describe("Reject tags present in the encoded payload", () => {
it("\(FAIL) [TX] Tag", async () => {
const transaction: Buffer = Buffer.concat([Buffer.from("TX"), msgpack.encode(randomBytes(64))])
const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} }

const encodedTx: Uint8Array = new Uint8Array(Buffer.from(transaction.toString('base64')))
expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND)
})

// Schema of what we are signing
const jsonSchema = {
type: "string"
}
it("\(FAIL) [MX] Tag", async () => {
const transaction: Buffer = Buffer.concat([Buffer.from("MX"), msgpack.encode(randomBytes(64))])
const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} }

const encodedTx: Buffer = Buffer.from(transaction.toString('base64'))
expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND)
})

const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: jsonSchema }
expect(cryptoService.signData(KeyContext.Identity,0, 0, encoded, metadata)).rejects.toThrowError()
})
it("\(FAIL) [Program] Tag", async () => {
const transaction: Buffer = Buffer.concat([Buffer.from("Program"), msgpack.encode(randomBytes(64))])
const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} }

const encodedTx: Buffer = Buffer.from(transaction.toString('base64'))
expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND)
})

describe("Reject Regular Transaction Signing. IF TAG Prexies are present signing must fail", () => {
it("\(FAIL) [progData] Tag", async () => {
const transaction: Buffer = Buffer.concat([Buffer.from("progData"), msgpack.encode(randomBytes(64))])
const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} }

const encodedTx: Buffer = Buffer.from(transaction.toString('base64'))
expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND)
})
})

it("\(FAIL) [TX] Tag", async () => {
const transaction: Buffer = Buffer.concat([Buffer.from("TX"), msgpack.encode(randomBytes(64))])
const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: {} }
Expand All @@ -358,23 +340,12 @@ describe("Contextual Derivation & Signing", () => {
})

it("\(FAIL) [progData] Tag", async () => {
const transaction: Buffer = Buffer.concat([Buffer.from("ProgData"), msgpack.encode(randomBytes(64))])
const transaction: Buffer = Buffer.concat([Buffer.from("progData"), msgpack.encode(randomBytes(64))])
const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: {} }
expect(cryptoService.signData(KeyContext.Identity,0, 0, transaction, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND)
})
})
})

describe("signing transactions", () => {
it("\(OK) Sign Transaction", async () => {
const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0)
// this transaction wes successfully submitted to the network https://testnet.explorer.perawallet.app/tx/UJG3NVCSCW5A63KPV35BPAABLXMXTTEM2CVUKNS4EML3H3EYGMCQ/
const tx = decodeUnsignedTransaction(Buffer.from("iaNhbXTNA+ijZmVlzQPoomZ2zgJHknejZ2VurHRlc3RuZXQtdjEuMKJnaMQgSGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiKibHbOAkeWX6NyY3bEIGL+gyt60QVEvoM3pnBDXlBkrkpm53vXiQl2W0a1dqbzo3NuZMQgYv6DK3rRBUS+gzemcENeUGSuSmbne9eJCXZbRrV2pvOkdHlwZaNwYXk=", "base64"))
const signed = await cryptoService.signAlgoTransaction(KeyContext.Address, 0, 0, tx)
const to_be_signed = tx.bytesToSign()
expect(encodeAddress(key)).toEqual("ML7IGK322ECUJPUDG6THAQ26KBSK4STG4555PCIJOZNUNNLWU3Z3ZFXITA")
expect(nacl.sign.detached.verify(to_be_signed, signed.sig!, key)).toBe(true)
})
})

describe("ECDH cases", () => {
Expand Down
122 changes: 36 additions & 86 deletions assets/arc-0052/contextual.api.crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ import {
ready,
crypto_sign_ed25519_pk_to_curve25519,
crypto_scalarmult,
crypto_sign_detached,
crypto_sign,
crypto_sign_SECRETKEYBYTES,
crypto_sign_ed25519_sk_to_pk,
crypto_scalarmult_ed25519_base,
crypto_generichash,
crypto_scalarmult_base
} from 'libsodium-wrappers-sumo';
import * as msgpack from "algo-msgpack-with-bigint"
import Ajv from "ajv"
import { deriveChildNodePrivate, fromSeed } from './bip32-ed25519';
import { Transaction, EncodedSignedTransaction } from 'algosdk';
import { randomBytes } from 'crypto';


/**
Expand All @@ -34,6 +40,7 @@ export interface ChannelKeys {
}

export enum Encoding {
CBOR = "cbor",
MSGPACK = "msgpack",
BASE64 = "base64",
NONE = "none"
Expand Down Expand Up @@ -116,48 +123,6 @@ export class ContextualCryptoApi {
return await this.deriveKey(rootKey, bip44Path, false)
}

/**
* Raw Signing function called by signData and signTransaction
*
* Ref: https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.6
*
* Edwards-Curve Digital Signature Algorithm (EdDSA)
*
* @param bip44Path
* - BIP44 path (m / purpose' / coin_type' / account' / change / address_index)
* @param data
* - data to be signed in raw bytes
*
* @returns
* - signature holding R and S, totally 64 bytes
*/
private async rawSign(bip44Path: number[], data: Uint8Array): Promise<Uint8Array> {
await ready // libsodium

const rootKey: Uint8Array = fromSeed(this.seed)
const raw: Uint8Array = await this.deriveKey(rootKey, bip44Path, true)

const scalar: Uint8Array = raw.slice(0, 32);
const c: Uint8Array = raw.slice(32, 64);

// \(1): pubKey = scalar * G (base point, no clamp)
const publicKey = crypto_scalarmult_ed25519_base_noclamp(scalar);

// \(2): h = hash(c || msg) mod q
const r = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([c, data])))

// \(4): R = r * G (base point, no clamp)
const R = crypto_scalarmult_ed25519_base_noclamp(r)

// h = hash(R || pubKey || msg) mod q
let h = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([R, publicKey, data])));

// \(5): S = (r + h * k) mod q
const S = crypto_core_ed25519_scalar_add(r, crypto_core_ed25519_scalar_mul(h, scalar))

return Buffer.concat([R, S]);
}

/**
* Ref: https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.6
*
Expand Down Expand Up @@ -185,44 +150,29 @@ export class ContextualCryptoApi {

await ready // libsodium

const rootKey: Uint8Array = fromSeed(this.seed)
const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex)
const raw: Uint8Array = await this.deriveKey(rootKey, bip44Path, true)

return await this.rawSign(bip44Path, data)
}

/**
* Sign Algorand transaction
* @param context
* - context of the key (i.e Address, Identity)
* @param account
* - account number. This value will be hardened as part of BIP44
* @param keyIndex
* - key index. This value will be a SOFT derivation as part of BIP44.
* @param tx
* - Transaction object containing parameters to be signed, e.g. sender, receiver, amount, fee,
*
* @returns stx
* - EncodedSignedTransaction object
*/
async signAlgoTransaction(context: KeyContext, account: number, keyIndex: number, tx: Transaction): Promise<EncodedSignedTransaction> {
await ready // libsodium
const scalar: Uint8Array = raw.slice(0, 32);
const c: Uint8Array = raw.slice(32, 64);

const prefixEncodedTx: Uint8Array = new Uint8Array(tx.bytesToSign())
const pk = await this.keyGen(context, account, keyIndex)
// \(1): pubKey = scalar * G (base point, no clamp)
const publicKey = crypto_scalarmult_ed25519_base_noclamp(scalar);

// \(2): h = hash(c || msg) mod q
const r = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([c, data])))

const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex)
// \(4): R = r * G (base point, no clamp)
const R = crypto_scalarmult_ed25519_base_noclamp(r)

const sig = await this.rawSign(bip44Path, prefixEncodedTx)
// h = hash(R || pubKey || msg) mod q
let h = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([R, publicKey, data])));

const stx: EncodedSignedTransaction = {
sig: Buffer.from(sig),
txn: tx.get_obj_for_encoding()
}
// \(5): S = (r + h * k) mod q
const S = crypto_core_ed25519_scalar_add(r, crypto_core_ed25519_scalar_mul(h, scalar))

if (pk !== tx.from.publicKey) {
stx.sgnr = Buffer.from(pk)
}
return stx
return Buffer.concat([R, S]);
}


Expand Down Expand Up @@ -256,6 +206,12 @@ export class ContextualCryptoApi {
default:
throw new Error("Invalid encoding")
}

// Check after decoding too
// Some one might try to encode a regular transaction with the protocol reserved prefixes
if (this.hasAlgorandTags(decoded)) {
return ERROR_TAGS_FOUND
}

// validate with schema
const ajv = new Ajv()
Expand All @@ -276,18 +232,12 @@ export class ContextualCryptoApi {
*/
private hasAlgorandTags(message: Uint8Array): boolean {

// Check that decoded doesn't include the following prefixes
// Prefixes taken from go-algorand node software code
// https://github.com/algorand/go-algorand/blob/master/protocol/hash.go
const prefixes: string[] = [
"appID","arc","aB","aD","aO","aP","aS","AS","B256","BH","BR","CR","GE","KP","MA","MB",
"MX","NIC","NIR","NIV","NPR","OT1","OT2","PF","PL","Program","ProgData","PS","PK","SD",
"SpecialAddr","STIB","spc","spm","spp","sps","spv","TE","TG","TL","TX","VO"
]
for (const prefix of prefixes) {
if (Buffer.from(message.subarray(0, prefix.length)).toString("ascii") === prefix) {
return true
}
// Check that decoded doesn't include the following prefixes: TX, MX, progData, Program
if (Buffer.from(message.subarray(0, 2)).toString("ascii") === "TX" ||
Buffer.from(message.subarray(0, 2)).toString("ascii") === "MX" ||
Buffer.from(message.subarray(0, 8)).toString("ascii") === "progData" ||
Buffer.from(message.subarray(0, 7)).toString("ascii") === "Program") {
return true
}

return false
Expand Down
2 changes: 0 additions & 2 deletions assets/arc-0052/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@
"@types/libsodium-wrappers-sumo": "^0.7.7",
"@types/node": "^20.7.1",
"ts-jest": "^29.1.1",
"tweetnacl": "^1.0.3",
"typescript": "^5.2.2"
},
"dependencies": {
"ajv": "^8.12.0",
"algo-msgpack-with-bigint": "^2.1.1",
"algosdk": "^2.7.0",
"bip32-ed25519": "^0.0.4",
"bip39": "^3.1.0",
"bn.js": "^5.2.1",
Expand Down
Loading

0 comments on commit 8b4de1d

Please sign in to comment.