Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add symmetric capabilities to DecryptedThing #214

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/utils/crypto/src/aes256.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { field, fixedArray, variant } from "@dao-xyz/borsh";
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) })
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;
}

static async create(): Promise<Aes256Key> {
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;
}
return false;
}
toString(): string {
return "aes256/" + toHexString(this.key);
}
}
244 changes: 203 additions & 41 deletions packages/utils/crypto/src/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;

Expand All @@ -27,7 +30,10 @@ export abstract class MaybeEncrypted<T> {
}

decrypt(
keyOrKeychain?: Keychain | X25519Keypair
keyOrKeychain?:
| Keychain
| X25519Keypair
| Aes256Key /* Comment: The last is added */
): Promise<DecryptedThing<T>> | DecryptedThing<T> {
throw new Error("Not implemented");
}
Expand All @@ -45,6 +51,24 @@ export abstract class MaybeEncrypted<T> {
abstract get byteLength(): number;
}

type EncryptAsymmetricParameters = {
x25519Keypair: X25519Keypair;
receiverPublicKeys: (X25519PublicKey | Ed25519PublicKey)[];
};

type EncryptSymmetricParameters = { symmetricKey: Uint8Array };

type EncryptReturnValue<Parameters, T> =
Parameters extends EncryptSymmetricParameters
? EncryptedSymmetricThing<T>
: EncryptedThing<T>;

function isEncryptSymmetricParameters(
parameters: EncryptSymmetricParameters | EncryptAsymmetricParameters
): parameters is EncryptSymmetricParameters {
return (parameters as EncryptSymmetricParameters).symmetricKey !== undefined;
}

@variant(0)
export class DecryptedThing<T> extends MaybeEncrypted<T> {
@field({ type: Uint8Array })
Expand All @@ -69,50 +93,70 @@ export class DecryptedThing<T> extends MaybeEncrypted<T> {
return deserialize(this._data, clazz);
}

async encrypt(
x25519Keypair: X25519Keypair,
...receiverPublicKeys: (X25519PublicKey | Ed25519PublicKey)[]
): Promise<EncryptedThing<T>> {
async encrypt<
Parameters extends EncryptAsymmetricParameters | EncryptSymmetricParameters
>(
parameters: Parameters
/* Comment: instead of
x25519Keypair: X25519Keypair,
...receiverPublicKeys: (X25519PublicKey | Ed25519PublicKey)[] */
): Promise<
EncryptReturnValue<Parameters, T>
> /* Comment: instead of Promise<EncryptedThing<T>> */ {
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<T>({
const enc = new EncryptedThing<T>({
encrypted: new Uint8Array(cipher),
nonce,
envelope: new PublicKeyEnvelope({
senderPublicKey: x25519Keypair.publicKey,
ks
})
});
enc._decrypted = this;
return enc as EncryptReturnValue<Parameters, T>;
}
const enc = new EncryptedSymmetricThing<T>({
encrypted: new Uint8Array(cipher),
nonce,
envelope: new Envelope({
senderPublicKey: x25519Keypair.publicKey,
ks
envelope: new HashedKeyEnvelope({
hash: await sha256(parameters.symmetricKey)
})
});
enc._decrypted = this;
return enc;
return enc as EncryptReturnValue<Parameters, T>;
}

get decrypted(): DecryptedThing<T> {
Expand Down Expand Up @@ -196,23 +240,27 @@ export class K {
}
}

abstract class AbstractEnvelope {}

@variant(0)
export class Envelope {
class PublicKeyEnvelope extends AbstractEnvelope {
@field({ type: X25519PublicKey })
_senderPublicKey: X25519PublicKey;

@field({ type: vec(K) })
_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;
}
Expand All @@ -232,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<T> extends MaybeEncrypted<T> {
@field({ type: Uint8Array })
Expand All @@ -240,13 +313,13 @@ export class EncryptedThing<T> extends MaybeEncrypted<T> {
@field({ type: Uint8Array })
_nonce: Uint8Array;

@field({ type: Envelope })
_envelope: Envelope;
@field({ type: PublicKeyEnvelope })
_envelope: PublicKeyEnvelope;
benjaminpreiss marked this conversation as resolved.
Show resolved Hide resolved

constructor(props?: {
encrypted: Uint8Array;
nonce: Uint8Array;
envelope: Envelope;
envelope: PublicKeyEnvelope;
benjaminpreiss marked this conversation as resolved.
Show resolved Hide resolved
}) {
super();
if (props) {
Expand Down Expand Up @@ -374,3 +447,92 @@ export class EncryptedThing<T> extends MaybeEncrypted<T> {
return this._encrypted.byteLength; // ignore other metdata for now in the size calculation
}
}

@variant(2)
export class EncryptedSymmetricThing<T> extends MaybeEncrypted<T> {
@field({ type: Uint8Array })
_encrypted: Uint8Array;

@field({ type: 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;
}
}

_decrypted?: DecryptedThing<T>;
get decrypted(): DecryptedThing<T> {
if (!this._decrypted) {
throw new Error(
"Entry has not been decrypted, invoke decrypt method before"
);
}
return this._decrypted;
}

async decrypt(
keyResolver?: Aes256Key /* Comment: instead of Keychain | X25519Keypair */
): Promise<DecryptedThing<T>> {
if (this._decrypted) {
return this._decrypted;
}

if (!keyResolver) {
throw new AccessError("Expecting key resolver");
}

/* 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.bytes /* Comment: instead of epheremalKey */
),
DecryptedThing
);
this._decrypted = der as DecryptedThing<T>;
/* } else {
throw new AccessError('Failed to resolve decryption key');
} */
return this._decrypted;
}

equals(other: MaybeEncrypted<T>): 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
}
}