From 9bf9ababe9773bd2654dbccb7c1d5d4cd77054ba Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 9 Jul 2024 12:10:43 +0200 Subject: [PATCH 01/20] playground --- src/examples/zkprogram/program.ts | 214 +++++++++++++++++++----------- src/lib/provable/bytes.ts | 11 +- 2 files changed, 143 insertions(+), 82 deletions(-) diff --git a/src/examples/zkprogram/program.ts b/src/examples/zkprogram/program.ts index 8ab507a388..a45dd620b9 100644 --- a/src/examples/zkprogram/program.ts +++ b/src/examples/zkprogram/program.ts @@ -1,3 +1,4 @@ +import { zero } from 'dist/node/lib/provable/gates.js'; import { SelfProof, Field, @@ -7,88 +8,139 @@ import { JsonProof, Provable, Empty, + Bytes, + PublicKey, + Group, + Poseidon, + Scalar, + UInt8, + PrivateKey, + initializeBindings, } from 'o1js'; -let MyProgram = ZkProgram({ - name: 'example-with-output', - publicOutput: Field, - - methods: { - baseCase: { - privateInputs: [], - async method() { - return Field(0); - }, - }, - - inductiveCase: { - privateInputs: [SelfProof], - async method(earlierProof: SelfProof) { - earlierProof.verify(); - return earlierProof.publicOutput.add(1); - }, - }, +class Bytes31 extends Bytes(31) {} +const priv = PrivateKey.random(); +const pub = priv.toPublicKey(); + +const arr = Array.from({ length: 31 }).fill(255); + +const message = Bytes31.from(arr); + +function bytesToWord(wordBytes: UInt8[]): Field { + return wordBytes.reduce((acc, byte, idx) => { + const shift = 1n << BigInt(8 * idx); + return acc.add(byte.value.mul(shift)); + }, Field.from(0)); +} + +function wordToBytes(word: Field, bytesPerWord = 8): UInt8[] { + let bytes = Provable.witness(Provable.Array(UInt8, bytesPerWord), () => { + let w = word.toBigInt(); + return Array.from({ length: bytesPerWord }, (_, k) => + UInt8.from((w >> BigInt(8 * k)) & 0xffn) + ); + }); + Provable.log(bytes); + // check decomposition + // bytesToWord(bytes).assertEquals(word); + + return bytes; +} + +const { cipherText, publicKey } = await encrypt(message, pub); +let res = await decrypt( + { + publicKey, + cipherText, }, -}); -// type sanity checks -MyProgram.publicInputType satisfies Provable; -MyProgram.publicOutputType satisfies typeof Field; - -let MyProof = ZkProgram.Proof(MyProgram); - -console.log('program digest', MyProgram.digest()); - -console.log('compiling MyProgram...'); -let { verificationKey } = await MyProgram.compile(); -console.log('verification key', verificationKey.data.slice(0, 10) + '..'); - -console.log('proving base case...'); -let proof = await MyProgram.baseCase(); -proof = await testJsonRoundtrip(MyProof, proof); - -// type sanity check -proof satisfies Proof; - -console.log('verify...'); -let ok = await verify(proof.toJSON(), verificationKey); -console.log('ok?', ok); - -console.log('verify alternative...'); -ok = await MyProgram.verify(proof); -console.log('ok (alternative)?', ok); - -console.log('proving step 1...'); -proof = await MyProgram.inductiveCase(proof); -proof = await testJsonRoundtrip(MyProof, proof); - -console.log('verify...'); -ok = await verify(proof, verificationKey); -console.log('ok?', ok); - -console.log('verify alternative...'); -ok = await MyProgram.verify(proof); -console.log('ok (alternative)?', ok); - -console.log('proving step 2...'); -proof = await MyProgram.inductiveCase(proof); -proof = await testJsonRoundtrip(MyProof, proof); - -console.log('verify...'); -ok = await verify(proof.toJSON(), verificationKey); - -console.log('ok?', ok && proof.publicOutput.toString() === '2'); - -function testJsonRoundtrip< - P extends Proof, - MyProof extends { fromJSON(jsonProof: JsonProof): Promise

} ->(MyProof: MyProof, proof: P) { - let jsonProof = proof.toJSON(); - console.log( - 'json proof', - JSON.stringify({ - ...jsonProof, - proof: jsonProof.proof.slice(0, 10) + '..', - }) - ); - return MyProof.fromJSON(jsonProof); + priv +); +bytesToWord(message.bytes).assertEquals(res[0]); + +async function encrypt(message: Bytes, otherPublicKey: PublicKey) { + // pad message to a multiple of 31 so that we can then later append a frame bit to the message + const bytes = message.bytes; + const multipleOf = 31; + let n = Math.ceil(bytes.length / multipleOf) * multipleOf; + let padding = Array.from({ length: n - bytes.length }, () => UInt8.from(0)); + + message.bytes = bytes.concat(padding); + + // convert message into chunks of 31 bytes + const chunks = message.chunk(31); + + // key exchange + let privateKey = Provable.witness(Scalar, () => Scalar.random()); + let publicKey = Group.generator.scale(privateKey); + let sharedSecret = otherPublicKey.toGroup().scale(privateKey); + + await initializeBindings(); + let sponge = new Poseidon.Sponge(); + sponge.absorb(sharedSecret.x); + + // frame bits + const zeroBit = [UInt8.from(0)]; + const oneBit = [UInt8.from(1)]; + + // encryption + let cipherText = []; + for (let [n, chunk] of chunks.entries()) { + if (n === chunks.length - 1) { + // attach the one frame bit if its the last chunk + chunk = chunk.concat(oneBit); + } else { + // pad with zero frame bit + chunk = chunk.concat(zeroBit); + } + console.log('with bit', bytesToWord(chunk).toString()); + + let keyStream = sponge.squeeze(); + let 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 + let authenticationTag = sponge.squeeze(); + cipherText.push(authenticationTag); + + return { publicKey, cipherText }; +} + +async function decrypt( + { publicKey, cipherText }: { publicKey: Group; cipherText: Field[] }, + privateKey: PrivateKey +) { + // key exchange + let sharedSecret = publicKey.scale(privateKey.s); + await initializeBindings(); + let sponge = new Poseidon.Sponge(); + sponge.absorb(sharedSecret.x); + let authenticationTag = cipherText.pop(); + + // decryption + let message = []; + for (let i = 0; i < cipherText.length; i++) { + let keyStream = sponge.squeeze(); + let messageChunk = cipherText[i].sub(keyStream); + + const withFrameBit = wordToBytes(messageChunk, 32); + const frameBit = withFrameBit.pop()!; + + if (i === cipherText.length - 1) frameBit.assertEquals(1); + else frameBit.assertEquals(0); + + message.push(bytesToWord(withFrameBit)); + + 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!); + + return message; } diff --git a/src/lib/provable/bytes.ts b/src/lib/provable/bytes.ts index 645a2d7f6e..ac1490a621 100644 --- a/src/lib/provable/bytes.ts +++ b/src/lib/provable/bytes.ts @@ -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'; @@ -194,6 +194,15 @@ class Bytes { return Bytes.from(decodedB64Bytes); } + /** + * Returns chunk of size `size` of this bytes array. + * @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< From 1af83d4662363b3b93845ae8341758f3dddcc410 Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 10 Jul 2024 09:43:48 +0200 Subject: [PATCH 02/20] add v2 encryption --- src/lib/provable/crypto/encryption.ts | 93 ++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/src/lib/provable/crypto/encryption.ts b/src/lib/provable/crypto/encryption.ts index 8c025886ab..6640dd5bc7 100644 --- a/src/lib/provable/crypto/encryption.ts +++ b/src/lib/provable/crypto/encryption.ts @@ -2,8 +2,11 @@ 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; @@ -68,3 +71,91 @@ function decrypt( return message; } + +// v2 + +function decryptV2( + { publicKey, cipherText }: CipherText, + privateKey: PrivateKey +) { + // key exchange + let sharedSecret = publicKey.scale(privateKey.s); + let sponge = new Poseidon.Sponge(); + sponge.absorb(sharedSecret.x); + let authenticationTag = cipherText.pop(); + + // decryption + let message = []; + for (let i = 0; i < cipherText.length; i++) { + let keyStream = sponge.squeeze(); + let messageChunk = cipherText[i].sub(keyStream); + + const withFrameBit = wordToBytes(messageChunk, 32); + const frameBit = withFrameBit.pop()!; + + if (i === cipherText.length - 1) frameBit.assertEquals(1); + else frameBit.assertEquals(0); + + message.push(bytesToWord(withFrameBit)); + + 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!); + + return message; +} + +function encryptV2(message: Bytes, otherPublicKey: PublicKey): CipherText { + // pad message to a multiple of 31 so that we can then later append a frame bit to the message + const bytes = message.bytes; + const multipleOf = 31; + let n = Math.ceil(bytes.length / multipleOf) * multipleOf; + let padding = Array.from({ length: n - bytes.length }, () => UInt8.from(0)); + + message.bytes = bytes.concat(padding); + + // convert message into chunks of 31 bytes + const chunks = message.chunk(31); + + // key exchange + let privateKey = Provable.witness(Scalar, () => Scalar.random()); + let publicKey = Group.generator.scale(privateKey); + let sharedSecret = otherPublicKey.toGroup().scale(privateKey); + + let sponge = new Poseidon.Sponge(); + sponge.absorb(sharedSecret.x); + + // frame bits + const zeroBit = [UInt8.from(0)]; + const oneBit = [UInt8.from(1)]; + + // encryption + let cipherText = []; + for (let [n, chunk] of chunks.entries()) { + if (n === chunks.length - 1) { + // attach the one frame bit if its the last chunk + chunk = chunk.concat(oneBit); + } else { + // pad with zero frame bit + chunk = chunk.concat(zeroBit); + } + console.log('with bit', bytesToWord(chunk).toString()); + + let keyStream = sponge.squeeze(); + let 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 + let authenticationTag = sponge.squeeze(); + cipherText.push(authenticationTag); + + return { publicKey, cipherText }; +} From 4ce5714e1b4257362b8ae09f4269eec4876aec9c Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 10 Jul 2024 10:15:32 +0200 Subject: [PATCH 03/20] example and v2 --- src/examples/encryptionv2.ts | 25 +++ src/examples/zkprogram/program.ts | 214 ++++++++++---------------- src/lib/provable/crypto/encryption.ts | 34 ++-- 3 files changed, 126 insertions(+), 147 deletions(-) create mode 100644 src/examples/encryptionv2.ts diff --git a/src/examples/encryptionv2.ts b/src/examples/encryptionv2.ts new file mode 100644 index 0000000000..0ad60ad87d --- /dev/null +++ b/src/examples/encryptionv2.ts @@ -0,0 +1,25 @@ +import { + Bytes, + PrivateKey, + initializeBindings, + Encryption, + Encoding, + Provable, +} from 'o1js'; + +await initializeBindings(); + +class Bytes32 extends Bytes(31) {} +const priv = PrivateKey.random(); +const pub = priv.toPublicKey(); + +const plainMsg = 'Hello world'; +const message = Bytes.fromString(plainMsg); +console.log('plain message', plainMsg); +const cipher = Encryption.encryptV2(Bytes32.from(message), pub); +const plainText = Encryption.decryptV2(cipher, priv); + +console.log( + 'decrypted message', + Buffer.from(Bytes.from(plainText).toBytes()).toString() +); diff --git a/src/examples/zkprogram/program.ts b/src/examples/zkprogram/program.ts index a45dd620b9..8ab507a388 100644 --- a/src/examples/zkprogram/program.ts +++ b/src/examples/zkprogram/program.ts @@ -1,4 +1,3 @@ -import { zero } from 'dist/node/lib/provable/gates.js'; import { SelfProof, Field, @@ -8,139 +7,88 @@ import { JsonProof, Provable, Empty, - Bytes, - PublicKey, - Group, - Poseidon, - Scalar, - UInt8, - PrivateKey, - initializeBindings, } from 'o1js'; -class Bytes31 extends Bytes(31) {} -const priv = PrivateKey.random(); -const pub = priv.toPublicKey(); - -const arr = Array.from({ length: 31 }).fill(255); - -const message = Bytes31.from(arr); - -function bytesToWord(wordBytes: UInt8[]): Field { - return wordBytes.reduce((acc, byte, idx) => { - const shift = 1n << BigInt(8 * idx); - return acc.add(byte.value.mul(shift)); - }, Field.from(0)); -} - -function wordToBytes(word: Field, bytesPerWord = 8): UInt8[] { - let bytes = Provable.witness(Provable.Array(UInt8, bytesPerWord), () => { - let w = word.toBigInt(); - return Array.from({ length: bytesPerWord }, (_, k) => - UInt8.from((w >> BigInt(8 * k)) & 0xffn) - ); - }); - Provable.log(bytes); - // check decomposition - // bytesToWord(bytes).assertEquals(word); - - return bytes; -} - -const { cipherText, publicKey } = await encrypt(message, pub); -let res = await decrypt( - { - publicKey, - cipherText, +let MyProgram = ZkProgram({ + name: 'example-with-output', + publicOutput: Field, + + methods: { + baseCase: { + privateInputs: [], + async method() { + return Field(0); + }, + }, + + inductiveCase: { + privateInputs: [SelfProof], + async method(earlierProof: SelfProof) { + earlierProof.verify(); + return earlierProof.publicOutput.add(1); + }, + }, }, - priv -); -bytesToWord(message.bytes).assertEquals(res[0]); - -async function encrypt(message: Bytes, otherPublicKey: PublicKey) { - // pad message to a multiple of 31 so that we can then later append a frame bit to the message - const bytes = message.bytes; - const multipleOf = 31; - let n = Math.ceil(bytes.length / multipleOf) * multipleOf; - let padding = Array.from({ length: n - bytes.length }, () => UInt8.from(0)); - - message.bytes = bytes.concat(padding); - - // convert message into chunks of 31 bytes - const chunks = message.chunk(31); - - // key exchange - let privateKey = Provable.witness(Scalar, () => Scalar.random()); - let publicKey = Group.generator.scale(privateKey); - let sharedSecret = otherPublicKey.toGroup().scale(privateKey); - - await initializeBindings(); - let sponge = new Poseidon.Sponge(); - sponge.absorb(sharedSecret.x); - - // frame bits - const zeroBit = [UInt8.from(0)]; - const oneBit = [UInt8.from(1)]; - - // encryption - let cipherText = []; - for (let [n, chunk] of chunks.entries()) { - if (n === chunks.length - 1) { - // attach the one frame bit if its the last chunk - chunk = chunk.concat(oneBit); - } else { - // pad with zero frame bit - chunk = chunk.concat(zeroBit); - } - console.log('with bit', bytesToWord(chunk).toString()); - - let keyStream = sponge.squeeze(); - let 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 - let authenticationTag = sponge.squeeze(); - cipherText.push(authenticationTag); - - return { publicKey, cipherText }; -} - -async function decrypt( - { publicKey, cipherText }: { publicKey: Group; cipherText: Field[] }, - privateKey: PrivateKey -) { - // key exchange - let sharedSecret = publicKey.scale(privateKey.s); - await initializeBindings(); - let sponge = new Poseidon.Sponge(); - sponge.absorb(sharedSecret.x); - let authenticationTag = cipherText.pop(); - - // decryption - let message = []; - for (let i = 0; i < cipherText.length; i++) { - let keyStream = sponge.squeeze(); - let messageChunk = cipherText[i].sub(keyStream); - - const withFrameBit = wordToBytes(messageChunk, 32); - const frameBit = withFrameBit.pop()!; - - if (i === cipherText.length - 1) frameBit.assertEquals(1); - else frameBit.assertEquals(0); - - message.push(bytesToWord(withFrameBit)); - - 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!); - - return message; +}); +// type sanity checks +MyProgram.publicInputType satisfies Provable; +MyProgram.publicOutputType satisfies typeof Field; + +let MyProof = ZkProgram.Proof(MyProgram); + +console.log('program digest', MyProgram.digest()); + +console.log('compiling MyProgram...'); +let { verificationKey } = await MyProgram.compile(); +console.log('verification key', verificationKey.data.slice(0, 10) + '..'); + +console.log('proving base case...'); +let proof = await MyProgram.baseCase(); +proof = await testJsonRoundtrip(MyProof, proof); + +// type sanity check +proof satisfies Proof; + +console.log('verify...'); +let ok = await verify(proof.toJSON(), verificationKey); +console.log('ok?', ok); + +console.log('verify alternative...'); +ok = await MyProgram.verify(proof); +console.log('ok (alternative)?', ok); + +console.log('proving step 1...'); +proof = await MyProgram.inductiveCase(proof); +proof = await testJsonRoundtrip(MyProof, proof); + +console.log('verify...'); +ok = await verify(proof, verificationKey); +console.log('ok?', ok); + +console.log('verify alternative...'); +ok = await MyProgram.verify(proof); +console.log('ok (alternative)?', ok); + +console.log('proving step 2...'); +proof = await MyProgram.inductiveCase(proof); +proof = await testJsonRoundtrip(MyProof, proof); + +console.log('verify...'); +ok = await verify(proof.toJSON(), verificationKey); + +console.log('ok?', ok && proof.publicOutput.toString() === '2'); + +function testJsonRoundtrip< + P extends Proof, + MyProof extends { fromJSON(jsonProof: JsonProof): Promise

} +>(MyProof: MyProof, proof: P) { + let jsonProof = proof.toJSON(); + console.log( + 'json proof', + JSON.stringify({ + ...jsonProof, + proof: jsonProof.proof.slice(0, 10) + '..', + }) + ); + return MyProof.fromJSON(jsonProof); } diff --git a/src/lib/provable/crypto/encryption.ts b/src/lib/provable/crypto/encryption.ts index 6640dd5bc7..5a23f8e6cf 100644 --- a/src/lib/provable/crypto/encryption.ts +++ b/src/lib/provable/crypto/encryption.ts @@ -14,6 +14,7 @@ type CipherText = { }; /** + * @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) { @@ -43,6 +44,7 @@ function encrypt(message: Field[], otherPublicKey: PublicKey) { } /** + * @deprecated Use {@link decryptV2} instead. * Decrypts a {@link CipherText} using a {@link PrivateKey}.^ */ function decrypt( @@ -90,31 +92,40 @@ function decryptV2( let keyStream = sponge.squeeze(); let messageChunk = cipherText[i].sub(keyStream); - const withFrameBit = wordToBytes(messageChunk, 32); - const frameBit = withFrameBit.pop()!; + // convert to bytes + const byteMessage = wordToBytes(messageChunk, 32); + // pop frame bit + const frameBit = byteMessage.pop()!; + + // check frame bit - if last element of the cipher text, frame bit must equal 1 + // otherwise 0 if (i === cipherText.length - 1) frameBit.assertEquals(1); else frameBit.assertEquals(0); - - message.push(bytesToWord(withFrameBit)); + // 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!); - return message; + // return the message as a flat array of bytes + return message.flat(); } function encryptV2(message: Bytes, otherPublicKey: PublicKey): CipherText { - // pad message to a multiple of 31 so that we can then later append a frame bit to the message const bytes = message.bytes; + + // pad message to a multiple of 31 so that we can then later append a frame bit to the message const multipleOf = 31; let n = Math.ceil(bytes.length / multipleOf) * multipleOf; - let padding = Array.from({ length: n - bytes.length }, () => UInt8.from(0)); + // create the padding + let padding = Array.from({ length: n - bytes.length }, () => UInt8.from(0)); message.bytes = bytes.concat(padding); // convert message into chunks of 31 bytes @@ -128,21 +139,16 @@ function encryptV2(message: Bytes, otherPublicKey: PublicKey): CipherText { let sponge = new Poseidon.Sponge(); sponge.absorb(sharedSecret.x); - // frame bits - const zeroBit = [UInt8.from(0)]; - const oneBit = [UInt8.from(1)]; - // encryption let cipherText = []; for (let [n, chunk] of chunks.entries()) { if (n === chunks.length - 1) { // attach the one frame bit if its the last chunk - chunk = chunk.concat(oneBit); + chunk = chunk.concat(UInt8.from(1)); } else { // pad with zero frame bit - chunk = chunk.concat(zeroBit); + chunk = chunk.concat(UInt8.from(0)); } - console.log('with bit', bytesToWord(chunk).toString()); let keyStream = sponge.squeeze(); let encryptedChunk = bytesToWord(chunk).add(keyStream); From b990854dc0306d5ea7c014072b2b72424917ef69 Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 10 Jul 2024 17:17:41 +0200 Subject: [PATCH 04/20] Update encryption.ts --- src/lib/provable/crypto/encryption.ts | 29 +++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/lib/provable/crypto/encryption.ts b/src/lib/provable/crypto/encryption.ts index 5a23f8e6cf..0f13422f7c 100644 --- a/src/lib/provable/crypto/encryption.ts +++ b/src/lib/provable/crypto/encryption.ts @@ -77,7 +77,11 @@ function decrypt( // v2 function decryptV2( - { publicKey, cipherText }: CipherText, + { + publicKey, + cipherText, + messageLength, + }: CipherText & { messageLength: number }, privateKey: PrivateKey ) { // key exchange @@ -113,19 +117,28 @@ function decryptV2( // authentication tag sponge.squeeze().assertEquals(authenticationTag!); - // return the message as a flat array of bytes - return message.flat(); + // calculate padding + const multipleOf = 31; + let n = Math.ceil(messageLength / multipleOf) * multipleOf; + + // return the message as a flat array of bytes, slice the padding off of the final message + return message.flat().slice(0, messageLength - n); } -function encryptV2(message: Bytes, otherPublicKey: PublicKey): CipherText { +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 that we can then later append a frame bit to the message const multipleOf = 31; - let n = Math.ceil(bytes.length / multipleOf) * multipleOf; + let n = Math.ceil(messageLength / multipleOf) * multipleOf; // create the padding - let padding = Array.from({ length: n - bytes.length }, () => UInt8.from(0)); + let padding = Array.from({ length: n - messageLength }, () => UInt8.from(0)); message.bytes = bytes.concat(padding); // convert message into chunks of 31 bytes @@ -163,5 +176,5 @@ function encryptV2(message: Bytes, otherPublicKey: PublicKey): CipherText { let authenticationTag = sponge.squeeze(); cipherText.push(authenticationTag); - return { publicKey, cipherText }; + return { publicKey, cipherText, messageLength }; } From a8bfbf2f2bbf10e4e3057e96743526df5c7d404c Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 10 Jul 2024 17:20:31 +0200 Subject: [PATCH 05/20] update exampel --- src/examples/encryptionv2.ts | 13 ++++++------- src/lib/provable/crypto/encryption.ts | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/examples/encryptionv2.ts b/src/examples/encryptionv2.ts index 0ad60ad87d..f501d075b3 100644 --- a/src/examples/encryptionv2.ts +++ b/src/examples/encryptionv2.ts @@ -9,17 +9,16 @@ import { await initializeBindings(); -class Bytes32 extends Bytes(31) {} +class Bytes32 extends Bytes(32) {} const priv = PrivateKey.random(); const pub = priv.toPublicKey(); const plainMsg = 'Hello world'; -const message = Bytes.fromString(plainMsg); +const message = Bytes32.fromString(plainMsg); console.log('plain message', plainMsg); -const cipher = Encryption.encryptV2(Bytes32.from(message), pub); +const cipher = Encryption.encryptV2(message, pub); const plainText = Encryption.decryptV2(cipher, priv); -console.log( - 'decrypted message', - Buffer.from(Bytes.from(plainText).toBytes()).toString() -); +Provable.log(plainText.length); + +console.log('decrypted message', Buffer.from(plainText.toBytes()).toString()); diff --git a/src/lib/provable/crypto/encryption.ts b/src/lib/provable/crypto/encryption.ts index 0f13422f7c..f3837e473b 100644 --- a/src/lib/provable/crypto/encryption.ts +++ b/src/lib/provable/crypto/encryption.ts @@ -122,7 +122,7 @@ function decryptV2( let n = Math.ceil(messageLength / multipleOf) * multipleOf; // return the message as a flat array of bytes, slice the padding off of the final message - return message.flat().slice(0, messageLength - n); + return Bytes.from(message.flat().slice(0, messageLength - n)); } function encryptV2( From d7bab6fd891b927392968520bc4e40c401963a25 Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 10 Jul 2024 17:26:11 +0200 Subject: [PATCH 06/20] clean up --- src/examples/encryptionv2.ts | 3 -- src/lib/provable/crypto/encryption.ts | 50 +++++++++++++-------------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/examples/encryptionv2.ts b/src/examples/encryptionv2.ts index f501d075b3..5facc72897 100644 --- a/src/examples/encryptionv2.ts +++ b/src/examples/encryptionv2.ts @@ -18,7 +18,4 @@ const message = Bytes32.fromString(plainMsg); console.log('plain message', plainMsg); const cipher = Encryption.encryptV2(message, pub); const plainText = Encryption.decryptV2(cipher, priv); - -Provable.log(plainText.length); - console.log('decrypted message', Buffer.from(plainText.toBytes()).toString()); diff --git a/src/lib/provable/crypto/encryption.ts b/src/lib/provable/crypto/encryption.ts index f3837e473b..8bc1d222b8 100644 --- a/src/lib/provable/crypto/encryption.ts +++ b/src/lib/provable/crypto/encryption.ts @@ -85,16 +85,16 @@ function decryptV2( privateKey: PrivateKey ) { // key exchange - let sharedSecret = publicKey.scale(privateKey.s); - let sponge = new Poseidon.Sponge(); + const sharedSecret = publicKey.scale(privateKey.s); + const sponge = new Poseidon.Sponge(); sponge.absorb(sharedSecret.x); - let authenticationTag = cipherText.pop(); + const authenticationTag = cipherText.pop(); // decryption - let message = []; + const message = []; for (let i = 0; i < cipherText.length; i++) { - let keyStream = sponge.squeeze(); - let messageChunk = cipherText[i].sub(keyStream); + const keyStream = sponge.squeeze(); + const messageChunk = cipherText[i].sub(keyStream); // convert to bytes const byteMessage = wordToBytes(messageChunk, 32); @@ -119,7 +119,7 @@ function decryptV2( // calculate padding const multipleOf = 31; - let n = Math.ceil(messageLength / multipleOf) * multipleOf; + 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)); @@ -133,38 +133,38 @@ function encryptV2( } { const bytes = message.bytes; const messageLength = bytes.length; - // pad message to a multiple of 31 so that we can then later append a frame bit to the message + + // pad message to a multiple of 31 so that we can append a frame bit to the message const multipleOf = 31; - let n = Math.ceil(messageLength / multipleOf) * multipleOf; + const n = Math.ceil(messageLength / multipleOf) * multipleOf; // create the padding - let padding = Array.from({ length: n - messageLength }, () => UInt8.from(0)); + 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 - let privateKey = Provable.witness(Scalar, () => Scalar.random()); - let publicKey = Group.generator.scale(privateKey); - let sharedSecret = otherPublicKey.toGroup().scale(privateKey); + const privateKey = Provable.witness(Scalar, () => Scalar.random()); + const publicKey = Group.generator.scale(privateKey); + const sharedSecret = otherPublicKey.toGroup().scale(privateKey); - let sponge = new Poseidon.Sponge(); + const sponge = new Poseidon.Sponge(); sponge.absorb(sharedSecret.x); // encryption - let cipherText = []; + const cipherText = []; for (let [n, chunk] of chunks.entries()) { - if (n === chunks.length - 1) { - // attach the one frame bit if its the last chunk - chunk = chunk.concat(UInt8.from(1)); - } else { - // pad with zero frame bit - chunk = chunk.concat(UInt8.from(0)); - } + // attach frame bit if its the last chunk + // pad with zero frame bit if its any other chunk + if (n === chunks.length - 1) chunk = chunk.concat(UInt8.from(1)); + else chunk = chunk.concat(UInt8.from(0)); - let keyStream = sponge.squeeze(); - let encryptedChunk = bytesToWord(chunk).add(keyStream); + 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) @@ -173,7 +173,7 @@ function encryptV2( } // authentication tag - let authenticationTag = sponge.squeeze(); + const authenticationTag = sponge.squeeze(); cipherText.push(authenticationTag); return { publicKey, cipherText, messageLength }; From 7bcbebc8cd2517d456fdbe698f2d38a8e2d15e30 Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 10 Jul 2024 17:27:22 +0200 Subject: [PATCH 07/20] clean up --- src/lib/provable/crypto/encryption.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/provable/crypto/encryption.ts b/src/lib/provable/crypto/encryption.ts index 8bc1d222b8..6e135f917f 100644 --- a/src/lib/provable/crypto/encryption.ts +++ b/src/lib/provable/crypto/encryption.ts @@ -45,7 +45,7 @@ function encrypt(message: Field[], otherPublicKey: PublicKey) { /** * @deprecated Use {@link decryptV2} instead. - * Decrypts a {@link CipherText} using a {@link PrivateKey}.^ + * Decrypts a {@link CipherText} using a {@link PrivateKey}. */ function decrypt( { publicKey, cipherText }: CipherText, @@ -74,8 +74,9 @@ function decrypt( return message; } -// v2 - +/** + * Decrypts a {@link CipherText} using a {@link PrivateKey}. + */ function decryptV2( { publicKey, @@ -125,6 +126,9 @@ function decryptV2( return Bytes.from(message.flat().slice(0, messageLength - n)); } +/** + * Public Key Encryption, encrypts Bytes using a {@link PublicKey}. + */ function encryptV2( message: Bytes, otherPublicKey: PublicKey From 400eb7dc9530b2091f8cd733b53595144c7ba55c Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 10 Jul 2024 17:29:53 +0200 Subject: [PATCH 08/20] Update bytes.ts --- src/lib/provable/bytes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/provable/bytes.ts b/src/lib/provable/bytes.ts index ac1490a621..3a9717ed6c 100644 --- a/src/lib/provable/bytes.ts +++ b/src/lib/provable/bytes.ts @@ -195,7 +195,7 @@ class Bytes { } /** - * Returns chunk of size `size` of this bytes array. + * Returns an array of chunks, each of size `size`. * @param size size of each chunk * @returns an array of {@link UInt8} chunks */ From 45849140226163d3d0ff8b530cf2c7c3a1cc8ff4 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 23 Jul 2024 11:58:37 +0200 Subject: [PATCH 09/20] absorb frame bit --- src/lib/provable/crypto/encryption.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/lib/provable/crypto/encryption.ts b/src/lib/provable/crypto/encryption.ts index 6e135f917f..eeb9f88eed 100644 --- a/src/lib/provable/crypto/encryption.ts +++ b/src/lib/provable/crypto/encryption.ts @@ -94,19 +94,16 @@ function decryptV2( // decryption const message = []; for (let i = 0; i < cipherText.length; i++) { + // frame bit + 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); - // pop frame bit - const frameBit = byteMessage.pop()!; - - // check frame bit - if last element of the cipher text, frame bit must equal 1 - // otherwise 0 - if (i === cipherText.length - 1) frameBit.assertEquals(1); - else frameBit.assertEquals(0); // push the message to our final message array message.push(byteMessage); @@ -164,8 +161,8 @@ function encryptV2( for (let [n, chunk] of chunks.entries()) { // attach frame bit if its the last chunk // pad with zero frame bit if its any other chunk - if (n === chunks.length - 1) chunk = chunk.concat(UInt8.from(1)); - else chunk = chunk.concat(UInt8.from(0)); + if (n === chunks.length - 1) sponge.absorb(Field(1)); + else sponge.absorb(Field(0)); const keyStream = sponge.squeeze(); const encryptedChunk = bytesToWord(chunk).add(keyStream); From 213879d6feacee8156be0e80fdf3ab25c52c5f12 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 23 Jul 2024 12:03:55 +0200 Subject: [PATCH 10/20] comment --- src/lib/provable/crypto/encryption.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib/provable/crypto/encryption.ts b/src/lib/provable/crypto/encryption.ts index eeb9f88eed..03e8cf80bd 100644 --- a/src/lib/provable/crypto/encryption.ts +++ b/src/lib/provable/crypto/encryption.ts @@ -94,7 +94,7 @@ function decryptV2( // decryption const message = []; for (let i = 0; i < cipherText.length; i++) { - // frame bit + // absorb frame tag if (i === cipherText.length - 1) sponge.absorb(Field(1)); else sponge.absorb(Field(0)); @@ -159,8 +159,7 @@ function encryptV2( // encryption const cipherText = []; for (let [n, chunk] of chunks.entries()) { - // attach frame bit if its the last chunk - // pad with zero frame bit if its any other chunk + // absorb frame tag if (n === chunks.length - 1) sponge.absorb(Field(1)); else sponge.absorb(Field(0)); From 8edf5d9dce5b35d4d7a810f3032a6eb56a848f93 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 23 Jul 2024 12:05:49 +0200 Subject: [PATCH 11/20] doc comment --- src/lib/provable/crypto/encryption.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/provable/crypto/encryption.ts b/src/lib/provable/crypto/encryption.ts index 03e8cf80bd..054263d41e 100644 --- a/src/lib/provable/crypto/encryption.ts +++ b/src/lib/provable/crypto/encryption.ts @@ -159,7 +159,7 @@ function encryptV2( // encryption const cipherText = []; for (let [n, chunk] of chunks.entries()) { - // absorb frame tag + // absorb frame bit if (n === chunks.length - 1) sponge.absorb(Field(1)); else sponge.absorb(Field(0)); From 7835237b2ebc290969945cf81724985bc02b50e8 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 23 Jul 2024 12:26:52 +0200 Subject: [PATCH 12/20] Update encryption.ts --- src/lib/provable/crypto/encryption.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/provable/crypto/encryption.ts b/src/lib/provable/crypto/encryption.ts index 054263d41e..734d059b25 100644 --- a/src/lib/provable/crypto/encryption.ts +++ b/src/lib/provable/crypto/encryption.ts @@ -135,7 +135,7 @@ function encryptV2( const bytes = message.bytes; const messageLength = bytes.length; - // pad message to a multiple of 31 so that we can append a frame bit to the message + // 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; From b6b163c075c648684fe546466c67267e056e3c2b Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 29 Jul 2024 10:07:02 +0200 Subject: [PATCH 13/20] changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d4d55df0a..473f2555ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From 2eff375fbb33765d75463a183146a73657967391 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 30 Jul 2024 11:56:05 +0200 Subject: [PATCH 14/20] dont mutate input --- src/lib/provable/crypto/encryption.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/provable/crypto/encryption.ts b/src/lib/provable/crypto/encryption.ts index 734d059b25..29bce01442 100644 --- a/src/lib/provable/crypto/encryption.ts +++ b/src/lib/provable/crypto/encryption.ts @@ -5,6 +5,7 @@ import { PrivateKey, PublicKey } from './signature.js'; import { bytesToWord, wordToBytes } from '../gadgets/bit-slices.js'; import { Bytes } from '../bytes.js'; import { UInt8 } from '../int.js'; +import { chunk } from '../../util/arrays.js'; export { encrypt, decrypt, encryptV2, decryptV2 }; @@ -102,7 +103,7 @@ function decryptV2( const messageChunk = cipherText[i].sub(keyStream); // convert to bytes - const byteMessage = wordToBytes(messageChunk, 32); + const byteMessage = wordToBytes(messageChunk, 31); // push the message to our final message array message.push(byteMessage); @@ -143,10 +144,9 @@ function encryptV2( 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); + const chunks = chunk(bytes.concat(padding), 31); // key exchange const privateKey = Provable.witness(Scalar, () => Scalar.random()); From bbc9200dad92a35d9dc2d1fdb4fe61838f51ed25 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 30 Jul 2024 12:00:40 +0200 Subject: [PATCH 15/20] remove additional absorb step --- src/lib/provable/crypto/encryption.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/lib/provable/crypto/encryption.ts b/src/lib/provable/crypto/encryption.ts index 29bce01442..c504cc1cea 100644 --- a/src/lib/provable/crypto/encryption.ts +++ b/src/lib/provable/crypto/encryption.ts @@ -108,9 +108,7 @@ function decryptV2( // 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]); + sponge.absorb(cipherText[i]); } // authentication tag @@ -167,9 +165,7 @@ function encryptV2( 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]); + sponge.absorb(encryptedChunk); } // authentication tag From 1e6f808997824010acff99cff132c6399702cc2f Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 30 Jul 2024 12:01:34 +0200 Subject: [PATCH 16/20] larger message, tests tbd --- src/examples/encryptionv2.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/examples/encryptionv2.ts b/src/examples/encryptionv2.ts index 5facc72897..61d01dae4e 100644 --- a/src/examples/encryptionv2.ts +++ b/src/examples/encryptionv2.ts @@ -9,12 +9,12 @@ import { await initializeBindings(); -class Bytes32 extends Bytes(32) {} +class Bytes256 extends Bytes(256) {} const priv = PrivateKey.random(); const pub = priv.toPublicKey(); const plainMsg = 'Hello world'; -const message = Bytes32.fromString(plainMsg); +const message = Bytes256.fromString(plainMsg); console.log('plain message', plainMsg); const cipher = Encryption.encryptV2(message, pub); const plainText = Encryption.decryptV2(cipher, priv); From 9e3fc1945bef8b42e706dcbc9e63a38073d9f3c7 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 30 Jul 2024 12:22:05 +0200 Subject: [PATCH 17/20] add native field de/encryption --- src/examples/encryptionv2.ts | 16 +++- src/lib/provable/crypto/encryption.ts | 112 ++++++++++++++++---------- 2 files changed, 83 insertions(+), 45 deletions(-) diff --git a/src/examples/encryptionv2.ts b/src/examples/encryptionv2.ts index 61d01dae4e..f9087c3d40 100644 --- a/src/examples/encryptionv2.ts +++ b/src/examples/encryptionv2.ts @@ -1,3 +1,4 @@ +import assert from 'assert'; import { Bytes, PrivateKey, @@ -14,8 +15,19 @@ const priv = PrivateKey.random(); const pub = priv.toPublicKey(); const plainMsg = 'Hello world'; + +console.log('en/decryption of field elements'); +const cipher2 = Encryption.encryptV2(Encoding.stringToFields(plainMsg), pub); +const plainText2 = Encryption.decryptV2(cipher2, priv); + +assert( + Encoding.stringFromFields(plainText2) === plainMsg, + 'Plain message and decrypted message are the same' +); + +console.log('en/decryption of bytes'); const message = Bytes256.fromString(plainMsg); console.log('plain message', plainMsg); -const cipher = Encryption.encryptV2(message, pub); -const plainText = Encryption.decryptV2(cipher, priv); +const cipher = Encryption.encryptBytes(message, pub); +const plainText = Encryption.decryptBytes(cipher, priv); console.log('decrypted message', Buffer.from(plainText.toBytes()).toString()); diff --git a/src/lib/provable/crypto/encryption.ts b/src/lib/provable/crypto/encryption.ts index c504cc1cea..4043f5c48c 100644 --- a/src/lib/provable/crypto/encryption.ts +++ b/src/lib/provable/crypto/encryption.ts @@ -7,12 +7,22 @@ import { Bytes } from '../bytes.js'; import { UInt8 } from '../int.js'; import { chunk } from '../../util/arrays.js'; -export { encrypt, decrypt, encryptV2, decryptV2 }; +export { + encrypt, + decrypt, + encryptV2, + decryptV2, + encryptBytes, + decryptBytes, + CipherTextBytes, + CipherText, +}; type CipherText = { publicKey: Group; cipherText: Field[]; }; +type CipherTextBytes = CipherText & { messageLength: number }; /** * @deprecated Use {@link encryptV2} instead. @@ -79,11 +89,7 @@ function decrypt( * Decrypts a {@link CipherText} using a {@link PrivateKey}. */ function decryptV2( - { - publicKey, - cipherText, - messageLength, - }: CipherText & { messageLength: number }, + { publicKey, cipherText }: CipherText, privateKey: PrivateKey ) { // key exchange @@ -102,50 +108,23 @@ function decryptV2( const keyStream = sponge.squeeze(); const messageChunk = cipherText[i].sub(keyStream); - // convert to bytes - const byteMessage = wordToBytes(messageChunk, 31); - - // push the message to our final message array - message.push(byteMessage); + // push the message to our final messages + message.push(messageChunk); + // absorb the cipher text chunk 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)); + return message; } /** - * Public Key Encryption, encrypts Bytes using a {@link PublicKey}. + * Public Key Encryption, encrypts Field elements 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) - ); - - // convert message into chunks of 31 bytes - const chunks = chunk(bytes.concat(padding), 31); - +function encryptV2(message: Field[], otherPublicKey: PublicKey): CipherText { // key exchange const privateKey = Provable.witness(Scalar, () => Scalar.random()); const publicKey = Group.generator.scale(privateKey); @@ -156,13 +135,13 @@ function encryptV2( // encryption const cipherText = []; - for (let [n, chunk] of chunks.entries()) { + for (let [n, chunk] of message.entries()) { // absorb frame bit - if (n === chunks.length - 1) sponge.absorb(Field(1)); + if (n === message.length - 1) sponge.absorb(Field(1)); else sponge.absorb(Field(0)); const keyStream = sponge.squeeze(); - const encryptedChunk = bytesToWord(chunk).add(keyStream); + const encryptedChunk = chunk.add(keyStream); cipherText.push(encryptedChunk); sponge.absorb(encryptedChunk); @@ -172,5 +151,52 @@ function encryptV2( const authenticationTag = sponge.squeeze(); cipherText.push(authenticationTag); - return { publicKey, cipherText, messageLength }; + return { publicKey, cipherText }; +} + +/** + * Public Key Encryption, encrypts Bytes using a {@link PublicKey}. + */ +function encryptBytes( + message: Bytes, + otherPublicKey: PublicKey +): CipherTextBytes { + 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) + ); + + // convert message into chunks of 31 bytes + const chunks = chunk(bytes.concat(padding), 31); + + // call into encryption() and convert chunk to field elements + return { + ...encryptV2( + chunks.map((chunk) => bytesToWord(chunk)), + otherPublicKey + ), + messageLength, + }; +} + +/** + * Decrypts a {@link CipherText} using a {@link PrivateKey}. + */ +function decryptBytes(cipherText: CipherTextBytes, privateKey: PrivateKey) { + // calculate padding + const messageLength = cipherText.messageLength; + const multipleOf = 31; + const n = Math.ceil(messageLength / multipleOf) * multipleOf; + + // decrypt plain field elements and convert them into bytes + const message = decryptV2(cipherText, privateKey); + const bytes = message.map((m) => wordToBytes(m, 31)); + return Bytes.from(bytes.flat().slice(0, messageLength - n)); } From 23cccd7a8c76aec8b1bd01b0555eead3d639b0cc Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 30 Jul 2024 12:27:00 +0200 Subject: [PATCH 18/20] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 473f2555ce..fa3397abfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ 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. + - Also added `Encryption.encryptBytes()` and `Encryption.decryptBytes()` using the same algorithm. ### Deprecated From 499fdc9fdc60dc5d6e86840426a9688316c5627f Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 30 Jul 2024 15:46:07 +0200 Subject: [PATCH 19/20] fix merge conflict --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c35031c7e2..baf5beaf6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,12 +67,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - 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 - <<<<<<< HEAD -- # `Encryption.encrypt()` and `Encryption.decrypt()` in favor of `Encryption.encryptV2()` and `Encryption.decryptV2()` - `Int64` constructor, recommending `Int64.create()` instead https://github.com/o1-labs/o1js/pull/1735 - Original `div()` and `fromObject`, methods in favor of V2 versions https://github.com/o1-labs/o1js/pull/1735 - Deprecate `AccountUpdate.defaultAccountUpdate()` in favor of `AccountUpdate.default()` https://github.com/o1-labs/o1js/pull/1676 - > > > > > > > main ### Fixed From 78a12d65b633ecba8139cdb92a88300c7d53adf0 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 30 Jul 2024 15:53:51 +0200 Subject: [PATCH 20/20] larger message --- src/examples/encryptionv2.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/examples/encryptionv2.ts b/src/examples/encryptionv2.ts index f9087c3d40..e2ee166ad9 100644 --- a/src/examples/encryptionv2.ts +++ b/src/examples/encryptionv2.ts @@ -5,7 +5,6 @@ import { initializeBindings, Encryption, Encoding, - Provable, } from 'o1js'; await initializeBindings(); @@ -14,7 +13,7 @@ class Bytes256 extends Bytes(256) {} const priv = PrivateKey.random(); const pub = priv.toPublicKey(); -const plainMsg = 'Hello world'; +const plainMsg = 'The quick brown fox jumped over the angry dog.'; console.log('en/decryption of field elements'); const cipher2 = Encryption.encryptV2(Encoding.stringToFields(plainMsg), pub);