Files
jazz-tools/src/crypto.ts
2023-07-20 13:57:15 +01:00

236 lines
6.8 KiB
TypeScript

import { ed25519, x25519 } from "@noble/curves/ed25519";
import { xsalsa20_poly1305, xsalsa20 } from "@noble/ciphers/salsa";
import { JsonValue } from "./jsonValue";
import { base58, base64url } from "@scure/base";
import stableStringify from "fast-json-stable-stringify";
import { blake3 } from "@noble/hashes/blake3";
import { randomBytes } from "@noble/ciphers/webcrypto/utils";
import { MultiLogID, SessionID, TransactionID } from "./multilog";
export type SignatorySecret = `signatorySecret_z${string}`;
export type SignatoryID = `signatory_z${string}`;
export type Signature = `signature_z${string}`;
export type RecipientSecret = `recipientSecret_z${string}`;
export type RecipientID = `recipient_z${string}`;
export type Sealed = `sealed_U${string}`;
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
export function newRandomSignatory(): SignatorySecret {
return `signatorySecret_z${base58.encode(
ed25519.utils.randomPrivateKey()
)}`;
}
export function getSignatoryID(secret: SignatorySecret): SignatoryID {
return `signatory_z${base58.encode(
ed25519.getPublicKey(
base58.decode(secret.substring("signatorySecret_z".length))
)
)}`;
}
export function sign(secret: SignatorySecret, message: JsonValue): Signature {
const signature = ed25519.sign(
textEncoder.encode(stableStringify(message)),
base58.decode(secret.substring("signatorySecret_z".length))
);
return `signature_z${base58.encode(signature)}`;
}
export function verify(
signature: Signature,
message: JsonValue,
id: SignatoryID
): boolean {
return ed25519.verify(
base58.decode(signature.substring("signature_z".length)),
textEncoder.encode(stableStringify(message)),
base58.decode(id.substring("signatory_z".length))
);
}
export function newRandomRecipient(): RecipientSecret {
return `recipientSecret_z${base58.encode(x25519.utils.randomPrivateKey())}`;
}
export function getRecipientID(secret: RecipientSecret): RecipientID {
return `recipient_z${base58.encode(
x25519.getPublicKey(
base58.decode(secret.substring("recipientSecret_z".length))
)
)}`;
}
export type SealedSet = {
[recipient: RecipientID]: Sealed;
};
export function seal(
message: JsonValue,
from: RecipientSecret,
to: Set<RecipientID>,
nOnceMaterial: { in: MultiLogID; tx: TransactionID }
): SealedSet {
const nOnce = blake3(
textEncoder.encode(stableStringify(nOnceMaterial))
).slice(0, 24);
const recipientsSorted = Array.from(to).sort();
const recipientPubs = recipientsSorted.map((recipient) => {
return base58.decode(recipient.substring("recipient_z".length));
});
const senderPriv = base58.decode(
from.substring("recipientSecret_z".length)
);
const plaintext = textEncoder.encode(stableStringify(message));
const sealedSet: SealedSet = {};
for (let i = 0; i < recipientsSorted.length; i++) {
const recipient = recipientsSorted[i];
const sharedSecret = x25519.getSharedSecret(
senderPriv,
recipientPubs[i]
);
const sealedBytes = xsalsa20_poly1305(sharedSecret, nOnce).encrypt(
plaintext
);
sealedSet[recipient] = `sealed_U${base64url.encode(sealedBytes)}`;
}
return sealedSet;
}
export function openAs(
sealedSet: SealedSet,
recipient: RecipientSecret,
from: RecipientID,
nOnceMaterial: { in: MultiLogID; tx: TransactionID }
): JsonValue | undefined {
const nOnce = blake3(
textEncoder.encode(stableStringify(nOnceMaterial))
).slice(0, 24);
const recipientPriv = base58.decode(
recipient.substring("recipientSecret_z".length)
);
const senderPub = base58.decode(from.substring("recipient_z".length));
const sealed = sealedSet[getRecipientID(recipient)];
if (!sealed) {
return undefined;
}
const sealedBytes = base64url.decode(sealed.substring("sealed_U".length));
const sharedSecret = x25519.getSharedSecret(recipientPriv, senderPub);
const plaintext = xsalsa20_poly1305(sharedSecret, nOnce).decrypt(
sealedBytes
);
try {
return JSON.parse(textDecoder.decode(plaintext));
} catch (e) {
return undefined;
}
}
export type Hash = `hash_z${string}`;
export function secureHash(value: JsonValue): Hash {
return `hash_z${base58.encode(
blake3(textEncoder.encode(stableStringify(value)))
)}`;
}
export class StreamingHash {
state: ReturnType<typeof blake3.create>;
constructor(fromClone?: ReturnType<typeof blake3.create>) {
this.state = fromClone || blake3.create({});
}
update(value: JsonValue) {
this.state.update(textEncoder.encode(stableStringify(value)));
}
digest(): Hash {
const hash = this.state.digest();
return `hash_z${base58.encode(hash)}`;
}
clone(): StreamingHash {
return new StreamingHash(this.state.clone());
}
}
export type ShortHash = `shortHash_z${string}`;
export function shortHash(value: JsonValue): ShortHash {
return `shortHash_z${base58.encode(
blake3(textEncoder.encode(stableStringify(value))).slice(0, 19)
)}`;
}
export type Encrypted<T extends JsonValue> =
`encrypted_U${string}`;
export type KeySecret = `keySecret_z${string}`;
export type KeyID = `key_z${string}`;
export function newRandomKeySecret(): { secret: KeySecret; id: KeyID } {
return {
secret: `keySecret_z${base58.encode(randomBytes(32))}`,
id: `key_z${base58.encode(randomBytes(12))}`,
};
}
export function encrypt<T extends JsonValue>(value: T, keySecret: KeySecret, nOnceMaterial: {in: MultiLogID, tx: TransactionID}): Encrypted<T> {
const keySecretBytes = base58.decode(
keySecret.substring("keySecret_z".length)
);
const nOnce = blake3(
textEncoder.encode(stableStringify(nOnceMaterial))
).slice(0, 24);
const plaintext = textEncoder.encode(stableStringify(value));
const ciphertext = xsalsa20(
keySecretBytes,
nOnce,
plaintext,
);
return `encrypted_U${base64url.encode(ciphertext)}`;
};
export function decrypt<T extends JsonValue>(encrypted: Encrypted<T>, keySecret: KeySecret, nOnceMaterial: {in: MultiLogID, tx: TransactionID}): T | undefined {
const keySecretBytes = base58.decode(
keySecret.substring("keySecret_z".length)
);
const nOnce = blake3(
textEncoder.encode(stableStringify(nOnceMaterial))
).slice(0, 24);
const ciphertext = base64url.decode(
encrypted.substring("encrypted_U".length)
);
const plaintext = xsalsa20(
keySecretBytes,
nOnce,
ciphertext,
);
try {
return JSON.parse(textDecoder.decode(plaintext));
} catch (e) {
return undefined;
}
}