Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/accounts/account.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,42 @@ describe("test account methods", function () {
"561bc58f1dc6b10de208b2d2c22c9a474ea5e8cabb59c3d3ce06bbda21cc46454aa71a85d5a60442bd7784effa2e062fcb8fb421c521f898abf7f5ec165e5d0f",
);
});

it("should verify message", async function () {
const message = new Message({
data: new Uint8Array(Buffer.from("hello")),
});

const account = Account.newFromMnemonic(DUMMY_MNEMONIC);
message.signature = await account.signMessage(message);
const isVerified = await account.verifyMessageSignature(message, message.signature);

assert.isTrue(isVerified);
});

it("should sign and verify transaction", async function () {
const transaction = new Transaction({
nonce: 89n,
value: 0n,
receiver: Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"),
sender: Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"),
gasPrice: 1000000000n,
gasLimit: 50000n,
data: new Uint8Array(),
chainID: "local-testnet",
version: 1,
options: 0,
});

const account = Account.newFromMnemonic(DUMMY_MNEMONIC);
transaction.signature = await account.signTransaction(transaction);

assert.equal(
Buffer.from(transaction.signature).toString("hex"),
"b56769014f2bdc5cf9fc4a05356807d71fcf8775c819b0f1b0964625b679c918ffa64862313bfef86f99b38cb84fcdb16fa33ad6eb565276616723405cd8f109",
);

const isVerified = await account.verifyTransactionSignature(transaction, transaction.signature);
assert.isTrue(isVerified);
});
});
14 changes: 13 additions & 1 deletion src/accounts/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export class Account implements IAccount {
return this.secretKey.sign(data);
}

