From 0d026ae238729288354b495431419fd7ed1ee2fc Mon Sep 17 00:00:00 2001 From: J Caso Date: Tue, 19 Nov 2024 17:03:05 +0100 Subject: [PATCH 1/5] feat: add Streamer, Texter and extend MatterCodex --- src/keri/core/matter.ts | 24 +++++++ src/keri/core/streamer.ts | 119 ++++++++++++++++++++++++++++++++++ src/keri/core/texter.ts | 66 +++++++++++++++++++ test/core/texter.test.ts | 131 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 340 insertions(+) create mode 100644 src/keri/core/streamer.ts create mode 100644 src/keri/core/texter.ts create mode 100644 test/core/texter.test.ts diff --git a/src/keri/core/matter.ts b/src/keri/core/matter.ts index cbbc0f85..892610c4 100644 --- a/src/keri/core/matter.ts +++ b/src/keri/core/matter.ts @@ -41,6 +41,30 @@ export class MatterCodex extends Codex { StrB64_Big_L0: string = '7AAA'; // String Base64 Only Big Lead Size 0 StrB64_Big_L1: string = '8AAA'; // String Base64 Only Big Lead Size 1 StrB64_Big_L2: string = '9AAA'; // String Base64 Only Big Lead Size 2 + Bytes_L0: string = '4B'; // Byte String Lead Size 0 + Bytes_L1: string = '5B'; // Byte String Lead Size 1 + Bytes_L2: string = '6B'; // Byte String Lead Size 2 + Bytes_Big_L0: string = '7AAB'; // Byte String Big Lead Size 0 + Bytes_Big_L1: string = '8AAB'; // Byte String Big Lead Size 1 + Bytes_Big_L2: string = '9AAB'; // Byte String Big Lead Size 2 + X25519_Cipher_L0: string = '4C' // X25519 sealed box cipher bytes of sniffable stream plaintext lead size 0 + X25519_Cipher_L1: string = '5C' // X25519 sealed box cipher bytes of sniffable stream plaintext lead size 1 + X25519_Cipher_L2: string = '6C' // X25519 sealed box cipher bytes of sniffable stream plaintext lead size 2 + X25519_Cipher_Big_L0: string = '7AAC' // X25519 sealed box cipher bytes of sniffable stream plaintext big lead size 0 + X25519_Cipher_Big_L1: string = '8AAC' // X25519 sealed box cipher bytes of sniffable stream plaintext big lead size 1 + X25519_Cipher_Big_L2: string = '9AAC' // X25519 sealed box cipher bytes of sniffable stream plaintext big lead size 2 + X25519_Cipher_QB64_L0: string = '4D' // X25519 sealed box cipher bytes of QB64 plaintext lead size 0 + X25519_Cipher_QB64_L1: string = '5D' // X25519 sealed box cipher bytes of QB64 plaintext lead size 1 + X25519_Cipher_QB64_L2: string = '6D' // X25519 sealed box cipher bytes of QB64 plaintext lead size 2 + X25519_Cipher_QB64_Big_L0: string = '7AAD' // X25519 sealed box cipher bytes of QB64 plaintext big lead size 0 + X25519_Cipher_QB64_Big_L1: string = '8AAD' // X25519 sealed box cipher bytes of QB64 plaintext big lead size 1 + X25519_Cipher_QB64_Big_L2: string = '9AAD' // X25519 sealed box cipher bytes of QB64 plaintext big lead size 2 + X25519_Cipher_QB2_L0: string = '4E' // X25519 sealed box cipher bytes of QB2 plaintext lead size 0 + X25519_Cipher_QB2_L1: string = '5E' // X25519 sealed box cipher bytes of QB2 plaintext lead size 1 + X25519_Cipher_QB2_L2: string = '6E' // X25519 sealed box cipher bytes of QB2 plaintext lead size 2 + X25519_Cipher_QB2_Big_L0: string = '7AAE' // X25519 sealed box cipher bytes of QB2 plaintext big lead size 0 + X25519_Cipher_QB2_Big_L1: string = '8AAE' // X25519 sealed box cipher bytes of QB2 plaintext big lead size 1 + X25519_Cipher_QB2_Big_L2: string = '9AAE' // X25519 sealed box cipher bytes of QB2 plaintext big lead size 2 } export const MtrDex = new MatterCodex(); diff --git a/src/keri/core/streamer.ts b/src/keri/core/streamer.ts new file mode 100644 index 00000000..8e6481bd --- /dev/null +++ b/src/keri/core/streamer.ts @@ -0,0 +1,119 @@ +/** + * Streamer class for handling and verifying CESR streams. + * CESR refers to Composable Event Streaming Representation, + * a specification used in systems like KERI. + * + * This class allows creating instances of streams that can be verified + * and managed according to the CESR specification. + */ +import {Texter} from "./texter"; +import {Bexter} from "./bexter"; +import {encodeBase64Url} from "./base64"; + +export class Streamer { + private _stream: Uint8Array; + + /** + * Initializes an instance of the Streamer class. + * It accepts a CESR stream as either a string or Uint8Array and stores it internally + * as Uint8Array to ensure uniformity in data handling. + * + * @param streamInput The input stream which can be either a string or Uint8Array. + * @throws Error if the input stream type is not valid. + */ + constructor(streamInput: string | Uint8Array) { + if (typeof streamInput === 'string') { + // Converts string input to Uint8Array using Buffer.from + this._stream = new Uint8Array(Buffer.from(streamInput)); + } else if (streamInput instanceof Uint8Array) { + // Directly uses the Uint8Array if the input is already of this type + this._stream = streamInput; + } else { + throw new Error("Invalid stream type: Input must be a string or Uint8Array."); + } + } + + /** + * @returns {boolean} Returns True if .stream is sniffable, False otherwise + */ + public verify(): boolean { + + if (!this._stream || this._stream.length === 0) { + return false; + } + + // TODO: check if sniffable + return false; + } + + /** + * Gets the CESR stream in its current format. + * + * @returns {Uint8Array} The stream as a Uint8Array. + */ + get stream(): Uint8Array { + return this._stream; + } + + /** + * Returns the stream where all primitives and groups are expanded to qb64. + * This property requires parsing the full depth of the stream to ensure consistent expansion. + * + * @returns {string} The expanded text qb64 version of the stream. + */ + get text(): string { + return Buffer.from(this._stream).toString('base64'); + } + + /** + * Returns the stream where all primitives and groups are compacted to qb2. + * This property requires parsing the full depth of the stream to ensure consistent compaction. + * + * @returns {Uint8Array} The compacted binary qb2 version of the stream. + */ + get binary(): Uint8Array { + return this._stream; // This should actually compact the data. + } + + /** + * Represents the stream as a Texter instance. + * A Texter is a hypothetical class used for handling textual representations of streams. + * + * @returns {Texter} A Texter primitive of the stream suitable for wrapping. + */ + get texter(): Texter { + if (!this._stream) { + throw new Error("Stream data is not available."); + } + // Create a Texter instance using the raw binary data + return new Texter({ + raw: this._stream + }); + } + + /** + * Gets a Bexter instance representing the stream. + * Encodes the internal stream data to a Bexter instance, handling the CESR format correctly + * and ensuring the text does not start with 'A' to prevent length ambiguity. + * + * @returns {Bexter} A Bexter instance initialized with the encoded stream. + * @throws Error if the stream data is not available. + */ + get bexter(): Bexter { + if (!this._stream) { + throw new Error("Stream data is not available."); + } + + // @ts-ignore + const encodedText = encodeBase64Url(this._stream); // Convert the raw stream to base64 URL-safe format. + + // Ensure not to start with 'A' which could be a padding character in base64 indicating a certain byte length. + if (encodedText.startsWith('A')) { + throw new Error("Base64 representation of the stream starts with 'A', leading to length ambiguity."); + } + + return new Bexter({ + qb64: encodedText + }); + } +} \ No newline at end of file diff --git a/src/keri/core/texter.ts b/src/keri/core/texter.ts new file mode 100644 index 00000000..3f723fa5 --- /dev/null +++ b/src/keri/core/texter.ts @@ -0,0 +1,66 @@ +import { Matter, MtrDex } from './matter'; +import { EmptyMaterialError } from "./kering"; + +interface TexterArgs { + raw?: Uint8Array; + qb64b?: Uint8Array; + qb64?: string; + qb2?: Uint8Array; + code?: string; + text?: string | Uint8Array; +} + +class Texter extends Matter { + constructor({ + raw, + qb64b, + qb64, + qb2, + code, + text, + }: TexterArgs) { + + if (text !== undefined) { + if (typeof text === "string") { + raw = new TextEncoder().encode(text); + } else { + raw = text; + } + } + + if (!code && raw) { + const length = raw.length; + if (length < 64 ** 2) { + code = MtrDex.Bytes_L0; // Handle data < 4096 bytes + } else if (length < 64 ** 3) { + code = MtrDex.Bytes_L1; + } else if (length < 64 ** 4) { + code = MtrDex.Bytes_L2; + } else if (length < 64 ** 5) { + code = MtrDex.Bytes_Big_L0; + } else if (length < 64 ** 6) { + code = MtrDex.Bytes_Big_L1; + } else if (length < 64 ** 7) { + code = MtrDex.Bytes_Big_L2; + } else { + throw new Error("Text size exceeds the maximum supported size."); + } + } + + if (!raw && !qb64b && !qb64 && !qb2 && text === undefined) { + throw new EmptyMaterialError("Missing text string."); + } + + super({ raw, qb64b, qb64, qb2, code }); + + if (!code || ![MtrDex.Bytes_L0, MtrDex.Bytes_L1, MtrDex.Bytes_L2, MtrDex.Bytes_Big_L0, MtrDex.Bytes_Big_L1, MtrDex.Bytes_Big_L2].includes(code)) { + throw new Error(`Invalid code = ${code} for Texter.`); + } + } + + get text(): string { + return new TextDecoder().decode(this.raw); + } +} + +export {Texter} \ No newline at end of file diff --git a/test/core/texter.test.ts b/test/core/texter.test.ts new file mode 100644 index 00000000..2c4f8a11 --- /dev/null +++ b/test/core/texter.test.ts @@ -0,0 +1,131 @@ +import {EmptyMaterialError, MtrDex} from "../../src"; +import {Texter} from "../../src/keri/core/texter"; + +describe('Texter', () => { + + test('throws EmptyMaterialError when no input is provided', () => { + expect(() => { + new Texter({}); + }).toThrow(EmptyMaterialError); + }); + + test('handles empty text correctly', () => { + const texter = new Texter({ text: "" }); + expect(texter.code).toBe(MtrDex.Bytes_L0); + expect(texter.both).toBe('4BAA'); + expect(texter.raw).toEqual(new Uint8Array([])); + expect(texter.qb64).toBe('4BAA'); + expect(texter.text).toBe(""); + }); + + test('handles empty byte text correctly', () => { + const texter = new Texter({ text: new Uint8Array([]) }); + expect(texter.both).toBe('4BAA'); + expect(texter.raw).toEqual(new Uint8Array([])); + }); + + test('handles non-empty text correctly', () => { + const text = "$"; + const texter = new Texter({ text }); + expect(texter.code).toBe(MtrDex.Bytes_L2); + expect(texter.both).toBe('6BAB'); + expect(texter.raw).toEqual(new TextEncoder().encode(text)); + expect(texter.qb64).toBe('6BABAAAk'); + expect(texter.text).toBe(text); + }); + + test('handles single character text correctly', () => { + const text = "$"; + const texter = new Texter({ text }); + expect(texter.code).toBe(MtrDex.Bytes_L2); + expect(texter.both).toBe('6BAB'); + expect(texter.raw).toEqual(new TextEncoder().encode(text)); + expect(texter.qb64).toBe('6BABAAAk'); + expect(texter.text).toBe(text); + }); + + test('handles control character \\n correctly', () => { + const text = "\n"; + const texter = new Texter({ text }); + expect(texter.code).toBe(MtrDex.Bytes_L2); + expect(texter.both).toBe('6BAB'); + expect(texter.qb64).toBe('6BABAAAK'); + expect(texter.text).toBe(text); + }); + + test('handles text with special characters correctly', () => { + const text = "@!"; + const texter = new Texter({ text }); + expect(texter.code).toBe(MtrDex.Bytes_L1); + expect(texter.both).toBe('5BAB'); + expect(texter.raw).toEqual(new TextEncoder().encode(text)); + expect(texter.qb64).toBe('5BABAEAh'); + expect(texter.text).toBe(text); + }); + + test('handles multi-character text correctly', () => { + const text = "^*#"; + const texter = new Texter({ text }); + expect(texter.code).toBe(MtrDex.Bytes_L0); + expect(texter.both).toBe('4BAB'); + expect(texter.raw).toEqual(new TextEncoder().encode(text)); + expect(texter.qb64).toBe('4BABXioj'); + expect(texter.text).toBe(text); + }); + + test('handles large text symbols correctly', () => { + const text = "&~?%"; + const texter = new Texter({ text }); + expect(texter.code).toBe(MtrDex.Bytes_L2); + expect(texter.both).toBe('6BAC'); + expect(texter.raw).toEqual(new TextEncoder().encode(text)); + expect(texter.qb64).toBe('6BACAAAmfj8l'); + expect(texter.text).toBe(text); + }); + + test('ensures encoding and decoding for complex strings', () => { + const complexText = "😊🚀👍🏽🌟"; + const texter = new Texter({ text: complexText }); + expect(texter.text).toBe(complexText); + expect(new TextDecoder().decode(texter.raw)).toBe(complexText); + }); + + test('handles long text correctly', () => { + const text = "Did the lazy fox jumped over the big dog? But it's not its dog!\n"; + const texter = new Texter({ text }); + expect(texter.code).toBe(MtrDex.Bytes_L2); + expect(texter.both).toBe('6BAW'); + expect(texter.text).toBe(text); + }); + + // Edge case for text exactly at the size limit + test('handles text at maximum size limit without error', () => { + const maxText = "a".repeat(64 ** 4 - 1); + expect(() => new Texter({ text: maxText })).not.toThrow(); + }); + + test('handles very large text sizes correctly', () => { + const text = "a".repeat((64 ** 2) * 3); + const texter = new Texter({ text }); + expect(texter.code).toBe(MtrDex.Bytes_Big_L0); + expect(texter.both).toBe('7AABABAA'); + expect(texter.text).toBe(text); + }); + + test('handles edge cases for big variable size texts', () => { + const text = "b".repeat((64 ** 2) * 3 + 1); + const texter = new Texter({ text }); + expect(texter.code).toBe(MtrDex.Bytes_Big_L2); + expect(texter.both).toBe('9AABABAB'); + expect(texter.raw).toEqual(new TextEncoder().encode(text)); + expect(texter.text).toBe(text); + }); + + test('throws error with unsupported very large text', () => { + const text = "c".repeat((64 ** 4) * 3); // Excessive size + expect(() => { + new Texter({ text }); + }).toThrow(Error); + }); +}); + From 86d9afee1749e75baf6003541fca20e39b171ef0 Mon Sep 17 00:00:00 2001 From: J Caso Date: Tue, 19 Nov 2024 17:27:22 +0100 Subject: [PATCH 2/5] feat: extend Matter sizes --- src/keri/core/matter.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/keri/core/matter.ts b/src/keri/core/matter.ts index 892610c4..2706a6af 100644 --- a/src/keri/core/matter.ts +++ b/src/keri/core/matter.ts @@ -47,6 +47,7 @@ export class MatterCodex extends Codex { Bytes_Big_L0: string = '7AAB'; // Byte String Big Lead Size 0 Bytes_Big_L1: string = '8AAB'; // Byte String Big Lead Size 1 Bytes_Big_L2: string = '9AAB'; // Byte String Big Lead Size 2 + X25519_Cipher_L0: string = '4C' // X25519 sealed box cipher bytes of sniffable stream plaintext lead size 0 X25519_Cipher_L1: string = '5C' // X25519 sealed box cipher bytes of sniffable stream plaintext lead size 1 X25519_Cipher_L2: string = '6C' // X25519 sealed box cipher bytes of sniffable stream plaintext lead size 2 @@ -204,6 +205,24 @@ export class Matter { '7AAB': new Sizage(4, 4, undefined, 0), '8AAB': new Sizage(4, 4, undefined, 1), '9AAB': new Sizage(4, 4, undefined, 2), + '4C': new Sizage(2, 2, undefined, 0), + '5C': new Sizage(2, 2, undefined, 1), + '6C': new Sizage(2, 2, undefined, 2), + '7AAC': new Sizage(4, 4, undefined, 0), + '8AAC': new Sizage(4, 4, undefined, 1), + '9AAC': new Sizage(4, 4, undefined, 2), + '4D': new Sizage(2, 2, undefined, 0), + '5D': new Sizage(2, 2, undefined, 1), + '6D': new Sizage(2, 2, undefined, 2), + '7AAD': new Sizage(4, 4, undefined, 0), + '8AAD': new Sizage(4, 4, undefined, 1), + '9AAD': new Sizage(4, 4, undefined, 2), + '4E': new Sizage(2, 2, undefined, 0), + '5E': new Sizage(2, 2, undefined, 1), + '6E': new Sizage(2, 2, undefined, 2), + '7AAE': new Sizage(4, 4, undefined, 0), + '8AAE': new Sizage(4, 4, undefined, 1), + '9AAE': new Sizage(4, 4, undefined, 2) }) ); From 185f4a956a6cc5a397c00f56fa73a4fcdb159fa7 Mon Sep 17 00:00:00 2001 From: J Caso Date: Tue, 19 Nov 2024 17:57:27 +0100 Subject: [PATCH 3/5] feat: extend supported codes for Cipher --- src/keri/core/cipher.ts | 15 +++++++++------ src/keri/core/matter.ts | 31 ++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/keri/core/cipher.ts b/src/keri/core/cipher.ts index ba11262e..dd590bf4 100644 --- a/src/keri/core/cipher.ts +++ b/src/keri/core/cipher.ts @@ -14,12 +14,15 @@ export class Cipher extends Matter { } super({ raw: raw, code: code, qb64b: qb64b, qb64: qb64, qb2: qb2 }); - if ( - !Array.from([ - MtrDex.X25519_Cipher_Salt, - MtrDex.X25519_Cipher_Seed, - ]).includes(this.code) - ) { + if (!this.code || ![ + MtrDex.X25519_Cipher_Salt, MtrDex.X25519_Cipher_Seed, + MtrDex.X25519_Cipher_L0, MtrDex.X25519_Cipher_L1, MtrDex.X25519_Cipher_L2, + MtrDex.X25519_Cipher_Big_L0, MtrDex.X25519_Cipher_Big_L1, MtrDex.X25519_Cipher_Big_L2, + MtrDex.X25519_Cipher_QB64_L0, MtrDex.X25519_Cipher_QB64_L1, MtrDex.X25519_Cipher_QB64_L2, + MtrDex.X25519_Cipher_QB64_Big_L0, MtrDex.X25519_Cipher_QB64_Big_L1, MtrDex.X25519_Cipher_QB64_Big_L2, + MtrDex.X25519_Cipher_QB2_L0, MtrDex.X25519_Cipher_QB2_L1, MtrDex.X25519_Cipher_QB2_L2, + MtrDex.X25519_Cipher_QB2_Big_L0, MtrDex.X25519_Cipher_QB2_Big_L1, MtrDex.X25519_Cipher_QB2_Big_L2 + ].includes(this.code)) { throw new Error(`Unsupported Cipher code == ${this.code}`); } } diff --git a/src/keri/core/matter.ts b/src/keri/core/matter.ts index 2706a6af..5735af7d 100644 --- a/src/keri/core/matter.ts +++ b/src/keri/core/matter.ts @@ -47,7 +47,6 @@ export class MatterCodex extends Codex { Bytes_Big_L0: string = '7AAB'; // Byte String Big Lead Size 0 Bytes_Big_L1: string = '8AAB'; // Byte String Big Lead Size 1 Bytes_Big_L2: string = '9AAB'; // Byte String Big Lead Size 2 - X25519_Cipher_L0: string = '4C' // X25519 sealed box cipher bytes of sniffable stream plaintext lead size 0 X25519_Cipher_L1: string = '5C' // X25519 sealed box cipher bytes of sniffable stream plaintext lead size 1 X25519_Cipher_L2: string = '6C' // X25519 sealed box cipher bytes of sniffable stream plaintext lead size 2 @@ -70,6 +69,36 @@ export class MatterCodex extends Codex { export const MtrDex = new MatterCodex(); +export const ciXAllQB64Dex = [ + MtrDex.X25519_Cipher_Seed, + MtrDex.X25519_Cipher_Salt, + MtrDex.X25519_Cipher_QB64_L0, + MtrDex.X25519_Cipher_QB64_L1, + MtrDex.X25519_Cipher_QB64_L2, + MtrDex.X25519_Cipher_QB64_Big_L0, + MtrDex.X25519_Cipher_QB64_Big_L1, + MtrDex.X25519_Cipher_QB64_Big_L2 +] + +export const ciXVarQB2Dex = [ + MtrDex.X25519_Cipher_QB2_L0, + MtrDex.X25519_Cipher_QB2_L1, + MtrDex.X25519_Cipher_QB2_L2, + MtrDex.X25519_Cipher_QB2_Big_L0, + MtrDex.X25519_Cipher_QB2_Big_L1, + MtrDex.X25519_Cipher_QB2_Big_L2 +] + +export const ciXVarStrmDex = [ + MtrDex.X25519_Cipher_L0, + MtrDex.X25519_Cipher_L1, + MtrDex.X25519_Cipher_L2, + MtrDex.X25519_Cipher_Big_L0, + MtrDex.X25519_Cipher_Big_L1, + MtrDex.X25519_Cipher_Big_L2 +] + + export class NonTransCodex extends Codex { Ed25519N: string = 'B'; // Ed25519 verification key non-transferable, basic derivation. ECDSA_256k1N: string = '1AAA'; // ECDSA secp256k1 verification key non-transferable, basic derivation. From e357c4fec051e7bb1a549def3fe5bbedd20354e6 Mon Sep 17 00:00:00 2001 From: J Caso Date: Tue, 19 Nov 2024 18:33:46 +0100 Subject: [PATCH 4/5] feat: Encrypter variable length --- src/keri/core/encrypter.ts | 43 ++++++++++++++++++++++++++------------ src/keri/core/matter.ts | 30 ++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/keri/core/encrypter.ts b/src/keri/core/encrypter.ts index 0ac77b26..fddc280a 100644 --- a/src/keri/core/encrypter.ts +++ b/src/keri/core/encrypter.ts @@ -1,10 +1,11 @@ import libsodium from 'libsodium-wrappers-sumo'; -import { Matter, MatterArgs, MtrDex } from './matter'; +import {ciXAllQB64Dex, ciXVarStrmDex, Matter, MatterArgs, MatterCodex, MtrDex} from './matter'; import { Verfer } from './verfer'; import { Signer } from './signer'; import { Cipher } from './cipher'; import { arrayEquals } from './utils'; +import {Streamer} from "./streamer"; export class Encrypter extends Matter { private _encrypt: any; @@ -44,23 +45,39 @@ export class Encrypter extends Matter { return arrayEquals(pubkey, this.raw); } - encrypt(ser: Uint8Array | null = null, matter: Matter | null = null) { - if (ser == null && matter == null) { - throw new Error('Neither ser nor matter are provided.'); - } + encrypt(ser: Uint8Array | null = null, matter: Matter | Streamer | null = null, code: string | null = null) { + + if (!ser) { + if (!matter){ + throw new Error('Neither ser nor matter are provided.'); + } - if (ser != null) { - matter = new Matter({ qb64b: ser }); + if (!code) { + if (!(matter instanceof Matter) || matter.code == MtrDex.Salt_128){ + code = MtrDex.X25519_Cipher_Salt; + } else if (matter.code == MtrDex.Ed25519_Seed){ + code = MtrDex.X25519_Cipher_Seed; + } else { + throw new Error(`Unsupported primitive with code = ${matter.code} when cipher code is missing`); + } + } + + if (ciXAllQB64Dex.includes(code)) { + if (matter instanceof Matter) { + ser = matter.qb64b; + } + } else if (ciXVarStrmDex.includes(code)){ + ser = (matter as Streamer).stream; + } else { + throw new Error(`Invalid primitive cipher ${(matter instanceof Matter) ? matter.code : matter.stream} not qb64`); + } } - let code; - if (matter!.code == MtrDex.Salt_128) { - code = MtrDex.X25519_Cipher_Salt; - } else { - code = MtrDex.X25519_Cipher_Seed; + if (!code) { // assumes default is sniffable stream + code = MtrDex.X25519_Cipher_L0; } - return this._encrypt(matter!.qb64, this.raw, code); + return this._encrypt(ser, this.raw, code); } _x25519(ser: Uint8Array, pubkey: Uint8Array, code: string) { diff --git a/src/keri/core/matter.ts b/src/keri/core/matter.ts index 5735af7d..0360167c 100644 --- a/src/keri/core/matter.ts +++ b/src/keri/core/matter.ts @@ -590,4 +590,34 @@ export class Matter { private _bexfil(qb2: Uint8Array) { throw new Error(`qb2 not yet supported: ${qb2}`); } + + static determineMatterCode(length: number, format: string): string { + const isQB64 = format === 'qb64'; + const isQB2 = format === 'qb2'; + + if (length <= Matter._rawSize(MtrDex.X25519_Cipher_L2)) { + if (isQB64) { + return (length % 3 === 0) ? MtrDex.X25519_Cipher_QB64_L0 : + (length % 3 === 1) ? MtrDex.X25519_Cipher_QB64_L1 : MtrDex.X25519_Cipher_QB64_L2; + } else if (isQB2) { + return (length % 3 === 0) ? MtrDex.X25519_Cipher_QB2_L0 : + (length % 3 === 1) ? MtrDex.X25519_Cipher_QB2_L1 : MtrDex.X25519_Cipher_QB2_L2; + } else { + return (length % 3 === 0) ? MtrDex.X25519_Cipher_L0 : + (length % 3 === 1) ? MtrDex.X25519_Cipher_L1 : MtrDex.X25519_Cipher_L2; + } + } else { + if (isQB64) { + return (length % 3 === 0) ? MtrDex.X25519_Cipher_QB64_Big_L0 : + (length % 3 === 1) ? MtrDex.X25519_Cipher_QB64_Big_L1 : MtrDex.X25519_Cipher_QB64_Big_L2; + } else if (isQB2) { + return (length % 3 === 0) ? MtrDex.X25519_Cipher_QB2_Big_L0 : + (length % 3 === 1) ? MtrDex.X25519_Cipher_QB2_Big_L1 : MtrDex.X25519_Cipher_QB2_Big_L2; + } else { + return (length % 3 === 0) ? MtrDex.X25519_Cipher_Big_L0 : + (length % 3 === 1) ? MtrDex.X25519_Cipher_Big_L1 : MtrDex.X25519_Cipher_Big_L2; + } + } + } + } From 591d33eed8195600ef3b58d57209be0b9ae4eaa3 Mon Sep 17 00:00:00 2001 From: J Caso Date: Tue, 3 Dec 2024 13:30:58 +0100 Subject: [PATCH 5/5] feat: Decrypter and Encrypter --- src/keri/app/controller.ts | 2 +- src/keri/core/decrypter.ts | 53 ++++++++++++++++++++++++++----------- src/keri/core/keeping.ts | 13 +++++---- src/keri/core/manager.ts | 4 +-- test/core/decrypter.test.ts | 3 +++ 5 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/keri/app/controller.ts b/src/keri/app/controller.ts index bdfcc7ba..2859afe4 100644 --- a/src/keri/app/controller.ts +++ b/src/keri/app/controller.ts @@ -325,7 +325,7 @@ export class Controller { const signers = []; for (const prx of prxs) { const cipher = new Cipher({ qb64: prx }); - const dsigner = decrypter.decrypt(null, cipher, true); + const dsigner = decrypter.decrypt(null, cipher, null,true); signers.push(dsigner); nprxs.push(encrypter.encrypt(b(dsigner.qb64)).qb64); } diff --git a/src/keri/core/decrypter.ts b/src/keri/core/decrypter.ts index 9be6a1d4..7ddc8276 100644 --- a/src/keri/core/decrypter.ts +++ b/src/keri/core/decrypter.ts @@ -1,10 +1,11 @@ import libsodium from 'libsodium-wrappers-sumo'; -import { Matter, MatterArgs, MtrDex } from './matter'; +import {ciXAllQB64Dex, ciXVarQB2Dex, ciXVarStrmDex, Matter, MatterArgs, MtrDex} from './matter'; import { Signer } from './signer'; import { Cipher } from './cipher'; import { EmptyMaterialError } from './kering'; import { Salter } from './salter'; +import {Streamer} from "./streamer"; export class Decrypter extends Matter { private readonly _decrypt: any; @@ -47,34 +48,56 @@ export class Decrypter extends Matter { } decrypt( - ser: Uint8Array | null = null, + ser: Uint8Array | null = null, // qb64b cipher: Cipher | null = null, - transferable: boolean = false + klas = null, + transferable: boolean = false, + bare: boolean = false ) { - if (ser == null && cipher == null) { - throw new EmptyMaterialError('Neither ser or cipher were provided'); - } - if (ser != null) { - cipher = new Cipher({ qb64b: ser }); + if (!cipher){ + if (ser != null) { + cipher = new Cipher({ qb64b: ser }); + } else { + throw new Error(`Need one of cipher or qb64`); + } } - return this._decrypt(cipher, this.raw, transferable); + return this._decrypt(cipher, this.raw, klas, transferable, bare); } - _x25519(cipher: Cipher, prikey: Uint8Array, transferable: boolean = false) { + _x25519(cipher: Cipher, prikey: Uint8Array, Klas?: typeof Matter | typeof Streamer, transferable: boolean = false, bare: boolean = false) { const pubkey = libsodium.crypto_scalarmult_base(prikey); const plain = libsodium.crypto_box_seal_open( cipher.raw, pubkey, prikey ); - if (cipher.code == MtrDex.X25519_Cipher_Salt) { - return new Salter({ qb64b: plain }); - } else if (cipher.code == MtrDex.X25519_Cipher_Seed) { - return new Signer({ qb64b: plain, transferable: transferable }); + + if (bare) { + return plain } else { - throw new Error(`Unsupported cipher text code == ${cipher.code}`); + if (!Klas) { + if (cipher.code === MtrDex.X25519_Cipher_Salt){ + Klas = Salter; + } else if (cipher.code === MtrDex.X25519_Cipher_Seed) { + Klas = Signer; + } else if (ciXVarStrmDex.includes(cipher.code)){ + Klas = Streamer; + } else { + throw new Error(`Unsupported cipher code = ${cipher.code} when klas missing.`); + } + } + + if (ciXAllQB64Dex.includes(cipher.code)) { + // @ts-ignore + return new Klas({qb64b: plain, transferable}); + } else if (ciXVarStrmDex.includes(cipher.code)){ + // @ts-ignore + return new Klas(plain) + } else { + throw new Error(`Unsupported cipher code = ${cipher.code}.`); + } } } } diff --git a/src/keri/core/keeping.ts b/src/keri/core/keeping.ts index 9dba85ef..660b9fd9 100644 --- a/src/keri/core/keeping.ts +++ b/src/keri/core/keeping.ts @@ -508,8 +508,9 @@ export class RandyKeeper implements Keeper { this.signers = this.prxs.map((prx) => this.decrypter.decrypt( - new Cipher({ qb64: prx }).qb64b, - undefined, + null, + new Cipher({ qb64: prx }), + null, this.transferable ) ); @@ -567,8 +568,9 @@ export class RandyKeeper implements Keeper { const signers = this.nxts!.map((nxt) => this.decrypter.decrypt( - undefined, + null, new Cipher({ qb64: nxt }), + null, this.transferable ) ); @@ -600,8 +602,9 @@ export class RandyKeeper implements Keeper { ): Promise { const signers = this.prxs!.map((prx) => this.decrypter.decrypt( - new Cipher({ qb64: prx }).qb64b, - undefined, + null, + new Cipher({ qb64: prx }), + null, this.transferable ) ); diff --git a/src/keri/core/manager.ts b/src/keri/core/manager.ts index dbbb8f2d..7ef15489 100644 --- a/src/keri/core/manager.ts +++ b/src/keri/core/manager.ts @@ -1077,7 +1077,7 @@ class Keeper implements KeyStore { const out = new Array<[string, Signer]>(); this._pris.forEach(function (val, pubKey) { const verfer = new Verfer({ qb64: pubKey }); - const signer = decrypter.decrypt(val, null, verfer.transferable); + const signer = decrypter.decrypt(val, null, null, verfer.transferable); out.push([pubKey, signer]); }); return out; @@ -1104,7 +1104,7 @@ class Keeper implements KeyStore { } const verfer = new Verfer({ qb64: pubKey }); - return decrypter.decrypt(val, null, verfer.transferable); + return decrypter.decrypt(val, null, null, verfer.transferable); } pinPths(pubKey: string, val: PubPath): boolean { diff --git a/test/core/decrypter.test.ts b/test/core/decrypter.test.ts index 539f1b9a..c39b8db4 100644 --- a/test/core/decrypter.test.ts +++ b/test/core/decrypter.test.ts @@ -98,6 +98,7 @@ describe('Decrypter', () => { let designer = decrypter.decrypt( seedcipher.qb64b, null, + null, signer.verfer.transferable ); assert.deepStrictEqual(designer.qb64b, seedqb64b); @@ -109,6 +110,7 @@ describe('Decrypter', () => { designer = decrypter.decrypt( null, seedcipher, + null, signer.verfer.transferable ); assert.deepStrictEqual(designer.qb64b, seedqb64b); @@ -138,6 +140,7 @@ describe('Decrypter', () => { designer = decrypter.decrypt( b(cipherseed), null, + null, signer.verfer.transferable ); assert.deepStrictEqual(designer.qb64b, seedqb64b);