feat: nip44
This commit is contained in:
@ -1,8 +1,9 @@
|
||||
import * as secp from "@noble/curves/secp256k1";
|
||||
import * as utils from "@noble/curves/abstract/utils";
|
||||
|
||||
import { EventKind, HexKey, NostrEvent } from ".";
|
||||
import base64 from "@protobufjs/base64";
|
||||
import { sha256, unixNow } from "./Utils";
|
||||
import { Nip4WebCryptoEncryptor } from "./impl/nip4";
|
||||
|
||||
export interface Tag {
|
||||
key: string
|
||||
@ -135,58 +136,15 @@ export abstract class EventExt {
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the given message content
|
||||
*/
|
||||
static async encryptData(content: string, pubkey: HexKey, privkey: HexKey) {
|
||||
const key = await this.#getDmSharedKey(pubkey, privkey);
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(16));
|
||||
const data = new TextEncoder().encode(content);
|
||||
const result = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-CBC",
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
data
|
||||
);
|
||||
const uData = new Uint8Array(result);
|
||||
return `${base64.encode(uData, 0, result.byteLength)}?iv=${base64.encode(iv, 0, 16)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the content of the message
|
||||
*/
|
||||
static async decryptData(cyphertext: string, privkey: HexKey, pubkey: HexKey) {
|
||||
const key = await this.#getDmSharedKey(pubkey, privkey);
|
||||
const cSplit = cyphertext.split("?iv=");
|
||||
const data = new Uint8Array(base64.length(cSplit[0]));
|
||||
base64.decode(cSplit[0], data, 0);
|
||||
|
||||
const iv = new Uint8Array(base64.length(cSplit[1]));
|
||||
base64.decode(cSplit[1], iv, 0);
|
||||
|
||||
const result = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-CBC",
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
data
|
||||
);
|
||||
return new TextDecoder().decode(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the content of this message in place
|
||||
*/
|
||||
static async decryptDm(content: string, privkey: HexKey, pubkey: HexKey) {
|
||||
return await this.decryptData(content, privkey, pubkey);
|
||||
const enc = new Nip4WebCryptoEncryptor();
|
||||
const key = enc.getSharedSecret(privkey, pubkey);
|
||||
return await enc.decryptData(content, key);
|
||||
}
|
||||
|
||||
static async #getDmSharedKey(pubkey: HexKey, privkey: HexKey) {
|
||||
const sharedPoint = secp.secp256k1.getSharedSecret(privkey, "02" + pubkey);
|
||||
const sharedX = sharedPoint.slice(1, 33);
|
||||
return await window.crypto.subtle.importKey("raw", sharedX, { name: "AES-CBC" }, false, ["encrypt", "decrypt"]);
|
||||
static async encryptDm(content: string, privKey: HexKey, pubKey: HexKey) {
|
||||
const enc = new Nip4WebCryptoEncryptor();
|
||||
const secret = enc.getSharedSecret(privKey, pubKey);
|
||||
return await enc.encryptData(content, secret);
|
||||
}
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ export class EventPublisher {
|
||||
unwrap(window.nostr?.nip04?.encrypt).call(window.nostr?.nip04, key, content)
|
||||
);
|
||||
} else if (this.#privateKey) {
|
||||
return await EventExt.encryptData(content, key, this.#privateKey);
|
||||
return await EventExt.encryptDm(content, this.#privateKey, key);
|
||||
} else {
|
||||
throw new Error("Can't encrypt content, no private keys available");
|
||||
}
|
||||
|
52
packages/system/src/impl/nip4.ts
Normal file
52
packages/system/src/impl/nip4.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { MessageEncryptor } from "index";
|
||||
|
||||
import { base64 } from "@scure/base";
|
||||
import { secp256k1 } from "@noble/curves/secp256k1";
|
||||
|
||||
export class Nip4WebCryptoEncryptor implements MessageEncryptor {
|
||||
getSharedSecret(privateKey: string, publicKey: string) {
|
||||
const sharedPoint = secp256k1.getSharedSecret(privateKey, "02" + publicKey);
|
||||
const sharedX = sharedPoint.slice(1, 33);
|
||||
return sharedX;
|
||||
}
|
||||
|
||||
async encryptData(content: string, sharedSecet: Uint8Array) {
|
||||
const key = await this.#importKey(sharedSecet);
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(16));
|
||||
const data = new TextEncoder().encode(content);
|
||||
const result = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-CBC",
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
data
|
||||
);
|
||||
const uData = new Uint8Array(result);
|
||||
return `${base64.encode(uData)}?iv=${base64.encode(iv)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the content of the message
|
||||
*/
|
||||
async decryptData(cyphertext: string, sharedSecet: Uint8Array) {
|
||||
const key = await this.#importKey(sharedSecet);
|
||||
const cSplit = cyphertext.split("?iv=");
|
||||
const data = base64.decode(cSplit[0]);
|
||||
const iv = base64.decode(cSplit[1]);
|
||||
|
||||
const result = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-CBC",
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
data
|
||||
);
|
||||
return new TextDecoder().decode(result);
|
||||
}
|
||||
|
||||
async #importKey(sharedSecet: Uint8Array) {
|
||||
return await window.crypto.subtle.importKey("raw", sharedSecet, { name: "AES-CBC" }, false, ["encrypt", "decrypt"]);
|
||||
}
|
||||
}
|
40
packages/system/src/impl/nip44.ts
Normal file
40
packages/system/src/impl/nip44.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { MessageEncryptor } from "index";
|
||||
|
||||
import { base64 } from "@scure/base";
|
||||
import { randomBytes } from '@noble/hashes/utils'
|
||||
import { streamXOR as xchacha20 } from '@stablelib/xchacha20'
|
||||
import { secp256k1 } from "@noble/curves/secp256k1";
|
||||
import { sha256 } from '@noble/hashes/sha256'
|
||||
|
||||
export enum Nip44Version {
|
||||
Reserved = 0x00,
|
||||
XChaCha20 = 0x01
|
||||
}
|
||||
|
||||
export class Nip44Encryptor implements MessageEncryptor {
|
||||
getSharedSecret(privateKey: string, publicKey: string) {
|
||||
const key = secp256k1.getSharedSecret(privateKey, '02' + publicKey)
|
||||
return sha256(key.slice(1, 33));
|
||||
}
|
||||
|
||||
encryptData(content: string, sharedSecret: Uint8Array) {
|
||||
const nonce = randomBytes(24)
|
||||
const plaintext = new TextEncoder().encode(content)
|
||||
const ciphertext = xchacha20(sharedSecret, nonce, plaintext, plaintext);
|
||||
const ctb64 = base64.encode(Uint8Array.from(ciphertext))
|
||||
const nonceb64 = base64.encode(nonce)
|
||||
return JSON.stringify({ ciphertext: ctb64, nonce: nonceb64, v: Nip44Version.XChaCha20 })
|
||||
}
|
||||
|
||||
decryptData(cyphertext: string, sharedSecret: Uint8Array) {
|
||||
const dt = JSON.parse(cyphertext)
|
||||
if (dt.v !== 1) throw new Error('NIP44: unknown encryption version')
|
||||
|
||||
const ciphertext = base64.decode(dt.ciphertext)
|
||||
const nonce = base64.decode(dt.nonce)
|
||||
const plaintext = xchacha20(sharedSecret, nonce, ciphertext, ciphertext)
|
||||
const text = new TextDecoder().decode(plaintext)
|
||||
return text;
|
||||
}
|
||||
|
||||
}
|
@ -19,6 +19,8 @@ export * from "./EventBuilder";
|
||||
export * from "./NostrLink";
|
||||
export * from "./cache";
|
||||
export * from "./ProfileCache";
|
||||
export * from "./impl/nip4";
|
||||
export * from "./impl/nip44";
|
||||
|
||||
export interface SystemInterface {
|
||||
/**
|
||||
@ -40,4 +42,10 @@ export interface SystemSnapshot {
|
||||
filters: Array<ReqFilter>;
|
||||
subFilters: Array<ReqFilter>;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface MessageEncryptor {
|
||||
getSharedSecret(privateKey: string, publicKey: string): Promise<Uint8Array> | Uint8Array
|
||||
encryptData(plaintext: string, sharedSecet: Uint8Array): Promise<string> | string
|
||||
decryptData(cyphertext: string, sharedSecet: Uint8Array): Promise<string> | string
|
||||
}
|
Reference in New Issue
Block a user