From e006ae3eaa050239f3fe8bdc1ab737ff0ef3cc29 Mon Sep 17 00:00:00 2001 From: Benjamin Preiss Date: Wed, 4 Oct 2023 18:29:11 +0200 Subject: [PATCH 1/5] Add symmetric capabilities to DecryptedThing --- packages/utils/crypto/src/encryption.ts | 245 ++++++++++++++++++++---- 1 file changed, 209 insertions(+), 36 deletions(-) diff --git a/packages/utils/crypto/src/encryption.ts b/packages/utils/crypto/src/encryption.ts index e57507593..dd29db04a 100644 --- a/packages/utils/crypto/src/encryption.ts +++ b/packages/utils/crypto/src/encryption.ts @@ -27,7 +27,10 @@ export abstract class MaybeEncrypted { } decrypt( - keyOrKeychain?: Keychain | X25519Keypair + keyOrKeychain?: + | Keychain + | X25519Keypair + | Uint8Array /* Comment: The last is added */ ): Promise> | DecryptedThing { throw new Error("Not implemented"); } @@ -45,6 +48,24 @@ export abstract class MaybeEncrypted { abstract get byteLength(): number; } +type EncryptAsymmetricParameters = { + x25519Keypair: X25519Keypair; + receiverPublicKeys: (X25519PublicKey | Ed25519PublicKey)[]; +}; + +type EncryptSymmetricParameters = { symmetricKey: Uint8Array }; + +type EncryptReturnValue = + Parameters extends EncryptSymmetricParameters + ? EncryptedSymmetricThing + : EncryptedThing; + +function isEncryptSymmetricParameters( + parameters: EncryptSymmetricParameters | EncryptAsymmetricParameters +): parameters is EncryptSymmetricParameters { + return (parameters as EncryptSymmetricParameters).symmetricKey !== undefined; +} + @variant(0) export class DecryptedThing extends MaybeEncrypted { @field({ type: Uint8Array }) @@ -69,50 +90,67 @@ export class DecryptedThing extends MaybeEncrypted { return deserialize(this._data, clazz); } - async encrypt( - x25519Keypair: X25519Keypair, - ...receiverPublicKeys: (X25519PublicKey | Ed25519PublicKey)[] - ): Promise> { + async encrypt< + Parameters extends EncryptAsymmetricParameters | EncryptSymmetricParameters + >( + parameters: EncryptAsymmetricParameters | EncryptSymmetricParameters + /* Comment: instead of + x25519Keypair: X25519Keypair, + ...receiverPublicKeys: (X25519PublicKey | Ed25519PublicKey)[] */ + ): Promise< + EncryptReturnValue + > /* Comment: instead of Promise> */ { const bytes = serialize(this); - const epheremalKey = sodium.crypto_secretbox_keygen(); + const epheremalKey = isEncryptSymmetricParameters(parameters) + ? parameters.symmetricKey + : sodium.crypto_secretbox_keygen(); + /* Comment: instead of const epheremalKey = sodium.crypto_secretbox_keygen(); */ const nonce = randomBytes(NONCE_LENGTH); // crypto random is faster than sodim random const cipher = sodium.crypto_secretbox_easy(bytes, nonce, epheremalKey); - const receiverX25519PublicKeys = await Promise.all( - receiverPublicKeys.map((key) => { - if (key instanceof Ed25519PublicKey) { - return X25519PublicKey.from(key); - } - return key; - }) - ); + if (!isEncryptSymmetricParameters(parameters)) { + const { receiverPublicKeys, x25519Keypair } = parameters; + const receiverX25519PublicKeys = await Promise.all( + receiverPublicKeys.map((key) => { + if (key instanceof Ed25519PublicKey) { + return X25519PublicKey.from(key); + } + return key; + }) + ); - const ks = receiverX25519PublicKeys.map((receiverPublicKey) => { - const kNonce = randomBytes(NONCE_LENGTH); // crypto random is faster than sodium random - return new K({ - encryptedKey: new CipherWithNonce({ - cipher: sodium.crypto_box_easy( - epheremalKey, - kNonce, - receiverPublicKey.publicKey, - x25519Keypair.secretKey.secretKey - ), - nonce: kNonce - }), - receiverPublicKey + const ks = receiverX25519PublicKeys.map((receiverPublicKey) => { + const kNonce = randomBytes(NONCE_LENGTH); // crypto random is faster than sodium random + return new K({ + encryptedKey: new CipherWithNonce({ + cipher: sodium.crypto_box_easy( + epheremalKey, + kNonce, + receiverPublicKey.publicKey, + x25519Keypair.secretKey.secretKey + ), + nonce: kNonce + }), + receiverPublicKey + }); }); - }); - - const enc = new EncryptedThing({ + const enc = new EncryptedThing({ + encrypted: new Uint8Array(cipher), + nonce, + envelope: new Envelope({ + senderPublicKey: x25519Keypair.publicKey, + ks + }) + }); + enc._decrypted = this; + return enc as EncryptReturnValue; + } + const enc = new EncryptedSymmetricThing({ encrypted: new Uint8Array(cipher), - nonce, - envelope: new Envelope({ - senderPublicKey: x25519Keypair.publicKey, - ks - }) + nonce }); enc._decrypted = this; - return enc; + return enc as EncryptReturnValue; } get decrypted(): DecryptedThing { @@ -374,3 +412,138 @@ export class EncryptedThing extends MaybeEncrypted { return this._encrypted.byteLength; // ignore other metdata for now in the size calculation } } + +@variant(2) +export class EncryptedSymmetricThing extends MaybeEncrypted { + @field({ type: Uint8Array }) + _encrypted: Uint8Array; + + @field({ type: Uint8Array }) + _nonce: Uint8Array; + + constructor(props?: { encrypted: Uint8Array; nonce: Uint8Array }) { + super(); + if (props) { + this._encrypted = props.encrypted; + this._nonce = props.nonce; + } + } + + _decrypted?: DecryptedThing; + get decrypted(): DecryptedThing { + if (!this._decrypted) { + throw new Error( + "Entry has not been decrypted, invoke decrypt method before" + ); + } + return this._decrypted; + } + + async decrypt( + keyResolver?: Uint8Array /* Comment: instead of Keychain | X25519Keypair */ + ): Promise> { + if (this._decrypted) { + return this._decrypted; + } + + if (!keyResolver) { + throw new AccessError("Expecting key resolver"); + } + /* + // We only need to open with one of the keys + let key: { index: number; keypair: X25519Keypair } | undefined; + if (keyResolver instanceof X25519Keypair) { + for (const [i, k] of this._envelope._ks.entries()) { + if (k._receiverPublicKey.equals(keyResolver.publicKey)) { + key = { + index: i, + keypair: keyResolver + }; + } + } + } else { + for (const [i, k] of this._envelope._ks.entries()) { + const exported = await keyResolver.exportByKey(k._receiverPublicKey); + if (exported) { + key = { + index: i, + keypair: exported + }; + break; + } + } + } */ + + /* if (key) { + const k = this._envelope._ks[key.index]; + let secretKey: X25519SecretKey = undefined as any; + if (key.keypair instanceof X25519Keypair) { + secretKey = key.keypair.secretKey; + } else { + secretKey = await X25519SecretKey.from(key.keypair); + } + let epheremalKey: Uint8Array; + try { + epheremalKey = sodium.crypto_box_open_easy( + k._encryptedKey.cipher, + k._encryptedKey.nonce, + this._envelope._senderPublicKey.publicKey, + secretKey.secretKey + ); + } catch (error) { + throw new AccessError('Failed to decrypt'); + } */ + + // TODO: is nested decryption necessary? + /* let der: any = this; + let counter = 0; + while (der instanceof EncryptedThing) { + const decrypted = await sodium.crypto_secretbox_open_easy(this._encrypted, this._nonce, epheremalKey); + der = deserialize(decrypted, DecryptedThing) + counter += 1; + if (counter >= 10) { + throw new Error("Unexpected decryption behaviour, data seems to always be in encrypted state") + } + } */ + + const der = deserialize( + sodium.crypto_secretbox_open_easy( + this._encrypted, + this._nonce, + keyResolver /* Comment: instead of epheremalKey */ + ), + DecryptedThing + ); + this._decrypted = der as DecryptedThing; + /* } else { + throw new AccessError('Failed to resolve decryption key'); + } */ + return this._decrypted; + } + + equals(other: MaybeEncrypted): boolean { + if (other instanceof EncryptedSymmetricThing) { + if (!equals(this._encrypted, other._encrypted)) { + return false; + } + if (!equals(this._nonce, other._nonce)) { + return false; + } + /* + if (!this._envelope.equals(other._envelope)) { + return false; + } */ + return true; + } else { + return false; + } + } + + clear() { + this._decrypted = undefined; + } + + get byteLength() { + return this._encrypted.byteLength; // ignore other metdata for now in the size calculation + } +} From 9bc0facb2e16ec26cbe1f7b6e76b6abf4c35e089 Mon Sep 17 00:00:00 2001 From: Benjamin Preiss Date: Fri, 6 Oct 2023 00:16:45 +0200 Subject: [PATCH 2/5] Implement HashedKeyEnvelope --- packages/utils/crypto/src/aes256.ts | 28 +++++ packages/utils/crypto/src/encryption.ts | 131 +++++++++++------------- 2 files changed, 88 insertions(+), 71 deletions(-) create mode 100644 packages/utils/crypto/src/aes256.ts diff --git a/packages/utils/crypto/src/aes256.ts b/packages/utils/crypto/src/aes256.ts new file mode 100644 index 000000000..63f292549 --- /dev/null +++ b/packages/utils/crypto/src/aes256.ts @@ -0,0 +1,28 @@ +import { field, fixedArray, variant } from "@dao-xyz/borsh"; +import { PlainKey } from "./key"; +import { compare } from "@peerbit/uint8arrays"; +import { toHexString } from "./utils"; + +@variant(0) +export class Aes256Key extends PlainKey { + @field({ type: fixedArray("u8", 32) }) + key: Uint8Array; + + constructor(properties: { key: Uint8Array }) { + super(); + if (properties.key.length !== 32) { + throw new Error("Expecting key to have length 32"); + } + this.key = properties.key; + } + + equals(other: Aes256Key): boolean { + if (other instanceof Aes256Key) { + return compare(this.key, other.key) === 0; + } + return false; + } + toString(): string { + return "aes256/" + toHexString(this.key); + } +} diff --git a/packages/utils/crypto/src/encryption.ts b/packages/utils/crypto/src/encryption.ts index dd29db04a..1684ea31c 100644 --- a/packages/utils/crypto/src/encryption.ts +++ b/packages/utils/crypto/src/encryption.ts @@ -5,7 +5,8 @@ import { field, serialize, variant, - vec + vec, + fixedArray } from "@dao-xyz/borsh"; import { equals } from "@peerbit/uint8arrays"; import { AccessError } from "./errors.js"; @@ -14,6 +15,8 @@ import { X25519Keypair, X25519PublicKey, X25519SecretKey } from "./x25519.js"; import { Ed25519Keypair, Ed25519PublicKey } from "./ed25519.js"; import { randomBytes } from "./random.js"; import { Keychain } from "./keychain.js"; +import { sha256 } from "./hash.js"; +import { Aes256Key } from "./aes256.js"; const NONCE_LENGTH = 24; @@ -30,7 +33,7 @@ export abstract class MaybeEncrypted { keyOrKeychain?: | Keychain | X25519Keypair - | Uint8Array /* Comment: The last is added */ + | Aes256Key /* Comment: The last is added */ ): Promise> | DecryptedThing { throw new Error("Not implemented"); } @@ -93,7 +96,7 @@ export class DecryptedThing extends MaybeEncrypted { async encrypt< Parameters extends EncryptAsymmetricParameters | EncryptSymmetricParameters >( - parameters: EncryptAsymmetricParameters | EncryptSymmetricParameters + parameters: Parameters /* Comment: instead of x25519Keypair: X25519Keypair, ...receiverPublicKeys: (X25519PublicKey | Ed25519PublicKey)[] */ @@ -137,7 +140,7 @@ export class DecryptedThing extends MaybeEncrypted { const enc = new EncryptedThing({ encrypted: new Uint8Array(cipher), nonce, - envelope: new Envelope({ + envelope: new PublicKeyEnvelope({ senderPublicKey: x25519Keypair.publicKey, ks }) @@ -147,7 +150,10 @@ export class DecryptedThing extends MaybeEncrypted { } const enc = new EncryptedSymmetricThing({ encrypted: new Uint8Array(cipher), - nonce + nonce, + envelope: new HashedKeyEnvelope({ + hash: await sha256(parameters.symmetricKey) + }) }); enc._decrypted = this; return enc as EncryptReturnValue; @@ -234,8 +240,10 @@ export class K { } } +abstract class AbstractEnvelope {} + @variant(0) -export class Envelope { +class PublicKeyEnvelope extends AbstractEnvelope { @field({ type: X25519PublicKey }) _senderPublicKey: X25519PublicKey; @@ -243,14 +251,16 @@ export class Envelope { _ks: K[]; constructor(props?: { senderPublicKey: X25519PublicKey; ks: K[] }) { + super(); if (props) { this._senderPublicKey = props.senderPublicKey; this._ks = props.ks; } } - equals(other: Envelope): boolean { - if (other instanceof Envelope) { + // TODO: should this be comparable to AbstractEnvelope? + equals(other: PublicKeyEnvelope): boolean { + if (other instanceof PublicKeyEnvelope) { if (!this._senderPublicKey.equals(other._senderPublicKey)) { return false; } @@ -270,6 +280,31 @@ export class Envelope { } } +@variant(1) +class HashedKeyEnvelope extends AbstractEnvelope { + @field({ type: fixedArray("u8", 32) }) + hash: Uint8Array; + // TODO: Do we need a salt here? + constructor(props?: { hash: Uint8Array }) { + super(); + if (props) { + this.hash = props.hash; + } + } + + // TODO: should this be comparable to AbstractEnvelope? + equals(other: HashedKeyEnvelope): boolean { + if (other instanceof HashedKeyEnvelope) { + if (!equals(this.hash, other.hash)) { + return false; + } + return true; + } else { + return false; + } + } +} + @variant(1) export class EncryptedThing extends MaybeEncrypted { @field({ type: Uint8Array }) @@ -278,13 +313,13 @@ export class EncryptedThing extends MaybeEncrypted { @field({ type: Uint8Array }) _nonce: Uint8Array; - @field({ type: Envelope }) - _envelope: Envelope; + @field({ type: PublicKeyEnvelope }) + _envelope: PublicKeyEnvelope; constructor(props?: { encrypted: Uint8Array; nonce: Uint8Array; - envelope: Envelope; + envelope: PublicKeyEnvelope; }) { super(); if (props) { @@ -421,11 +456,19 @@ export class EncryptedSymmetricThing extends MaybeEncrypted { @field({ type: Uint8Array }) _nonce: Uint8Array; - constructor(props?: { encrypted: Uint8Array; nonce: Uint8Array }) { + @field({ type: HashedKeyEnvelope }) + _envelope: HashedKeyEnvelope; + + constructor(props?: { + encrypted: Uint8Array; + nonce: Uint8Array; + envelope: HashedKeyEnvelope; + }) { super(); if (props) { this._encrypted = props.encrypted; this._nonce = props.nonce; + this._envelope = props.envelope; } } @@ -440,7 +483,7 @@ export class EncryptedSymmetricThing extends MaybeEncrypted { } async decrypt( - keyResolver?: Uint8Array /* Comment: instead of Keychain | X25519Keypair */ + keyResolver?: Aes256Key /* Comment: instead of Keychain | X25519Keypair */ ): Promise> { if (this._decrypted) { return this._decrypted; @@ -449,68 +492,14 @@ export class EncryptedSymmetricThing extends MaybeEncrypted { if (!keyResolver) { throw new AccessError("Expecting key resolver"); } - /* - // We only need to open with one of the keys - let key: { index: number; keypair: X25519Keypair } | undefined; - if (keyResolver instanceof X25519Keypair) { - for (const [i, k] of this._envelope._ks.entries()) { - if (k._receiverPublicKey.equals(keyResolver.publicKey)) { - key = { - index: i, - keypair: keyResolver - }; - } - } - } else { - for (const [i, k] of this._envelope._ks.entries()) { - const exported = await keyResolver.exportByKey(k._receiverPublicKey); - if (exported) { - key = { - index: i, - keypair: exported - }; - break; - } - } - } */ - /* if (key) { - const k = this._envelope._ks[key.index]; - let secretKey: X25519SecretKey = undefined as any; - if (key.keypair instanceof X25519Keypair) { - secretKey = key.keypair.secretKey; - } else { - secretKey = await X25519SecretKey.from(key.keypair); - } - let epheremalKey: Uint8Array; - try { - epheremalKey = sodium.crypto_box_open_easy( - k._encryptedKey.cipher, - k._encryptedKey.nonce, - this._envelope._senderPublicKey.publicKey, - secretKey.secretKey - ); - } catch (error) { - throw new AccessError('Failed to decrypt'); - } */ - - // TODO: is nested decryption necessary? - /* let der: any = this; - let counter = 0; - while (der instanceof EncryptedThing) { - const decrypted = await sodium.crypto_secretbox_open_easy(this._encrypted, this._nonce, epheremalKey); - der = deserialize(decrypted, DecryptedThing) - counter += 1; - if (counter >= 10) { - throw new Error("Unexpected decryption behaviour, data seems to always be in encrypted state") - } - } */ + /* Comment: Should we add a test for the question "Can we decrypt?" */ const der = deserialize( sodium.crypto_secretbox_open_easy( this._encrypted, this._nonce, - keyResolver /* Comment: instead of epheremalKey */ + keyResolver.bytes /* Comment: instead of epheremalKey */ ), DecryptedThing ); @@ -529,10 +518,10 @@ export class EncryptedSymmetricThing extends MaybeEncrypted { if (!equals(this._nonce, other._nonce)) { return false; } - /* + if (!this._envelope.equals(other._envelope)) { return false; - } */ + } return true; } else { return false; From 34179afa366f59dac4e6a43e6bbc2b4dc2d31673 Mon Sep 17 00:00:00 2001 From: Benjamin Preiss Date: Fri, 6 Oct 2023 09:38:37 +0200 Subject: [PATCH 3/5] Add .create function to Aes256 key --- packages/utils/crypto/src/aes256.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/utils/crypto/src/aes256.ts b/packages/utils/crypto/src/aes256.ts index 63f292549..6bd427b68 100644 --- a/packages/utils/crypto/src/aes256.ts +++ b/packages/utils/crypto/src/aes256.ts @@ -3,6 +3,8 @@ import { PlainKey } from "./key"; import { compare } from "@peerbit/uint8arrays"; import { toHexString } from "./utils"; +import sodium from "libsodium-wrappers"; + @variant(0) export class Aes256Key extends PlainKey { @field({ type: fixedArray("u8", 32) }) @@ -16,6 +18,16 @@ export class Aes256Key extends PlainKey { this.key = properties.key; } + static async create(): Promise { + await sodium.ready; + const generated = sodium.crypto_secretbox_keygen(); + const kp = new Aes256Key({ + key: generated + }); + + return kp; + } + equals(other: Aes256Key): boolean { if (other instanceof Aes256Key) { return compare(this.key, other.key) === 0; From e757cdcd8e32918104bd391df4ce1010cf8ad0f7 Mon Sep 17 00:00:00 2001 From: Benjamin Preiss Date: Fri, 6 Oct 2023 10:01:20 +0200 Subject: [PATCH 4/5] Update packages/utils/crypto/src/encryption.ts Co-authored-by: Marcus Pousette --- packages/utils/crypto/src/encryption.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/crypto/src/encryption.ts b/packages/utils/crypto/src/encryption.ts index 1684ea31c..8cce9d61e 100644 --- a/packages/utils/crypto/src/encryption.ts +++ b/packages/utils/crypto/src/encryption.ts @@ -319,7 +319,7 @@ export class EncryptedThing extends MaybeEncrypted { constructor(props?: { encrypted: Uint8Array; nonce: Uint8Array; - envelope: PublicKeyEnvelope; + envelope: PublicKeyEnvelope | HashedKeyEnvelope; }) { super(); if (props) { From d01c1ec41cc9fc8a14244b0aaaa44bcdd001ccb2 Mon Sep 17 00:00:00 2001 From: Benjamin Preiss Date: Fri, 6 Oct 2023 10:01:25 +0200 Subject: [PATCH 5/5] Update packages/utils/crypto/src/encryption.ts Co-authored-by: Marcus Pousette --- packages/utils/crypto/src/encryption.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/utils/crypto/src/encryption.ts b/packages/utils/crypto/src/encryption.ts index 8cce9d61e..272290b90 100644 --- a/packages/utils/crypto/src/encryption.ts +++ b/packages/utils/crypto/src/encryption.ts @@ -313,8 +313,8 @@ export class EncryptedThing extends MaybeEncrypted { @field({ type: Uint8Array }) _nonce: Uint8Array; - @field({ type: PublicKeyEnvelope }) - _envelope: PublicKeyEnvelope; + @field({type: AbstractEnvelope}) + _envelope: PublicKeyEnvelope | HashedKeyEnvelope constructor(props?: { encrypted: Uint8Array;