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
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Ignore artifacts:
dist
1 change: 1 addition & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
10 changes: 10 additions & 0 deletions backup/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { sign } from "./lib/sign";
import { verify } from "./lib/verify";

const Web3Token = {
sign,
verify,
};

export default Web3Token;
export { sign, verify };
58 changes: 58 additions & 0 deletions backup/sign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Base64 from "base-64";
import { timeSpan } from "./timespan.js";

export type COSESign1 = {
signature: string;
key: string;
}

export const sign = async (signer: (msg: string) => Promise<COSESign1>, expires_in: string = "1d", body: any = {}) => {
const expires_in_date = timeSpan(expires_in);

validateInput(body);

const data = {
"Web3-Token-Version": 1,
"Expire-Date": expires_in_date,
...body,
};

const msg = buildMessage(data);

let COSESign1Message = await signer(msg);

const token = Base64.encode(
JSON.stringify({
...COSESign1Message,
body: msg,
})
);

return token;
};

const validateInput = (body: any) => {
for (const key in body) {
const field = body[key];

if (key === "Expire-Date") {
throw new Error('Please do not rewrite "Expire-Date" field');
}

if (key === "Web3-Token-Version") {
throw new Error('Please do not rewrite "Web3-Token-Version" field');
}

if (typeof field !== "string") {
throw new Error("Body can only contain string values");
}
}
};

const buildMessage = (data: any) => {
const message = [];
for (const key in data) {
message.push(`${key}: ${data[key]}`);
}
return message.join("\n");
};
149 changes: 149 additions & 0 deletions backup/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import Base64 from "base-64";
import parseAsHeaders from "parse-headers";
import { Buffer } from "buffer";
import Loader from "./loader.js";

import type {
Address,
PublicKey,
} from "@emurgo/cardano-serialization-lib-browser";
import { COSESign1 } from "./sign.js";

type DataType = {
body: string;
} & COSESign1;

/**
*
* @param {string} token Signed Web3 Token
* @returns {boolean}
*/
export const verify = async (token: string) => {
if (!token || !token.length) {
throw new Error("Token required.");
}

try {
var base64_decoded = Base64.decode(token);
} catch (error) {
throw new Error("Token malformed (must be base64 encoded)");
}

if (!base64_decoded || !base64_decoded.length) {
throw new Error("Token malformed (must be base64 encoded)");
}

let msg: DataType;
try {
msg = JSON.parse(base64_decoded);
} catch (error) {
throw new Error("Token malformed (unparsable JSON)");
}

const { signature: signedRaw, key } = msg;

if (!signedRaw || !signedRaw.length) {
throw new Error("Token malformed (empty signature)");
}

await Loader.load();

const message = Loader.Message.COSESign1.from_bytes(
Buffer.from(signedRaw, "hex")
);
const headers = message.headers().protected().deserialized_headers();

const address = Loader.Cardano.Address.from_bytes(
headers.header(Loader.Message.Label.new_text("address")).as_bytes()
);

const coseKey = Loader.Message.COSEKey.from_bytes(Buffer.from(key, "hex"));

const publicKey = Loader.Cardano.PublicKey.from_bytes(
coseKey
.header(
Loader.Message.Label.new_int(
Loader.Message.Int.new_negative(Loader.Message.BigNum.from_str("2"))
)
)
.as_bytes()
);

// const algorithmId = headers.algorithm_id().as_int().as_i32();
const signature = Loader.Cardano.Ed25519Signature.from_bytes(
message.signature()
);

const data = message.signed_data().to_bytes();

const body = Buffer.from(data).toString("utf-8");

// Ensure that the Public Key matches up to the Address in the Signed data.
const verifyAddressResponse = verifyAddress(address, publicKey);

if (!verifyAddressResponse.verified) {
throw new Error(
`Address verification failed: (${verifyAddressResponse.message} (${verifyAddressResponse.code}))`
);
}

if (!publicKey.verify(data, signature)) {
throw new Error(
`Message integrity check failed (has the message been tampered with?)`
);
}

const parsed_body = parseAsHeaders(body);

if (
parsed_body["expire-date"] &&
new Date(parsed_body["expire-date"] as string) < new Date()
) {
throw new Error("Token expired");
}

return {
address: address.to_bech32(),
network: address.network_id(),
body: parsed_body,
};
};

