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

Encryption v2 #1729

Merged
merged 21 commits into from
Jul 30, 2024
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

- `ForeignField`-based representation of scalars via `ScalarField` https://github.com/o1-labs/o1js/pull/1705
- Introduced new V2 methods for nullifier operations: `isUnusedV2()`, `assertUnusedV2()`, and `setUsedV2()` https://github.com/o1-labs/o1js/pull/1715
- Added `Encryption.encryptV2()` and `Encryption.decryptV2()` for an updated encryption algorithm that guarantees cipher text integrity.

### Deprecated

- Deprecated `Nullifier.isUnused()`, `Nullifier.assertUnused()`, and `Nullifier.setUsed()` methods https://github.com/o1-labs/o1js/pull/1715
- `createEcdsa`, `createForeignCurve`, `ForeignCurve` and `EcdsaSignature` deprecated in favor of `V2` versions due to a security vulnerability found in the current implementation https://github.com/o1-labs/o1js/pull/1703
- `Encryption.encrypt()` and `Encryption.decrypt()` in favor of `Encryption.encryptV2()` and `Encryption.decryptV2()`

### Fixed

Expand Down
21 changes: 21 additions & 0 deletions src/examples/encryptionv2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
Bytes,
PrivateKey,
initializeBindings,
Encryption,
Encoding,
Provable,
} from 'o1js';

await initializeBindings();

class Bytes32 extends Bytes(32) {}
const priv = PrivateKey.random();
const pub = priv.toPublicKey();

const plainMsg = 'Hello world';
const message = Bytes32.fromString(plainMsg);
console.log('plain message', plainMsg);
const cipher = Encryption.encryptV2(message, pub);
const plainText = Encryption.decryptV2(cipher, priv);
console.log('decrypted message', Buffer.from(plainText.toBytes()).toString());
11 changes: 10 additions & 1 deletion src/lib/provable/bytes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { provableFromClass } from './types/provable-derivers.js';
import type { ProvablePureExtended } from './types/struct.js';
import { assert } from './gadgets/common.js';
import { chunkString } from '../util/arrays.js';
import { chunk, chunkString } from '../util/arrays.js';
import { Provable } from './provable.js';
import { UInt8 } from './int.js';
import { randomBytes } from '../../bindings/crypto/random.js';
Expand Down Expand Up @@ -194,6 +194,15 @@ class Bytes {
return Bytes.from(decodedB64Bytes);
}

/**
* Returns an array of chunks, each of size `size`.
* @param size size of each chunk
* @returns an array of {@link UInt8} chunks
*/
chunk(size: number) {
return chunk(this.bytes, size);
}

// dynamic subclassing infra
static _size?: number;
static _provable?: ProvablePureExtended<
Expand Down
114 changes: 112 additions & 2 deletions src/lib/provable/crypto/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ import { Field, Scalar, Group } from '../wrapped.js';
import { Poseidon } from './poseidon.js';
import { Provable } from '../provable.js';
import { PrivateKey, PublicKey } from './signature.js';
import { bytesToWord, wordToBytes } from '../gadgets/bit-slices.js';
import { Bytes } from '../bytes.js';
import { UInt8 } from '../int.js';

export { encrypt, decrypt };
export { encrypt, decrypt, encryptV2, decryptV2 };

type CipherText = {
publicKey: Group;
cipherText: Field[];
};

/**
* @deprecated Use {@link encryptV2} instead.
* Public Key Encryption, using a given array of {@link Field} elements and encrypts it using a {@link PublicKey}.
*/
function encrypt(message: Field[], otherPublicKey: PublicKey) {
Expand Down Expand Up @@ -40,7 +44,8 @@ function encrypt(message: Field[], otherPublicKey: PublicKey) {
}

/**
* Decrypts a {@link CipherText} using a {@link PrivateKey}.^
* @deprecated Use {@link decryptV2} instead.
* Decrypts a {@link CipherText} using a {@link PrivateKey}.
*/
function decrypt(
{ publicKey, cipherText }: CipherText,
Expand Down Expand Up @@ -68,3 +73,108 @@ function decrypt(

return message;
}

/**
* Decrypts a {@link CipherText} using a {@link PrivateKey}.
*/
function decryptV2(
{
publicKey,
cipherText,
messageLength,
}: CipherText & { messageLength: number },
privateKey: PrivateKey
) {
// key exchange
const sharedSecret = publicKey.scale(privateKey.s);
const sponge = new Poseidon.Sponge();
sponge.absorb(sharedSecret.x);
const authenticationTag = cipherText.pop();

// decryption
const message = [];
for (let i = 0; i < cipherText.length; i++) {
// absorb frame tag
if (i === cipherText.length - 1) sponge.absorb(Field(1));
else sponge.absorb(Field(0));

const keyStream = sponge.squeeze();
const messageChunk = cipherText[i].sub(keyStream);

// convert to bytes
const byteMessage = wordToBytes(messageChunk, 32);

// push the message to our final message array
message.push(byteMessage);

if (i % 2 === 1) sponge.absorb(cipherText[i - 1]);
if (i % 2 === 1 || i === cipherText.length - 1)
sponge.absorb(cipherText[i]);
}

// authentication tag
sponge.squeeze().assertEquals(authenticationTag!);

// calculate padding
const multipleOf = 31;
const n = Math.ceil(messageLength / multipleOf) * multipleOf;

// return the message as a flat array of bytes, slice the padding off of the final message
return Bytes.from(message.flat().slice(0, messageLength - n));
}

/**
* Public Key Encryption, encrypts Bytes using a {@link PublicKey}.
*/
function encryptV2(
message: Bytes,
otherPublicKey: PublicKey
): CipherText & {
messageLength: number;
} {
const bytes = message.bytes;
const messageLength = bytes.length;

// pad message to a multiple of 31 so they still fit into one field element
const multipleOf = 31;
const n = Math.ceil(messageLength / multipleOf) * multipleOf;

// create the padding
const padding = Array.from({ length: n - messageLength }, () =>
UInt8.from(0)
);
message.bytes = bytes.concat(padding);

// convert message into chunks of 31 bytes
const chunks = message.chunk(31);

// key exchange
const privateKey = Provable.witness(Scalar, () => Scalar.random());
const publicKey = Group.generator.scale(privateKey);
const sharedSecret = otherPublicKey.toGroup().scale(privateKey);

const sponge = new Poseidon.Sponge();
sponge.absorb(sharedSecret.x);

// encryption
const cipherText = [];
for (let [n, chunk] of chunks.entries()) {
// absorb frame bit
if (n === chunks.length - 1) sponge.absorb(Field(1));
else sponge.absorb(Field(0));

const keyStream = sponge.squeeze();
const encryptedChunk = bytesToWord(chunk).add(keyStream);
cipherText.push(encryptedChunk);

// absorb for the auth tag (two at a time for saving permutations)
if (n % 2 === 1) sponge.absorb(cipherText[n - 1]);
if (n % 2 === 1 || n === chunks.length - 1) sponge.absorb(cipherText[n]);
}

// authentication tag
const authenticationTag = sponge.squeeze();
cipherText.push(authenticationTag);

return { publicKey, cipherText, messageLength };
}