Refactoring
This commit is contained in:
494
src/identity.ts
494
src/identity.ts
@@ -1,98 +1,163 @@
|
||||
import { ecb } from '@noble/ciphers/aes.js';
|
||||
import { hmac } from '@noble/hashes/hmac.js';
|
||||
import { sha256 } from "@noble/hashes/sha2.js";
|
||||
import { ed25519, x25519 } from '@noble/curves/ed25519.js';
|
||||
import {
|
||||
BaseGroup,
|
||||
BaseGroupSecret,
|
||||
BaseIdentity,
|
||||
BaseKeyManager,
|
||||
BaseLocalIdentity,
|
||||
Contact,
|
||||
DecryptedAnonReq,
|
||||
DecryptedGroupData,
|
||||
DecryptedGroupText,
|
||||
DecryptedRequest,
|
||||
DecryptedResponse,
|
||||
DecryptedText,
|
||||
EncryptedPayload,
|
||||
NodeHash,
|
||||
Secret
|
||||
} from "./types";
|
||||
import { BufferReader, bytesToHex, equalBytes, hexToBytes } from "./parser";
|
||||
import { x25519 } from "@noble/curves/ed25519";
|
||||
import { PrivateKey, PublicKey, SharedSecret, StaticSecret } from "./crypto";
|
||||
import { IPublicKey } from "./crypto.types";
|
||||
import { NodeHash, IIdentity, ILocalIdentity } from "./identity.types";
|
||||
import { DecryptedGroupData, DecryptedGroupText } from "./packet.types";
|
||||
import { equalBytes, hexToBytes, BufferReader, BufferWriter } from "./parser";
|
||||
|
||||
// The "Public" group is a special group that all nodes are implicitly part of. It uses a fixed secret derived from the string "Public".
|
||||
const publicSecret = hexToBytes("8b3387e9c5cdea6ac9e5edbaa115cd72");
|
||||
|
||||
export class Identity extends BaseIdentity {
|
||||
constructor(publicKey: Uint8Array | string) {
|
||||
if (typeof publicKey === "string") {
|
||||
publicKey = hexToBytes(publicKey);
|
||||
export const parseNodeHash = (hash: NodeHash | string | Uint8Array): NodeHash => {
|
||||
if (hash instanceof Uint8Array) {
|
||||
return hash[0] as NodeHash;
|
||||
}
|
||||
if (typeof hash === "number") {
|
||||
if (hash < 0 || hash > 255) {
|
||||
throw new Error("NodeHash number must be between 0x00 and 0xFF");
|
||||
}
|
||||
super(publicKey);
|
||||
return hash as NodeHash;
|
||||
} else if (typeof hash === "string") {
|
||||
const parsed = hexToBytes(hash);
|
||||
if (parsed.length !== 1) {
|
||||
throw new Error("NodeHash string must represent a single byte");
|
||||
}
|
||||
return parsed[0] as NodeHash;
|
||||
}
|
||||
throw new Error("Invalid NodeHash type");
|
||||
}
|
||||
|
||||
public hash(): NodeHash {
|
||||
return bytesToHex(this.publicKey.slice(0, 1));
|
||||
}
|
||||
|
||||
public verify(message: Uint8Array, signature: Uint8Array): boolean {
|
||||
return ed25519.verify(message, signature, this.publicKey);
|
||||
const toPublicKeyBytes = (key: Identity | PublicKey | Uint8Array | string): Uint8Array => {
|
||||
if (key instanceof Identity) {
|
||||
return key.publicKey.toBytes();
|
||||
} else if (key instanceof PublicKey) {
|
||||
return key.toBytes();
|
||||
} else if (key instanceof Uint8Array) {
|
||||
return key;
|
||||
} else if (typeof key === 'string') {
|
||||
return hexToBytes(key);
|
||||
} else {
|
||||
throw new Error('Invalid type for toPublicKeyBytes');
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalIdentity extends Identity implements BaseLocalIdentity {
|
||||
public privateKey: Uint8Array;
|
||||
export class Identity implements IIdentity {
|
||||
public publicKey: PublicKey;
|
||||
|
||||
constructor(seed: Uint8Array | string) {
|
||||
if (typeof seed === "string") {
|
||||
seed = hexToBytes(seed);
|
||||
constructor(publicKey: PublicKey | Uint8Array | string) {
|
||||
if (publicKey instanceof PublicKey) {
|
||||
this.publicKey = publicKey;
|
||||
} else if (publicKey instanceof Uint8Array || typeof publicKey === 'string') {
|
||||
this.publicKey = new PublicKey(publicKey);
|
||||
} else {
|
||||
throw new Error('Invalid type for Identity constructor');
|
||||
}
|
||||
const { secretKey, publicKey } = ed25519.keygen(seed);
|
||||
super(publicKey);
|
||||
this.privateKey = secretKey;
|
||||
}
|
||||
|
||||
public sign(message: Uint8Array): Uint8Array {
|
||||
return ed25519.sign(message, this.privateKey);
|
||||
hash(): NodeHash {
|
||||
return this.publicKey.toHash();
|
||||
}
|
||||
|
||||
public calculateSharedSecret(other: BaseIdentity | Uint8Array): Uint8Array {
|
||||
if (other instanceof Uint8Array) {
|
||||
return x25519.getSharedSecret(this.privateKey, other);
|
||||
}
|
||||
return x25519.getSharedSecret(this.privateKey, other.publicKey);
|
||||
toString(): string {
|
||||
return this.publicKey.toString();
|
||||
}
|
||||
|
||||
public hash(): NodeHash {
|
||||
return super.hash();
|
||||
verify(signature: Uint8Array, message: Uint8Array): boolean {
|
||||
return this.publicKey.verify(message, signature);
|
||||
}
|
||||
|
||||
public verify(message: Uint8Array, signature: Uint8Array): boolean {
|
||||
return super.verify(message, signature);
|
||||
matches(other: Identity | PublicKey | Uint8Array | string): boolean {
|
||||
return this.publicKey.equals(toPublicKeyBytes(other));
|
||||
}
|
||||
}
|
||||
|
||||
export class Group extends BaseGroup {}
|
||||
export class LocalIdentity extends Identity implements ILocalIdentity {
|
||||
private privateKey: PrivateKey;
|
||||
|
||||
export class GroupSecret extends BaseGroupSecret {
|
||||
public static fromName(name: string): GroupSecret {
|
||||
if (name === "Public") {
|
||||
return new GroupSecret(publicSecret);
|
||||
} else if (!/^#/.test(name)) {
|
||||
throw new Error("Only the 'Public' group or groups starting with '#' are supported");
|
||||
constructor(privateKey: PrivateKey | Uint8Array | string, publicKey: PublicKey | Uint8Array | string) {
|
||||
if (publicKey instanceof PublicKey) {
|
||||
super(publicKey.toBytes());
|
||||
} else {
|
||||
super(publicKey);
|
||||
}
|
||||
|
||||
if (privateKey instanceof PrivateKey) {
|
||||
this.privateKey = privateKey;
|
||||
} else {
|
||||
this.privateKey = new PrivateKey(privateKey);
|
||||
}
|
||||
}
|
||||
|
||||
sign(message: Uint8Array): Uint8Array {
|
||||
return this.privateKey.sign(message);
|
||||
}
|
||||
|
||||
calculateSharedSecret(other: IIdentity | IPublicKey): SharedSecret {
|
||||
let otherPublicKey: PublicKey;
|
||||
if (other instanceof Identity) {
|
||||
otherPublicKey = other.publicKey;
|
||||
} else if ('toBytes' in other) {
|
||||
otherPublicKey = new PublicKey(other.toBytes());
|
||||
} else if ('publicKey' in other && other.publicKey instanceof Uint8Array) {
|
||||
otherPublicKey = new PublicKey(other.publicKey);
|
||||
} else if ('publicKey' in other && other.publicKey instanceof PublicKey) {
|
||||
otherPublicKey = other.publicKey;
|
||||
} else if ('publicKey' in other && typeof other.publicKey === 'function') {
|
||||
otherPublicKey = new PublicKey(other.publicKey().toBytes());
|
||||
} else {
|
||||
throw new Error('Invalid type for calculateSharedSecret comparison');
|
||||
}
|
||||
return new SharedSecret(this.privateKey.calculateSharedSecret(otherPublicKey));
|
||||
}
|
||||
}
|
||||
|
||||
export class Contact {
|
||||
public name: string = "";
|
||||
public identity: Identity;
|
||||
|
||||
constructor(name: string, identity: Identity | PublicKey | Uint8Array | string) {
|
||||
this.name = name;
|
||||
if (identity instanceof Identity) {
|
||||
this.identity = identity;
|
||||
} else if (identity instanceof PublicKey) {
|
||||
this.identity = new Identity(identity);
|
||||
} else if (identity instanceof Uint8Array || typeof identity === 'string') {
|
||||
this.identity = new Identity(identity);
|
||||
} else {
|
||||
throw new Error('Invalid type for Contact constructor');
|
||||
}
|
||||
}
|
||||
|
||||
public matches(hash: Uint8Array | PublicKey): boolean {
|
||||
return this.identity.publicKey.equals(hash);
|
||||
}
|
||||
|
||||
public publicKey(): PublicKey {
|
||||
return this.identity.publicKey;
|
||||
}
|
||||
|
||||
public calculateSharedSecret(me: LocalIdentity): SharedSecret {
|
||||
return me.calculateSharedSecret(this.identity);
|
||||
}
|
||||
}
|
||||
|
||||
export class Group {
|
||||
public name: string;
|
||||
private secret: SharedSecret;
|
||||
|
||||
constructor(name: string, secret?: SharedSecret) {
|
||||
this.name = name;
|
||||
if (secret) {
|
||||
this.secret = secret;
|
||||
} else {
|
||||
this.secret = SharedSecret.fromName(name);
|
||||
}
|
||||
const hash = sha256.create().update(new TextEncoder().encode(name)).digest();
|
||||
return new GroupSecret(hash.slice(0, 16));
|
||||
}
|
||||
|
||||
public hash(): NodeHash {
|
||||
return bytesToHex(this.secret.slice(0, 1));
|
||||
return this.secret.toHash();
|
||||
}
|
||||
|
||||
public decryptText(encrypted: EncryptedPayload): DecryptedGroupText {
|
||||
const data = this.macThenDecrypt(encrypted.cipherMAC, encrypted.cipherText);
|
||||
if (data.length < 8) {
|
||||
public decryptText(hmac: Uint8Array, ciphertext: Uint8Array): DecryptedGroupText {
|
||||
const data = this.secret.decrypt(hmac, ciphertext);
|
||||
if (data.length < 5) {
|
||||
throw new Error("Invalid ciphertext");
|
||||
}
|
||||
|
||||
@@ -110,9 +175,19 @@ export class GroupSecret extends BaseGroupSecret {
|
||||
}
|
||||
}
|
||||
|
||||
public decryptData(encrypted: EncryptedPayload): DecryptedGroupData {
|
||||
const data = this.macThenDecrypt(encrypted.cipherMAC, encrypted.cipherText);
|
||||
if (data.length < 8) {
|
||||
public encryptText(plain: DecryptedGroupText): { hmac: Uint8Array, ciphertext: Uint8Array } {
|
||||
const writer = new BufferWriter();
|
||||
writer.writeTimestamp(plain.timestamp);
|
||||
const flags = ((plain.textType & 0x3F) << 2) | (plain.attempt & 0x03);
|
||||
writer.writeByte(flags);
|
||||
writer.writeBytes(new TextEncoder().encode(plain.message));
|
||||
const data = writer.toBytes();
|
||||
return this.secret.encrypt(data);
|
||||
}
|
||||
|
||||
public decryptData(hmac: Uint8Array, ciphertext: Uint8Array): DecryptedGroupData {
|
||||
const data = this.secret.decrypt(hmac, ciphertext);
|
||||
if (data.length < 4) {
|
||||
throw new Error("Invalid ciphertext");
|
||||
}
|
||||
|
||||
@@ -120,212 +195,147 @@ export class GroupSecret extends BaseGroupSecret {
|
||||
return {
|
||||
timestamp: reader.readTimestamp(),
|
||||
data: reader.readBytes(reader.remainingBytes())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private macThenDecrypt(cipherMAC: Uint8Array, cipherText: Uint8Array): Uint8Array {
|
||||
const mac = hmac(sha256, this.secret, cipherText);
|
||||
if (!equalBytes(mac, cipherMAC)) {
|
||||
throw new Error("Invalid MAC");
|
||||
}
|
||||
|
||||
const block = ecb(this.secret.slice(0, 16), { disablePadding: true });
|
||||
const plain = block.decrypt(cipherText);
|
||||
|
||||
return plain;
|
||||
public encryptData(plain: DecryptedGroupData): { hmac: Uint8Array, ciphertext: Uint8Array } {
|
||||
const writer = new BufferWriter();
|
||||
writer.writeTimestamp(plain.timestamp);
|
||||
writer.writeBytes(plain.data);
|
||||
const data = writer.toBytes();
|
||||
return this.secret.encrypt(data);
|
||||
}
|
||||
}
|
||||
|
||||
export class KeyManager extends BaseKeyManager {
|
||||
private groups: Map<NodeHash, Group[]> = new Map();
|
||||
private contacts: Map<NodeHash, Contact[]> = new Map();
|
||||
private localIdentities: Map<NodeHash, LocalIdentity[]> = new Map();
|
||||
interface CachedLocalIdentity {
|
||||
identity: LocalIdentity;
|
||||
sharedSecrets: Record<string, SharedSecret>;
|
||||
}
|
||||
|
||||
public addGroup(group: Group): void {
|
||||
const hash = group.secret.hash();
|
||||
if (!this.groups.has(hash)) {
|
||||
this.groups.set(hash, [group]);
|
||||
} else {
|
||||
this.groups.get(hash)!.push(group);
|
||||
}
|
||||
export class Contacts {
|
||||
private localIdentities: CachedLocalIdentity[] = [];
|
||||
private contacts: Record<number, Contact[]> = {};
|
||||
private groups: Record<number, Group[]> = {};
|
||||
|
||||
public addLocalIdentity(identity: LocalIdentity) {
|
||||
this.localIdentities.push({ identity, sharedSecrets: {} });
|
||||
}
|
||||
|
||||
public addGroupSecret(name: string, secret?: Secret): void {
|
||||
if (typeof secret === "undefined") {
|
||||
secret = GroupSecret.fromName(name).secret;
|
||||
} else if (typeof secret === "string") {
|
||||
secret = hexToBytes(secret);
|
||||
public addContact(contact: Contact) {
|
||||
const hash = parseNodeHash(contact.identity.hash()) as number;
|
||||
if (!this.contacts[hash]) {
|
||||
this.contacts[hash] = [];
|
||||
}
|
||||
this.addGroup(new Group(name, new GroupSecret(secret)));
|
||||
this.contacts[hash].push(contact);
|
||||
}
|
||||
|
||||
public decryptGroupText(channelHash: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedGroupText, group: Group } {
|
||||
const groupSecrets = this.groups.get(channelHash);
|
||||
if (!groupSecrets) {
|
||||
throw new Error("No group secrets for channel");
|
||||
}
|
||||
|
||||
for (const group of groupSecrets) {
|
||||
try {
|
||||
const decrypted = group.decryptText(encrypted);
|
||||
return { decrypted, group: group };
|
||||
} catch {
|
||||
// Ignore and try next secret
|
||||
public decrypt(src: NodeHash | PublicKey, dst: NodeHash, hmac: Uint8Array, ciphertext: Uint8Array): {
|
||||
localIdentity: LocalIdentity,
|
||||
contact: Contact,
|
||||
decrypted: Uint8Array,
|
||||
} {
|
||||
// Find the public key associated with the source hash.
|
||||
let contacts: Contact[] = [];
|
||||
if (src instanceof PublicKey) {
|
||||
// Check if we have a contact with this exact public key (for direct messages).
|
||||
const srcHash = parseNodeHash(src.toHash()) as number;
|
||||
for (const contact of this.contacts[srcHash] || []) {
|
||||
if (contact.identity.matches(src)) {
|
||||
contacts.push(contact);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error("Failed to decrypt group text with any known secret");
|
||||
}
|
||||
|
||||
public decryptGroupData(channelHash: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedGroupData, group: Group } {
|
||||
const groupSecrets = this.groups.get(channelHash);
|
||||
if (!groupSecrets) {
|
||||
throw new Error("No group secrets for channel");
|
||||
}
|
||||
|
||||
for (const group of groupSecrets) {
|
||||
try {
|
||||
const decrypted = group.decryptData(encrypted);
|
||||
return { decrypted, group };
|
||||
} catch {
|
||||
// Ignore and try next secret
|
||||
// If no contact matches the public key, add a temporary contact with the hash and no name.
|
||||
if (contacts.length === 0) {
|
||||
contacts.push(new Contact("", new Identity(src.toBytes())));
|
||||
}
|
||||
}
|
||||
throw new Error("Failed to decrypt group data with any known secret");
|
||||
}
|
||||
|
||||
public addContact(contact: Contact): void {
|
||||
const hash = bytesToHex(contact.publicKey.slice(0, 1));
|
||||
if (!this.contacts.has(hash)) {
|
||||
this.contacts.set(hash, [contact]);
|
||||
} else {
|
||||
this.contacts.get(hash)!.push(contact);
|
||||
const srcHash = parseNodeHash(src) as number;
|
||||
contacts = this.contacts[srcHash] || [];
|
||||
}
|
||||
}
|
||||
|
||||
public addIdentity(name: string, publicKey: Uint8Array | string): void {
|
||||
if (typeof publicKey === "string") {
|
||||
publicKey = hexToBytes(publicKey);
|
||||
if (contacts.length === 0) {
|
||||
throw new Error("Unknown source hash");
|
||||
}
|
||||
this.addContact({ name, publicKey });
|
||||
}
|
||||
|
||||
public addLocalIdentity(seed: Secret): void {
|
||||
const localIdentity = new LocalIdentity(seed);
|
||||
const hash = localIdentity.hash();
|
||||
if (!this.localIdentities.has(hash)) {
|
||||
this.localIdentities.set(hash, [localIdentity]);
|
||||
} else {
|
||||
this.localIdentities.get(hash)!.push(localIdentity);
|
||||
// Find the local identity associated with the destination hash.
|
||||
const dstHash = parseNodeHash(dst) as number;
|
||||
const localIdentities = this.localIdentities.filter(li => li.identity.publicKey.key[0] === dstHash);
|
||||
if (localIdentities.length === 0) {
|
||||
throw new Error("Unknown destination hash");
|
||||
}
|
||||
}
|
||||
|
||||
private tryDecrypt(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: Uint8Array, contact: Contact, identity: BaseIdentity } {
|
||||
if (!this.localIdentities.has(dst)) {
|
||||
throw new Error(`No local identities for destination ${dst}`);
|
||||
}
|
||||
const localIdentities = this.localIdentities.get(dst)!;
|
||||
|
||||
if (!this.contacts.has(src)) {
|
||||
throw new Error(`No contacts for source ${src}`);
|
||||
}
|
||||
const contacts = this.contacts.get(src)!;
|
||||
|
||||
// Try to decrypt with each combination of local identity and their public key.
|
||||
for (const localIdentity of localIdentities) {
|
||||
for (const contact of contacts) {
|
||||
const sharedSecret = localIdentity.calculateSharedSecret(contact.publicKey);
|
||||
const mac = hmac(sha256, sharedSecret, encrypted.cipherText);
|
||||
if (!equalBytes(mac, encrypted.cipherMAC)) {
|
||||
continue; // Invalid MAC, try next combination
|
||||
const sharedSecret: SharedSecret = this.calculateSharedSecret(localIdentity, contact);
|
||||
try {
|
||||
const decrypted = sharedSecret.decrypt(hmac, ciphertext);
|
||||
return { localIdentity: localIdentity.identity, contact, decrypted };
|
||||
} catch {
|
||||
// Ignore decryption errors and try the next combination.
|
||||
}
|
||||
|
||||
const block = ecb(sharedSecret.slice(0, 16), { disablePadding: true });
|
||||
const plain = block.decrypt(encrypted.cipherText);
|
||||
if (plain.length < 8) {
|
||||
continue; // Invalid plaintext, try next combination
|
||||
}
|
||||
return { decrypted: plain, contact, identity: localIdentity };
|
||||
}
|
||||
}
|
||||
throw new Error("Failed to decrypt with any known identity/contact combination");
|
||||
|
||||
throw new Error("Decryption failed with all known identities and contacts");
|
||||
}
|
||||
|
||||
public decryptRequest(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedRequest, contact: Contact, identity: BaseIdentity } {
|
||||
const { decrypted, contact, identity } = this.tryDecrypt(dst, src, encrypted);
|
||||
const reader = new BufferReader(decrypted);
|
||||
return {
|
||||
decrypted: {
|
||||
timestamp: reader.readTimestamp(),
|
||||
requestType: reader.readByte(),
|
||||
requestData: reader.readBytes(),
|
||||
},
|
||||
contact,
|
||||
identity
|
||||
// Caches the calculated shared secret for a given local identity and contact to avoid redundant calculations.
|
||||
private calculateSharedSecret(localIdentity: CachedLocalIdentity, contact: Contact): SharedSecret {
|
||||
const cacheKey = contact.identity.toString();
|
||||
if (localIdentity.sharedSecrets[cacheKey]) {
|
||||
return localIdentity.sharedSecrets[cacheKey];
|
||||
}
|
||||
const sharedSecret = localIdentity.identity.calculateSharedSecret(contact.identity);
|
||||
localIdentity.sharedSecrets[cacheKey] = sharedSecret;
|
||||
return sharedSecret;
|
||||
}
|
||||
|
||||
public decryptResponse(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedResponse, contact: Contact, identity: BaseIdentity } {
|
||||
const { decrypted, contact, identity } = this.tryDecrypt(dst, src, encrypted);
|
||||
const reader = new BufferReader(decrypted);
|
||||
return {
|
||||
decrypted: {
|
||||
timestamp: reader.readTimestamp(),
|
||||
responseData: reader.readBytes(),
|
||||
},
|
||||
contact,
|
||||
identity
|
||||
public addGroup(group: Group) {
|
||||
const hash = parseNodeHash(group.hash()) as number;
|
||||
if (!this.groups[hash]) {
|
||||
this.groups[hash] = [];
|
||||
}
|
||||
this.groups[hash].push(group);
|
||||
}
|
||||
|
||||
public decryptText(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedText, contact: Contact, identity: BaseIdentity } {
|
||||
const { decrypted, contact, identity } = this.tryDecrypt(dst, src, encrypted);
|
||||
const reader = new BufferReader(decrypted);
|
||||
const timestamp = reader.readTimestamp();
|
||||
const flags = reader.readByte();
|
||||
const textType = (flags >> 2) & 0x3F;
|
||||
const attempt = flags & 0x03;
|
||||
const message = new TextDecoder('utf-8').decode(reader.readBytes());
|
||||
return {
|
||||
decrypted: {
|
||||
timestamp,
|
||||
textType,
|
||||
attempt,
|
||||
message
|
||||
},
|
||||
contact,
|
||||
identity
|
||||
public decryptGroupText(channelHash: NodeHash, hmac: Uint8Array, ciphertext: Uint8Array): {
|
||||
decrypted: DecryptedGroupText,
|
||||
group: Group
|
||||
} {
|
||||
const hash = parseNodeHash(channelHash) as number;
|
||||
const groups = this.groups[hash] || [];
|
||||
if (groups.length === 0) {
|
||||
throw new Error("Unknown group hash");
|
||||
}
|
||||
}
|
||||
|
||||
public decryptAnonReq(dst: NodeHash, publicKey: Uint8Array, encrypted: EncryptedPayload): { decrypted: DecryptedAnonReq, contact: Contact, identity: BaseIdentity } {
|
||||
if (!this.localIdentities.has(dst)) {
|
||||
throw new Error(`No local identities for destination ${dst}`);
|
||||
}
|
||||
const localIdentities = this.localIdentities.get(dst)!;
|
||||
|
||||
const contact = { publicKey } as Contact; // Create a temporary contact object for MAC verification
|
||||
|
||||
for (const localIdentity of localIdentities) {
|
||||
const sharedSecret = localIdentity.calculateSharedSecret(publicKey);
|
||||
const mac = hmac(sha256, sharedSecret, encrypted.cipherText);
|
||||
if (!equalBytes(mac, encrypted.cipherMAC)) {
|
||||
continue; // Invalid MAC, try next identity
|
||||
}
|
||||
|
||||
const block = ecb(sharedSecret.slice(0, 16), { disablePadding: true });
|
||||
const plain = block.decrypt(encrypted.cipherText);
|
||||
if (plain.length < 8) {
|
||||
continue; // Invalid plaintext, try next identity
|
||||
}
|
||||
const reader = new BufferReader(plain);
|
||||
return {
|
||||
decrypted: {
|
||||
timestamp: reader.readTimestamp(),
|
||||
data: reader.readBytes(),
|
||||
},
|
||||
contact,
|
||||
identity: localIdentity
|
||||
for (const group of groups) {
|
||||
try {
|
||||
const decrypted = group.decryptText(hmac, ciphertext);
|
||||
return { decrypted, group };
|
||||
} catch {
|
||||
// Ignore decryption errors and try the next group.
|
||||
}
|
||||
}
|
||||
throw new Error("Failed to decrypt anonymous request with any known identity");
|
||||
throw new Error("Decryption failed with all known groups");
|
||||
}
|
||||
|
||||
public decryptGroupData(channelHash: NodeHash, hmac: Uint8Array, ciphertext: Uint8Array): {
|
||||
decrypted: DecryptedGroupData,
|
||||
group: Group
|
||||
} {
|
||||
const hash = parseNodeHash(channelHash) as number;
|
||||
const groups = this.groups[hash] || [];
|
||||
if (groups.length === 0) {
|
||||
throw new Error("Unknown group hash");
|
||||
}
|
||||
for (const group of groups) {
|
||||
try {
|
||||
const decrypted = group.decryptData(hmac, ciphertext);
|
||||
return { decrypted, group };
|
||||
} catch {
|
||||
// Ignore decryption errors and try the next group.
|
||||
}
|
||||
}
|
||||
throw new Error("Decryption failed with all known groups");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user