verify(data: Uint8Array, signature: Uint8Array): boolean {
async verify(data: Uint8Array, signature: Uint8Array): Promise<boolean> {
return this.publicKey.verify(data, signature);
}

Expand All @@ -106,12 +106,24 @@ export class Account implements IAccount {
return this.secretKey.sign(serializedTransaction);
}

async verifyTransactionSignature(transaction: Transaction, signature: Uint8Array): Promise<boolean> {
const transactionComputer = new TransactionComputer();
const serializedTransaction = transactionComputer.computeBytesForVerifying(transaction);
return this.publicKey.verify(serializedTransaction, signature);
}

async signMessage(message: Message): Promise<Uint8Array> {
const messageComputer = new MessageComputer();
const serializedMessage = messageComputer.computeBytesForSigning(message);
return this.secretKey.sign(serializedMessage);
}

async verifyMessageSignature(message: Message, signature: Uint8Array): Promise<boolean> {
const messageComputer = new MessageComputer();
const serializedMessage = messageComputer.computeBytesForVerifying(message);
return this.publicKey.verify(serializedMessage, signature);
}

getNonceThenIncrement(): bigint {
let nonce = this.nonce;
this.nonce = this.nonce + 1n;
Expand Down
5 changes: 5 additions & 0 deletions src/accounts/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Message, Transaction } from "../core";
import { Address } from "../core/address";

export interface IAccount {
readonly address: Address;

sign(data: Uint8Array): Promise<Uint8Array>;
signTransaction(transaction: Transaction): Promise<Uint8Array>;
verifyTransactionSignature(transaction: Transaction, signature: Uint8Array): Promise<boolean>;
signMessage(message: Message): Promise<Uint8Array>;
verifyMessageSignature(message: Message, signature: Uint8Array): Promise<boolean>;
}
9 changes: 2 additions & 7 deletions src/core/message.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { assert } from "chai";
import { Account } from "../accounts";
import { getTestWalletsPath } from "../testutils/utils";
import { UserVerifier } from "../wallet";
import { DEFAULT_MESSAGE_VERSION, SDK_JS_SIGNER, UNKNOWN_SIGNER } from "./constants";
import { Message, MessageComputer } from "./message";

Expand Down Expand Up @@ -60,12 +59,8 @@ describe("test message", () => {
assert.deepEqual(unpackedMessage.version, message.version);
assert.deepEqual(unpackedMessage.signer, message.signer);

const verifier = UserVerifier.fromAddress(alice.address);
const isValid = verifier.verify(
Buffer.from(messageComputer.computeBytesForVerifying(unpackedMessage)),
Buffer.from(unpackedMessage.signature!),
);
assert.equal(isValid, true);
const isValid = await alice.verifyMessageSignature(unpackedMessage, Buffer.from(unpackedMessage.signature!));
assert.isTrue(isValid);
});

it("should unpack legacy message", async () => {
Expand Down
25 changes: 4 additions & 21 deletions src/core/transaction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { assert } from "chai";
import { Account } from "../accounts";
import { ProtoSerializer } from "../proto";
import { getTestWalletsPath } from "../testutils/utils";
import { UserPublicKey, UserVerifier } from "../wallet";
import { Address } from "./address";
import { MIN_TRANSACTION_VERSION_THAT_SUPPORTS_OPTIONS, TRANSACTION_OPTIONS_DEFAULT } from "./constants";
import { INetworkConfig } from "./interface";
Expand Down Expand Up @@ -692,17 +691,9 @@ describe("test transaction", async () => {

transaction.signature = await alice.signTransaction(transaction);

const userVerifier = new UserVerifier(new UserPublicKey(alice.address.getPublicKey()));
const isSignedByAlice = userVerifier.verify(
transactionComputer.computeBytesForVerifying(transaction),
transaction.signature,
);
const isSignedByAlice = await alice.verifyTransactionSignature(transaction, transaction.signature);

const wrongVerifier = new UserVerifier(new UserPublicKey(bob.address.getPublicKey()));
const isSignedByBob = wrongVerifier.verify(
transactionComputer.computeBytesForVerifying(transaction),
transaction.signature,
);
const isSignedByBob = await bob.verifyTransactionSignature(transaction, transaction.signature);

assert.equal(isSignedByAlice, true);
assert.equal(isSignedByBob, false);
Expand All @@ -721,17 +712,9 @@ describe("test transaction", async () => {

transaction.signature = await alice.sign(transactionComputer.computeHashForSigning(transaction));

const userVerifier = new UserVerifier(alice.publicKey);
const isSignedByAlice = userVerifier.verify(
transactionComputer.computeBytesForVerifying(transaction),
transaction.signature,
);
const isSignedByAlice = await alice.verifyTransactionSignature(transaction, transaction.signature);

const wrongVerifier = new UserVerifier(new UserPublicKey(bob.address.getPublicKey()));
const isSignedByBob = wrongVerifier.verify(
transactionComputer.computeBytesForVerifying(transaction),
transaction.signature,
);
const isSignedByBob = await bob.verifyTransactionSignature(transaction, transaction.signature);
assert.equal(isSignedByAlice, true);
assert.equal(isSignedByBob, false);
});
Expand Down
20 changes: 7 additions & 13 deletions src/entrypoints/entrypoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import { Account } from "../accounts";
import { IAccount } from "../accounts/interfaces";
import { Address } from "../core/address";
import { ErrInvalidNetworkProviderKind } from "../core/errors";
import { Message, MessageComputer } from "../core/message";
import { Message } from "../core/message";
import { Transaction } from "../core/transaction";
import { TransactionComputer } from "../core/transactionComputer";
import { TransactionOnNetwork } from "../core/transactionOnNetwork";
import { TransactionsFactoryConfig } from "../core/transactionsFactoryConfig";
import { TransactionWatcher } from "../core/transactionWatcher";
Expand All @@ -20,7 +19,7 @@ import { SmartContractController } from "../smartContracts/smartContractControll
import { TokenManagementController, TokenManagementTransactionsFactory } from "../tokenManagement";
import { TransferTransactionsFactory } from "../transfers";
import { TransfersController } from "../transfers/transfersControllers";
import { UserSecretKey, UserVerifier } from "../wallet";
import { UserSecretKey } from "../wallet";
import { DevnetEntrypointConfig, MainnetEntrypointConfig, TestnetEntrypointConfig } from "./config";

class NetworkEntrypoint {
Expand Down Expand Up @@ -49,17 +48,14 @@ class NetworkEntrypoint {
}

async signTransaction(transaction: Transaction, account: IAccount): Promise<void> {
const txComputer = new TransactionComputer();
transaction.signature = await account.sign(txComputer.computeBytesForSigning(transaction));
transaction.signature = await account.signTransaction(transaction);
}

verifyTransactionSignature(transaction: Transaction): boolean {
const verifier = UserVerifier.fromAddress(transaction.sender);
const txComputer = new TransactionComputer();
return verifier.verify(txComputer.computeBytesForVerifying(transaction), transaction.signature);
async verifyTransactionSignature(transaction: Transaction, account: IAccount): Promise<boolean> {
return await account.verifyTransactionSignature(transaction, transaction.signature);
}

verifyMessageSignature(message: Message): boolean {
async verifyMessageSignature(message: Message, account: IAccount): Promise<boolean> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In account, we have verifyTransaction and verifyMessage. Here, we also have the suffix signature. Keep it or drop it? (either way should be fine, but we must also reflect this in the specs).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of like having signature in the name, but if we decide to drop it, it's fine

if (!message.address) {
throw new Error("`address` property of Message is not set");
}
Expand All @@ -68,9 +64,7 @@ class NetworkEntrypoint {
throw new Error("`signature` property of Message is not set");
}

const verifier = UserVerifier.fromAddress(message.address);
const messageComputer = new MessageComputer();
return verifier.verify(messageComputer.computeBytesForVerifying(message), message.signature);
return await account.verifyMessageSignature(message, message.signature);
}

async recallAccountNonce(address: Address): Promise<bigint> {
Expand Down
23 changes: 12 additions & 11 deletions src/wallet/crypto/pubkeyDecryptor.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import crypto from "crypto";
import nacl from "tweetnacl";
import ed2curve from "ed2curve";
import { X25519EncryptedData } from "./x25519EncryptedData";
import nacl from "tweetnacl";
import { UserPublicKey, UserSecretKey } from "../userKeys";
import { X25519EncryptedData } from "./x25519EncryptedData";

export class PubkeyDecryptor {
static decrypt(data: X25519EncryptedData, decryptorSecretKey: UserSecretKey): Buffer {
const ciphertext = Buffer.from(data.ciphertext, 'hex');
const edhPubKey = Buffer.from(data.identities.ephemeralPubKey, 'hex');
const originatorPubKeyBuffer = Buffer.from(data.identities.originatorPubKey, 'hex');
static async decrypt(data: X25519EncryptedData, decryptorSecretKey: UserSecretKey): Promise<Buffer> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor breaking change. Let's mention it in the PR description, as well, so that we don't forget to document it in the release notes.

const ciphertext = Buffer.from(data.ciphertext, "hex");
const edhPubKey = Buffer.from(data.identities.ephemeralPubKey, "hex");
const originatorPubKeyBuffer = Buffer.from(data.identities.originatorPubKey, "hex");
const originatorPubKey = new UserPublicKey(originatorPubKeyBuffer);

const authMessage = crypto.createHash('sha256').update(
Buffer.concat([ciphertext, edhPubKey])
).digest();
const authMessage = crypto
.createHash("sha256")
.update(Buffer.concat([ciphertext, edhPubKey]))
.digest();

if (!originatorPubKey.verify(authMessage, Buffer.from(data.mac, 'hex'))) {
if (!(await originatorPubKey.verify(authMessage, Buffer.from(data.mac, "hex")))) {
throw new Error("Invalid authentication for encrypted message originator");
}

const nonce = Buffer.from(data.nonce, 'hex');
const nonce = Buffer.from(data.nonce, "hex");
const x25519Secret = ed2curve.convertSecretKey(decryptorSecretKey.valueOf());
const x25519EdhPubKey = ed2curve.convertPublicKey(edhPubKey);
if (x25519EdhPubKey === null) {
Expand Down
43 changes: 29 additions & 14 deletions src/wallet/crypto/pubkeyEncrypt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,39 @@ describe("test address", () => {
);
});

it("encrypts/decrypts", () => {
const decryptedData = PubkeyDecryptor.decrypt(encryptedDataOfAliceForBob, new UserSecretKey(bob.secretKey));
it("encrypts/decrypts", async function () {
const decryptedData = await PubkeyDecryptor.decrypt(
encryptedDataOfAliceForBob,
new UserSecretKey(bob.secretKey),
);
assert.equal(sensitiveData.toString("hex"), decryptedData.toString("hex"));
});

it("fails for different originator", () => {
encryptedDataOfAliceForBob.identities.originatorPubKey = carol.address.hex();
assert.throws(
() => PubkeyDecryptor.decrypt(encryptedDataOfAliceForBob, new UserSecretKey(bob.secretKey)),
"Invalid authentication for encrypted message originator",
);
it("fails for different originator", async function () {
encryptedDataOfAliceForBob.identities.originatorPubKey = carol.address.toHex();

try {
await PubkeyDecryptor.decrypt(encryptedDataOfAliceForBob, new UserSecretKey(bob.secretKey));
assert.fail("Invalid authentication for encrypted message originator");
} catch (error) {
assert(
error.message.includes("Invalid authentication for encrypted message originator"),
`Unexpected error message: ${error.message}`,
);
}
});

it("fails for different DH public key", () => {
encryptedDataOfAliceForBob.identities.ephemeralPubKey = carol.address.hex();
assert.throws(
() => PubkeyDecryptor.decrypt(encryptedDataOfAliceForBob, new UserSecretKey(bob.secretKey)),
"Invalid authentication for encrypted message originator",
);
it("fails for different DH public key", async function () {
encryptedDataOfAliceForBob.identities.ephemeralPubKey = carol.address.toHex();

try {
await PubkeyDecryptor.decrypt(encryptedDataOfAliceForBob, new UserSecretKey(bob.secretKey));
assert.fail("Expected an error but none was thrown");
} catch (error) {
assert(
error.message.includes("Invalid authentication for encrypted message originator"),
`Unexpected error message: ${error.message}`,
);
}
});
});
2 changes: 1 addition & 1 deletion src/wallet/userKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class UserPublicKey {
this.buffer = Buffer.from(buffer);
}

verify(data: Buffer | Uint8Array, signature: Buffer | Uint8Array): boolean {
async verify(data: Buffer | Uint8Array, signature: Buffer | Uint8Array): Promise<boolean> {
try {
const ok = ed.sync.verify(new Uint8Array(signature), new Uint8Array(data), new Uint8Array(this.buffer));
return ok;
Expand Down
2 changes: 1 addition & 1 deletion src/wallet/userVerifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class UserVerifier {
* @param signature the signature to be verified
* @returns true if the signature is valid, false otherwise
*/
verify(data: Buffer | Uint8Array, signature: Buffer | Uint8Array): boolean {
async verify(data: Buffer | Uint8Array, signature: Buffer | Uint8Array): Promise<boolean> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor breaking change, we should document (not forget about) it.

return this.publicKey.verify(data, signature);
}
}
17 changes: 9 additions & 8 deletions src/wallet/users.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { assert } from "chai";
import path from "path";
import { Account } from "../accounts";
import { Address, ErrBadMnemonicEntropy, ErrInvariantFailed, Message, Transaction } from "../core";
import {
DummyMnemonicOf12Words,
Expand Down Expand Up @@ -290,7 +291,7 @@ describe("test user wallets", () => {
});

it("should sign transactions", async () => {
let signer = new UserSigner(
let signer = new Account(
UserSecretKey.fromString("1a927e2af5306a9bb2ea777f73e06ecc0ac9aaa72fb4ea3fecf659451394cccf"),
);
let verifier = new UserVerifier(
Expand Down Expand Up @@ -323,7 +324,7 @@ describe("test user wallets", () => {
Buffer.from(signature).toString("hex"),
"a5db62c6186612d44094f83576aa6a664299315fb6e42d0c17a40e9cd33efa9a9df8b76943aeac7dceaff3d78a16a7414c914f03f7a88e786c2cf939eb111c06",
);
assert.isTrue(verifier.verify(serialized, signature));
assert.isTrue(await verifier.verify(serialized, signature));

// Without data field
transaction = new Transaction({
Expand Down Expand Up @@ -393,7 +394,7 @@ describe("test user wallets", () => {
Buffer.from(guardianSignature).toString("hex"),
"5695fde5d9c77a94bb320438fbebe3bbd60b7cc4d633fb38e42bb65f83d253cbb82cc5ae40d701a7f0b839a5231320ca356018ced949885baae473e469ec770e",
);
assert.isTrue(verifier.verify(serialized, signature));
assert.isTrue(await verifier.verify(serialized, signature));

// Without data field
transaction = new Transaction({
Expand Down Expand Up @@ -425,7 +426,7 @@ describe("test user wallets", () => {
Buffer.from(guardianSignature).toString("hex"),
"ea3b83adcc468b0c7d3613fca5f429a9764d5710137c34c27e15d06e625326724ccfa758968507acadb14345d19389ba6004a4f0a6c527799c01713e10cf650b",
);
assert.isTrue(verifier.verify(serialized, signature));
assert.isTrue(await verifier.verify(serialized, signature));
});

it("should sign transactions using PEM files", async () => {
Expand Down Expand Up @@ -469,10 +470,10 @@ describe("test user wallets", () => {
const signature = await signer.sign(message.data);

assert.deepEqual(await signer.sign(message.data), await signer.sign(Uint8Array.from(message.data)));
assert.isTrue(verifier.verify(message.data, signature));
assert.isTrue(verifier.verify(Uint8Array.from(message.data), Uint8Array.from(signature)));
assert.isFalse(verifier.verify(Buffer.from("hello"), signature));
assert.isFalse(verifier.verify(new TextEncoder().encode("hello"), signature));
assert.isTrue(await verifier.verify(message.data, signature));
assert.isTrue(await verifier.verify(Uint8Array.from(message.data), Uint8Array.from(signature)));
assert.isFalse(await verifier.verify(Buffer.from("hello"), signature));
assert.isFalse(await verifier.verify(new TextEncoder().encode("hello"), signature));
});

it("should create UserSigner from wallet", async function () {
Expand Down
4 changes: 2 additions & 2 deletions src/wallet/usersBenchmark.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe("behchmark sign and verify", () => {
console.time("verify (good)");

for (let i = 0; i < n; i++) {
const ok = publicKeys[i].verify(messages[i], goodSignatures[i]);
const ok = await publicKeys[i].verify(messages[i], goodSignatures[i]);
assert.isTrue(ok);
}

Expand All @@ -45,7 +45,7 @@ describe("behchmark sign and verify", () => {
console.time("verify (bad)");

for (let i = 0; i < n; i++) {
const ok = publicKeys[i].verify(messages[messages.length - i - 1], goodSignatures[i]);
const ok = await publicKeys[i].verify(messages[messages.length - i - 1], goodSignatures[i]);
assert.isFalse(ok);
}

Expand Down
Loading