function verifyAddress(checkAddress: Address, publicKey: PublicKey) {
console.log("In Verify Address");

const baseAddress = Loader.Cardano.BaseAddress.from_address(checkAddress);

try {
//reconstruct address
const paymentKeyHash = publicKey.hash();
const stakeKeyHash = baseAddress.stake_cred().to_keyhash();
const reconstructedAddress = Loader.Cardano.BaseAddress.new(
checkAddress.network_id(),
Loader.Cardano.StakeCredential.from_keyhash(paymentKeyHash),
Loader.Cardano.StakeCredential.from_keyhash(stakeKeyHash)
);

if (
checkAddress.to_bech32() !== reconstructedAddress.to_address().to_bech32()
) {
return {
verified: false,
code: 1,
message:
"Check address does not match Reconstructed Address (Public Key is not the correct key for this Address)",
};
}

return {
verified: true,
};

} catch (e) {
return {
verified: false,
code: 3,
message: e.message,
};
}
}
2 changes: 1 addition & 1 deletion dist/browser.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/node.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions src/browser.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
type COSESign1 = {
signature: string;
key: string;
}

type Signer = (msg: string) => PromiseLike<COSESign1>;

export function sign(
signer: Signer,
expires_in?: string | number,
body?: Object
): PromiseLike<string>;

export function verify(token: string): {
address: string;
body: Object;
};

declare const Web3Token: {
sign: typeof sign;
verify: typeof verify;
};

export default Web3Token;

20 changes: 6 additions & 14 deletions src/lib.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
type Signer = (msg: string) => PromiseLike<string>;
type COSESign1 = {
signature: string;
key: string;
}

type Signer = (msg: string) => PromiseLike<COSESign1>;

export function sign(
signer: Signer,
Expand All @@ -17,16 +22,3 @@ declare const Web3Token: {
};

export default Web3Token;

declare module "web3-cardano-token/dist/browser" {
export const Web3Token: {
sign: typeof sign;
verify: typeof verify;
};
}
declare module "web3-cardano-token/dist/node" {
export const Web3Token: {
sign: typeof sign;
verify: typeof verify;
};
}
5 changes: 4 additions & 1 deletion src/lib/sign.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const sign = async (signer, expires_in = '1d', body = {}) => {
const msg = buildMessage(data);

if(typeof signer === 'function') {
var signature = await signer(msg);
var COSESign1Message = await signer(msg);
} else {
throw new Error('"signer" argument should be a function that returns a signature eg: "msg => web3.eth.personal.sign(msg, <YOUR_ADDRESS>)"')
}
Expand All @@ -30,12 +30,15 @@ export const sign = async (signer, expires_in = '1d', body = {}) => {
signature = signature.signature
}

const {signature, key} = COSESign1Message;

if(typeof signature !== 'string') {
throw new Error('"signer" argument should be a function that returns a signature string (Promise<string>)')
}

const token = Base64.encode(JSON.stringify({
signature,
key,
body: msg,
}))

Expand Down
36 changes: 30 additions & 6 deletions src/lib/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const verify = async (token) => {
}

try {
var { body, signature } = JSON.parse(base64_decoded);
var { body, signature, key } = JSON.parse(base64_decoded);
} catch (error) {
throw new Error("Token malformed (unparsable JSON)");
}
Expand All @@ -48,14 +48,38 @@ export const verify = async (token) => {
headermap.header(Loader.Message.Label.new_text("address")).as_bytes()
);

const publicKey = Loader.Cardano.PublicKey.from_bytes(headermap.key_id());
const coseKey = Loader.Message.COSEKey.from_bytes(Buffer.from(key, "hex"));

const publicKey = Loader.Cardano.PublicKey.from_bytes(
coseKey
.header(
Loader.Message.Label.new_int(
Loader.Message.Int.new_negative(Loader.Message.BigNum.from_str("2"))
)
)
.as_bytes()
);

const verifyAddressResponse = verifyAddress(address, publicKey);
if (!verifyAddressResponse.status) {
throw new Error(verifyAddressResponse.msg);

if (!verifyAddressResponse.verified) {
throw new Error(
`Address verification failed: (${verifyAddressResponse.message} (${verifyAddressResponse.code}))`
);
}

const data = message.signed_data().to_bytes();
const body_from_token = Buffer.from(data).toString("utf-8");

const ed25519Sig = Loader.Cardano.Ed25519Signature.from_bytes(message.signature())

if (!publicKey.verify(data, ed25519Sig)) {
throw new Error(
`Message integrity check failed (has the message been tampered with?)`
);
}

const parsed_body = parseAsHeaders(body);
const parsed_body = parseAsHeaders(body_from_token);

if (
parsed_body["expire-date"] &&
Expand All @@ -72,6 +96,7 @@ export const verify = async (token) => {
};

/**

* Validate the Address provided. To do this we take the Address (or Base Address)
* and compare it to an address (BaseAddress or RewardAddress) reconstructed from the
* publicKey.
Expand All @@ -82,7 +107,6 @@ export const verify = async (token) => {
const verifyAddress = (checkAddress, publicKey) => {
let errorMsg = "";
try {
const baseAddress = Loader.Cardano.BaseAddress.from_address(checkAddress);
//reconstruct address
const paymentKeyHash = publicKey.hash();
const stakeKeyHash = baseAddress.stake_cred().to_keyhash();
Expand Down
Loading