Fixed bug in channel hash calculation and decryption
This commit is contained in:
@@ -9,7 +9,7 @@ import { NodeHash } from "./identity.types";
|
||||
const PUBLIC_KEY_SIZE = 32;
|
||||
const SEED_SIZE = 32;
|
||||
const HMAC_SIZE = 2;
|
||||
const SHARED_SECRET_SIZE = 32;
|
||||
const SHARED_SECRET_SIZE = 16;
|
||||
const SIGNATURE_SIZE = 64;
|
||||
const STATIC_SECRET_SIZE = 32;
|
||||
|
||||
@@ -136,28 +136,32 @@ export class SharedSecret implements ISharedSecret {
|
||||
private secret: Uint8Array;
|
||||
|
||||
constructor(secret: Uint8Array) {
|
||||
/*
|
||||
if (secret.length === SHARED_SECRET_SIZE / 2) {
|
||||
// Zero pad to the left if the secret is too short (e.g. from x25519)
|
||||
const padded = new Uint8Array(SHARED_SECRET_SIZE);
|
||||
padded.set(secret, SHARED_SECRET_SIZE - secret.length);
|
||||
padded.set(secret, 0);
|
||||
secret = padded;
|
||||
}
|
||||
*/
|
||||
if (secret.length !== SHARED_SECRET_SIZE) {
|
||||
throw new Error(`Invalid shared secret length: expected ${SHARED_SECRET_SIZE} bytes, got ${secret.length}`);
|
||||
}
|
||||
this.secret = secret;
|
||||
this.secret = new Uint8Array(SHARED_SECRET_SIZE * 2); // Pad to 32 bytes for hashing and encryption
|
||||
this.secret.set(secret, 0);
|
||||
}
|
||||
|
||||
public toHash(): NodeHash {
|
||||
return this.secret[0] as NodeHash;
|
||||
const hash = sha256.create().update(this.secret.slice(0, 16)).digest();
|
||||
return hash[0] as NodeHash;
|
||||
}
|
||||
|
||||
public toBytes(): Uint8Array {
|
||||
return this.secret;
|
||||
return this.secret.slice(0, 16);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return bytesToHex(this.secret);
|
||||
return bytesToHex(this.secret.slice(0, 16));
|
||||
}
|
||||
|
||||
public decrypt(hmac: Uint8Array, ciphertext: Uint8Array): Uint8Array {
|
||||
@@ -242,6 +246,6 @@ export class StaticSecret implements IStaticSecret {
|
||||
|
||||
public diffieHellman(otherPublicKey: IPublicKey): SharedSecret {
|
||||
const sharedSecret = x25519.getSharedSecret(this.secret, otherPublicKey.toBytes());
|
||||
return new SharedSecret(sharedSecret);
|
||||
return new SharedSecret(sharedSecret.slice(0, 16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ export class LocalIdentity extends Identity implements ILocalIdentity {
|
||||
} else {
|
||||
throw new Error("Invalid type for calculateSharedSecret comparison");
|
||||
}
|
||||
return new SharedSecret(this.privateKey.calculateSharedSecret(otherPublicKey));
|
||||
return new SharedSecret(this.privateKey.calculateSharedSecret(otherPublicKey).slice(0, 16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +216,12 @@ export class Contacts {
|
||||
private contacts: Record<number, Contact[]> = {};
|
||||
private groups: Record<number, Group[]> = {};
|
||||
|
||||
constructor() {
|
||||
// These groups are omnipresent:
|
||||
this.addGroup(new Group("Public"));
|
||||
this.addGroup(new Group("#test"));
|
||||
}
|
||||
|
||||
public addLocalIdentity(identity: LocalIdentity) {
|
||||
this.localIdentities.push({ identity, sharedSecrets: {} });
|
||||
}
|
||||
@@ -296,7 +302,7 @@ export class Contacts {
|
||||
}
|
||||
|
||||
public addGroup(group: Group) {
|
||||
const hash = parseNodeHash(group.hash()) as number;
|
||||
const hash = group.hash() as number;
|
||||
if (!this.groups[hash]) {
|
||||
this.groups[hash] = [];
|
||||
}
|
||||
|
||||
156
src/packet.ts
156
src/packet.ts
@@ -127,13 +127,13 @@ export class Packet implements IPacket {
|
||||
/* Header segment */
|
||||
{
|
||||
name: "header",
|
||||
data: new Uint8Array([this.header]),
|
||||
data: new Uint8Array([this.header]).buffer,
|
||||
fields: [
|
||||
/* Header flags */
|
||||
{
|
||||
name: "flags",
|
||||
type: FieldType.BITS,
|
||||
size: 1,
|
||||
length: 1,
|
||||
bits: [
|
||||
{ name: "payload version", size: 2 },
|
||||
{ name: "payload type", size: 4 },
|
||||
@@ -153,18 +153,18 @@ export class Packet implements IPacket {
|
||||
this.transport![0] & 0xff,
|
||||
(this.transport![1] >> 8) & 0xff,
|
||||
this.transport![1] & 0xff
|
||||
]),
|
||||
]).buffer,
|
||||
fields: [
|
||||
{
|
||||
name: "transport code 1",
|
||||
type: FieldType.UINT16_BE,
|
||||
size: 2,
|
||||
length: 2,
|
||||
value: this.transport![0]
|
||||
},
|
||||
{
|
||||
name: "transport code 2",
|
||||
type: FieldType.UINT16_BE,
|
||||
size: 2,
|
||||
length: 2,
|
||||
value: this.transport![1]
|
||||
}
|
||||
]
|
||||
@@ -175,12 +175,12 @@ export class Packet implements IPacket {
|
||||
/* Path length and hashes */
|
||||
{
|
||||
name: "path",
|
||||
data: new Uint8Array([this.pathLength, ...this.path]),
|
||||
data: new Uint8Array([this.pathLength, ...this.path]).buffer,
|
||||
fields: [
|
||||
{
|
||||
name: "path length",
|
||||
type: FieldType.UINT8,
|
||||
size: 1,
|
||||
length: 1,
|
||||
bits: [
|
||||
{ name: "path hash size", size: 2 },
|
||||
{ name: "path hash count", size: 6 }
|
||||
@@ -189,7 +189,7 @@ export class Packet implements IPacket {
|
||||
{
|
||||
name: "path hashes",
|
||||
type: pathHashType,
|
||||
size: this.path.length
|
||||
length: this.path.length
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -269,14 +269,19 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "request payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
fields: [
|
||||
{ name: "destination hash", type: FieldType.UINT8, size: 1, value: dst },
|
||||
{ name: "source hash", type: FieldType.UINT8, size: 1, value: src },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, size: 2, value: encrypted.cipherMAC },
|
||||
{ name: "cipher text", type: FieldType.BYTES, size: encrypted.cipherText.length, value: encrypted.cipherText }
|
||||
{ name: "destination hash", type: FieldType.UINT8, length: 1, value: dst },
|
||||
{ name: "source hash", type: FieldType.UINT8, length: 1, value: src },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, length: 2, value: encrypted.cipherMAC },
|
||||
{
|
||||
name: "cipher text",
|
||||
type: FieldType.BYTES,
|
||||
length: encrypted.cipherText.length,
|
||||
value: encrypted.cipherText
|
||||
}
|
||||
]
|
||||
};
|
||||
return { payload, segment };
|
||||
@@ -301,14 +306,19 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "response payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
fields: [
|
||||
{ name: "destination hash", type: FieldType.UINT8, size: 1, value: dst },
|
||||
{ name: "source hash", type: FieldType.UINT8, size: 1, value: src },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, size: 2, value: encrypted.cipherMAC },
|
||||
{ name: "cipher text", type: FieldType.BYTES, size: encrypted.cipherText.length, value: encrypted.cipherText }
|
||||
{ name: "destination hash", type: FieldType.UINT8, length: 1, value: dst },
|
||||
{ name: "source hash", type: FieldType.UINT8, length: 1, value: src },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, length: 2, value: encrypted.cipherMAC },
|
||||
{
|
||||
name: "cipher text",
|
||||
type: FieldType.BYTES,
|
||||
length: encrypted.cipherText.length,
|
||||
value: encrypted.cipherText
|
||||
}
|
||||
]
|
||||
};
|
||||
return { payload, segment };
|
||||
@@ -333,14 +343,19 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "text payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
fields: [
|
||||
{ name: "destination hash", type: FieldType.UINT8, size: 1, value: dst },
|
||||
{ name: "source hash", type: FieldType.UINT8, size: 1, value: src },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, size: 2, value: encrypted.cipherMAC },
|
||||
{ name: "cipher text", type: FieldType.BYTES, size: encrypted.cipherText.length, value: encrypted.cipherText }
|
||||
{ name: "destination hash", type: FieldType.UINT8, length: 1, value: dst },
|
||||
{ name: "source hash", type: FieldType.UINT8, length: 1, value: src },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, length: 2, value: encrypted.cipherMAC },
|
||||
{
|
||||
name: "cipher text",
|
||||
type: FieldType.BYTES,
|
||||
length: encrypted.cipherText.length,
|
||||
value: encrypted.cipherText
|
||||
}
|
||||
]
|
||||
};
|
||||
return { payload, segment };
|
||||
@@ -361,10 +376,10 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "ack payload",
|
||||
data: this.payload,
|
||||
fields: [{ name: "checksum", type: FieldType.BYTES, size: 4, value: checksum }]
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
fields: [{ name: "checksum", type: FieldType.BYTES, length: 4, value: checksum }]
|
||||
};
|
||||
return { payload, segment };
|
||||
}
|
||||
@@ -388,11 +403,11 @@ export class Packet implements IPacket {
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
segment = {
|
||||
name: "advert payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
fields: [
|
||||
{ type: FieldType.BYTES, name: "public key", size: 32 },
|
||||
{ type: FieldType.UINT32_LE, name: "timestamp", size: 4, value: payload.timestamp! },
|
||||
{ type: FieldType.BYTES, name: "signature", size: 64 }
|
||||
{ type: FieldType.BYTES, name: "public key", length: 32 },
|
||||
{ type: FieldType.UINT32_LE, name: "timestamp", length: 4, value: payload.timestamp! },
|
||||
{ type: FieldType.BYTES, name: "signature", length: 64 }
|
||||
]
|
||||
};
|
||||
}
|
||||
@@ -409,7 +424,7 @@ export class Packet implements IPacket {
|
||||
segment!.fields.push({
|
||||
type: FieldType.BITS,
|
||||
name: "flags",
|
||||
size: 1,
|
||||
length: 1,
|
||||
value: flags,
|
||||
bits: [
|
||||
{ size: 1, name: "name flag" },
|
||||
@@ -426,20 +441,20 @@ export class Packet implements IPacket {
|
||||
const lon = reader.int32() / 1000000;
|
||||
appdata.location = [lat, lon];
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
segment!.fields.push({ type: FieldType.UINT32_LE, name: "latitude", size: 4, value: lat });
|
||||
segment!.fields.push({ type: FieldType.UINT32_LE, name: "longitude", size: 4, value: lon });
|
||||
segment!.fields.push({ type: FieldType.UINT32_LE, name: "latitude", length: 4, value: lat });
|
||||
segment!.fields.push({ type: FieldType.UINT32_LE, name: "longitude", length: 4, value: lon });
|
||||
}
|
||||
}
|
||||
if (appdata.hasFeature1) {
|
||||
appdata.feature1 = reader.uint16();
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
segment!.fields.push({ type: FieldType.UINT16_LE, name: "feature1", size: 2, value: appdata.feature1 });
|
||||
segment!.fields.push({ type: FieldType.UINT16_LE, name: "feature1", length: 2, value: appdata.feature1 });
|
||||
}
|
||||
}
|
||||
if (appdata.hasFeature2) {
|
||||
appdata.feature2 = reader.uint16();
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
segment!.fields.push({ type: FieldType.UINT16_LE, name: "feature2", size: 2, value: appdata.feature2 });
|
||||
segment!.fields.push({ type: FieldType.UINT16_LE, name: "feature2", length: 2, value: appdata.feature2 });
|
||||
}
|
||||
}
|
||||
if (appdata.hasName) {
|
||||
@@ -448,7 +463,7 @@ export class Packet implements IPacket {
|
||||
segment!.fields.push({
|
||||
type: FieldType.C_STRING,
|
||||
name: "name",
|
||||
size: appdata.name.length,
|
||||
length: appdata.name.length,
|
||||
value: appdata.name
|
||||
});
|
||||
}
|
||||
@@ -476,13 +491,18 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "group text payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
fields: [
|
||||
{ name: "channel hash", type: FieldType.UINT8, size: 1, value: channelHash },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, size: 2, value: encrypted.cipherMAC },
|
||||
{ name: "cipher text", type: FieldType.BYTES, size: encrypted.cipherText.length, value: encrypted.cipherText }
|
||||
{ name: "channel hash", type: FieldType.UINT8, length: 1, value: channelHash },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, length: 2, value: encrypted.cipherMAC },
|
||||
{
|
||||
name: "cipher text",
|
||||
type: FieldType.BYTES,
|
||||
length: encrypted.cipherText.length,
|
||||
value: encrypted.cipherText
|
||||
}
|
||||
]
|
||||
};
|
||||
return { payload, segment };
|
||||
@@ -503,16 +523,16 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "group data payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
fields: [
|
||||
{ name: "channel hash", type: FieldType.UINT8, size: 1, value: payload.channelHash },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, size: 2, value: payload.encrypted.cipherMAC },
|
||||
{ name: "channel hash", type: FieldType.UINT8, length: 1, value: payload.channelHash },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, length: 2, value: payload.encrypted.cipherMAC },
|
||||
{
|
||||
name: "cipher text",
|
||||
type: FieldType.BYTES,
|
||||
size: payload.encrypted.cipherText.length,
|
||||
length: payload.encrypted.cipherText.length,
|
||||
value: payload.encrypted.cipherText
|
||||
}
|
||||
]
|
||||
@@ -536,17 +556,17 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "anon req payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
fields: [
|
||||
{ name: "destination hash", type: FieldType.UINT8, size: 1, value: payload.dst },
|
||||
{ name: "public key", type: FieldType.BYTES, size: 32, value: payload.publicKey },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, size: 2, value: payload.encrypted.cipherMAC },
|
||||
{ name: "destination hash", type: FieldType.UINT8, length: 1, value: payload.dst },
|
||||
{ name: "public key", type: FieldType.BYTES, length: 32, value: payload.publicKey },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, length: 2, value: payload.encrypted.cipherMAC },
|
||||
{
|
||||
name: "cipher text",
|
||||
type: FieldType.BYTES,
|
||||
size: payload.encrypted.cipherText.length,
|
||||
length: payload.encrypted.cipherText.length,
|
||||
value: payload.encrypted.cipherText
|
||||
}
|
||||
]
|
||||
@@ -569,12 +589,12 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "path payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
fields: [
|
||||
{ name: "destination hash", type: FieldType.UINT8, size: 1, value: payload.dst },
|
||||
{ name: "source hash", type: FieldType.UINT8, size: 1, value: payload.src }
|
||||
{ name: "destination hash", type: FieldType.UINT8, length: 1, value: payload.dst },
|
||||
{ name: "source hash", type: FieldType.UINT8, length: 1, value: payload.src }
|
||||
]
|
||||
};
|
||||
return { payload, segment };
|
||||
@@ -597,14 +617,14 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "trace payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
fields: [
|
||||
{ name: "tag", type: FieldType.DWORDS, size: 4, value: payload.tag },
|
||||
{ name: "auth code", type: FieldType.DWORDS, size: 4, value: payload.authCode },
|
||||
{ name: "flags", type: FieldType.UINT8, size: 1, value: payload.flags },
|
||||
{ name: "nodes", type: FieldType.BYTES, size: payload.nodes.length, value: payload.nodes }
|
||||
{ name: "tag", type: FieldType.DWORDS, length: 4, value: payload.tag },
|
||||
{ name: "auth code", type: FieldType.DWORDS, length: 4, value: payload.authCode },
|
||||
{ name: "flags", type: FieldType.UINT8, length: 1, value: payload.flags },
|
||||
{ name: "nodes", type: FieldType.BYTES, length: payload.nodes.length, value: payload.nodes }
|
||||
]
|
||||
};
|
||||
return { payload, segment };
|
||||
@@ -619,10 +639,10 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "raw custom payload",
|
||||
data: this.payload,
|
||||
fields: [{ name: "data", type: FieldType.BYTES, size: this.payload.length, value: this.payload }]
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
fields: [{ name: "data", type: FieldType.BYTES, length: this.payload.length, value: this.payload }]
|
||||
};
|
||||
return { payload, segment };
|
||||
}
|
||||
|
||||
@@ -112,18 +112,12 @@ describe("PrivateKey", () => {
|
||||
});
|
||||
|
||||
describe("SharedSecret", () => {
|
||||
const secret = randomBytes(32);
|
||||
const secret = randomBytes(16);
|
||||
|
||||
it("constructs from 32 bytes", () => {
|
||||
it("constructs from 16 bytes", () => {
|
||||
const ss = new SharedSecret(secret);
|
||||
expect(ss.toBytes()).toEqual(secret);
|
||||
});
|
||||
|
||||
it("pads if 16 bytes", () => {
|
||||
const short = randomBytes(16);
|
||||
const ss = new SharedSecret(short);
|
||||
expect(ss.toBytes().length).toBe(32);
|
||||
expect(Array.from(ss.toBytes()).slice(16)).toEqual(Array.from(short));
|
||||
expect(ss.toBytes().length).toBe(16);
|
||||
expect(bytesToHex(ss.toBytes())).toBe(bytesToHex(secret));
|
||||
});
|
||||
|
||||
it("throws on invalid length", () => {
|
||||
@@ -163,6 +157,9 @@ describe("SharedSecret", () => {
|
||||
it('fromName "Public"', () => {
|
||||
const ss = SharedSecret.fromName("Public");
|
||||
expect(ss).toBeInstanceOf(SharedSecret);
|
||||
const hash = ss.toHash();
|
||||
expect(typeof hash).toBe("number");
|
||||
expect(hash).toBe(0x11);
|
||||
});
|
||||
|
||||
it("fromName with #group", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { bytesToHex } from "@hamradio/packet";
|
||||
import { bytesToHex, hexToBytes } from "@hamradio/packet";
|
||||
import { Identity, LocalIdentity, Contact, Group, Contacts, parseNodeHash } from "../src/identity";
|
||||
import { PrivateKey, PublicKey, SharedSecret } from "../src/crypto";
|
||||
import { DecryptedGroupText, DecryptedGroupData } from "../src/packet.types";
|
||||
@@ -230,6 +230,18 @@ describe("Group", () => {
|
||||
it("decryptData throws on short ciphertext", () => {
|
||||
expect(() => group.decryptData(randomBytes(16), Uint8Array.of(1, 2, 3))).toThrow();
|
||||
});
|
||||
|
||||
it("hash is consistent with test vectors", () => {
|
||||
const testVectors = [
|
||||
{ name: "Public", hash: 0x11 },
|
||||
{ name: "#test", hash: 0xd9 }
|
||||
];
|
||||
|
||||
testVectors.forEach(({ name, hash }) => {
|
||||
const g = new Group(name);
|
||||
expect(`${name}:${g.hash()}`).toBe(`${name}:${hash}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Contacts", () => {
|
||||
@@ -501,4 +513,23 @@ describe("Contacts", () => {
|
||||
const res2 = contacts.decrypt(contactB.identity.hash(), localA.publicKey.key[0], hmac, ciphertext);
|
||||
expect(res2.decrypted).toEqual(msg);
|
||||
});
|
||||
|
||||
it("decryptGroupText on well known channels with test vectors", () => {
|
||||
const testVectors = [
|
||||
{
|
||||
name: "#test",
|
||||
channelHash: 0xd9,
|
||||
cipherMAC: hexToBytes("570D"),
|
||||
cipherText: hexToBytes("E397F0560B2B61396F7E236811FC70B70038E956045347D7F6B9976A46727427"),
|
||||
message: "corrauder 🏕️: Testing "
|
||||
}
|
||||
];
|
||||
|
||||
testVectors.forEach(({ name, channelHash, cipherMAC, cipherText, message }) => {
|
||||
const group = new Group(name);
|
||||
expect(group.hash()).toBe(channelHash);
|
||||
const decrypted = group.decryptText(cipherMAC, cipherText);
|
||||
expect(decrypted.message).toBe(message);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user