74 lines
2.9 KiB
JavaScript
74 lines
2.9 KiB
JavaScript
// Generated, do not edit. sha256:c9e44c618374bc6218029ec68095d9d8a62c6e5839ffe1dd3084b46600ccb118
|
|||
|
|
import {
|
||
|
|
secp256k1, sha256, chacha20, hkdfExtract, hkdfExpand, hmac, base64,
|
||
|
|
hexToBytes, utf8ToBytes, concatBytes,
|
||
|
|
} from '../vendor/crypto/noble-scure.js';
|
||
|
|
|
||
|
|
const VERSION = 2;
|
||
|
|
const SALT = utf8ToBytes('nip44-v2');
|
||
|
|
const MIN = 1;
|
||
|
|
const MAX = 65535;
|
||
|
|
|
||
|
|
const asBytes = (x) => (typeof x === 'string' ? hexToBytes(x) : x);
|
||
|
|
|
||
|
|
export function conversationKey(secret, pubkey) {
|
||
|
|
const shared = secp256k1.getSharedSecret(asBytes(secret), concatBytes(Uint8Array.of(2), asBytes(pubkey)));
|
||
|
|
return hkdfExtract(sha256, shared.subarray(1, 33), SALT);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function messageKeys(convKey, nonce) {
|
||
|
|
const keys = hkdfExpand(sha256, asBytes(convKey), asBytes(nonce), 76);
|
||
|
|
return { chachaKey: keys.subarray(0, 32), chachaNonce: keys.subarray(32, 44), hmacKey: keys.subarray(44, 76) };
|
||
|
|
}
|
||
|
|
|
||
|
|
export function paddedLength(len) {
|
||
|
|
if (len <= 32) return 32;
|
||
|
|
const next = 1 << (Math.floor(Math.log2(len - 1)) + 1);
|
||
|
|
const chunk = next <= 256 ? 32 : next / 8;
|
||
|
|
return chunk * (Math.floor((len - 1) / chunk) + 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
function pad(text) {
|
||
|
|
const bytes = utf8ToBytes(text);
|
||
|
|
if (bytes.length < MIN || bytes.length > MAX) throw new Error(`A NIP-44 message is ${MIN} to ${MAX} bytes.`);
|
||
|
|
const out = new Uint8Array(2 + paddedLength(bytes.length));
|
||
|
|
out[0] = bytes.length >> 8;
|
||
|
|
out[1] = bytes.length & 0xff;
|
||
|
|
out.set(bytes, 2);
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
function unpad(padded) {
|
||
|
|
const len = (padded[0] << 8) | padded[1];
|
||
|
|
if (len < MIN || padded.length !== 2 + paddedLength(len)) throw new Error('Invalid padding.');
|
||
|
|
return new TextDecoder('utf-8', { fatal: true }).decode(padded.subarray(2, 2 + len));
|
||
|
|
}
|
||
|
|
|
||
|
|
function sameBytes(a, b) {
|
||
|
|
if (a.length !== b.length) return false;
|
||
|
|
let d = 0;
|
||
|
|
for (let i = 0; i < a.length; i++) d |= a[i] ^ b[i];
|
||
|
|
return d === 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function encrypt(text, convKey, nonce = crypto.getRandomValues(new Uint8Array(32))) {
|
||
|
|
const n = asBytes(nonce);
|
||
|
|
const { chachaKey, chachaNonce, hmacKey } = messageKeys(convKey, n);
|
||
|
|
const ciphertext = chacha20(chachaKey, chachaNonce, pad(text));
|
||
|
|
const mac = hmac(sha256, hmacKey, concatBytes(n, ciphertext));
|
||
|
|
return base64.encode(concatBytes(Uint8Array.of(VERSION), n, ciphertext, mac));
|
||
|
|
}
|
||
|
|
|
||
|
|
export function decrypt(payload, convKey) {
|
||
|
|
const p = String(payload ?? '');
|
||
|
|
if (!p || p[0] === '#') throw new Error('Unknown NIP-44 version.');
|
||
|
|
if (p.length < 132 || p.length > 87472) throw new Error('Invalid NIP-44 payload length.');
|
||
|
|
const data = base64.decode(p);
|
||
|
|
if (data[0] !== VERSION) throw new Error(`Unknown NIP-44 version ${data[0]}.`);
|
||
|
|
const nonce = data.subarray(1, 33);
|
||
|
|
const ciphertext = data.subarray(33, data.length - 32);
|
||
|
|
const mac = data.subarray(data.length - 32);
|
||
|
|
const { chachaKey, chachaNonce, hmacKey } = messageKeys(convKey, nonce);
|
||
|
|
if (!sameBytes(mac, hmac(sha256, hmacKey, concatBytes(nonce, ciphertext)))) throw new Error('Invalid MAC.');
|
||
|
|
return unpad(chacha20(chachaKey, chachaNonce, ciphertext));
|
||
|
|
}
|