Skip to content

Commit

Permalink
fix: secret messages right next to each other
Browse files Browse the repository at this point in the history
  • Loading branch information
stepan662 committed Oct 9, 2024
1 parent b147813 commit e850f91
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
jest.autoMockOff();
import { InvisibleWrapper } from './InvisibleWrapper';
import { MESSAGE_END } from './secret';
import { InvisibleWrapper, MESSAGE_END } from './InvisibleWrapper';

const key1 = {
key: 'key1',
Expand Down
70 changes: 48 additions & 22 deletions packages/web/src/package/observers/invisible/InvisibleWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ import {
} from './secret';
import { ValueMemory } from './ValueMemory';

/**
* LF character to separate messages (when they are right next to each other)
*
* We can use the fact that `\n` characters get escaped inside the JSON strings and we don't need them with numbers
* so we can safely use newlines to separate strings
*
* WARNING: don't encode formatted json like this (because then there are newlines):
* {
* "a": "b"
* }
* this is correct:
* {"a":"b"}
*/
export const MESSAGE_END = '\x0A';

type EncodeValue = {
// key
k: string;
Expand Down Expand Up @@ -48,31 +63,42 @@ export function InvisibleWrapper({ fullKeyEncode }: Props): WrapperMiddleware {
}
}

function retrieveMessages(message: string) {
if (message[0] === '{') {
// there is a json inside - the full key is included, not just number `fullKeyEncode`
return message;
} else {
const valueCode = Number(message);
return keyMemory.numberToValue(valueCode);
}
function retrieveMessages(text: string) {
return text
.split(MESSAGE_END)
.filter((m) => m.length)
.map((message) => {
if (message[0] === '{') {
// there is a json inside - the full key is included, not just number `fullKeyEncode`
return message;
} else {
const valueCode = Number(message);
return keyMemory.numberToValue(valueCode);
}
});
}

function encodeWithSeparator(message: string) {
return encodeMessage(message + MESSAGE_END);
}

return Object.freeze({
unwrap(text: string): Unwrapped {
const keysAndParams = [] as KeyAndParams[];
const messages = decodeFromText(text);
messages.forEach((encodedValue: string) => {
const message = retrieveMessages(encodedValue);
const decodedVal = decodeValue(message);
if (decodedVal) {
const { k: key, d: defaultValue, n: ns } = decodedVal;
keysAndParams.push({
key,
defaultValue,
ns,
});
}
const texts = decodeFromText(text);
texts.forEach((encodedValue: string) => {
const messages = retrieveMessages(encodedValue);
messages.forEach((message) => {
const decodedVal = decodeValue(message);
if (decodedVal) {
const { k: key, d: defaultValue, n: ns } = decodedVal;
keysAndParams.push({
key,
defaultValue,
ns,
});
}
});
});

const result = removeSecrets(text);
Expand All @@ -85,11 +111,11 @@ export function InvisibleWrapper({ fullKeyEncode }: Props): WrapperMiddleware {
if (fullKeyEncode) {
// don't include default value, as that might be very long when encoded
const encodedValue = encodeValue({ key, ns });
invisibleMark = encodeMessage(encodedValue);
invisibleMark = encodeWithSeparator(encodedValue);
} else {
const encodedValue = encodeValue({ key, ns, defaultValue });
const code = keyMemory.valueToNumber(encodedValue);
invisibleMark = encodeMessage(String(code));
invisibleMark = encodeWithSeparator(String(code));
}

const value = translation || '';
Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/package/observers/invisible/secret.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ describe('Invisible encoder/decoder', () => {
expect(cleanedMessage).toEqual(originalMessage);
});

it('works with two messages right next to each other', () => {
it('works with two messages right next to each other (merges them into one)', () => {
const message =
'Tolgee' + encodeMessage('secret1') + encodeMessage('secret2');
expect(decodeFromText(message)).toEqual(['secret1', 'secret2']);
expect(decodeFromText(message)).toEqual(['secret1secret2']);
});

it('works with two messages spaced with regular text', () => {
Expand Down
13 changes: 3 additions & 10 deletions packages/web/src/package/observers/invisible/secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import * as FastTextEncoding from 'fast-text-encoding';
// eslint-disable-next-line no-console
console.assert?.(FastTextEncoding);

export const MESSAGE_END = '\x0A'; // using LF character to separate messages

export const INVISIBLE_CHARACTERS = ['\u200C', '\u200D'];

export const INVISIBLE_REGEX = RegExp(
Expand All @@ -29,7 +27,7 @@ function padToWholeBytes(binary: string) {
export function encodeMessage(text: string) {
// insert a message end character
// so we can distinguish two secret messages right next to each other
const bytes = toBytes(text + MESSAGE_END).map(Number);
const bytes = toBytes(text).map(Number);
const binary = bytes
.map((byte) => padToWholeBytes(byte.toString(2)) + '0')
.join('');
Expand Down Expand Up @@ -61,13 +59,8 @@ export function decodeFromText(text: string) {
.match(INVISIBLE_REGEX)
?.filter((m) => m.length > 8);
const result = [];
invisibleMessages?.map(decodeMessage).forEach((val) => {
// split by message end character, which separates multiple messages
val.split(MESSAGE_END).forEach((message) => {
if (message.length) {
result.push(message);
}
});
invisibleMessages?.map(decodeMessage).forEach((message) => {
result.push(message);
});

return result;
Expand Down

0 comments on commit e850f91

Please sign in to comment.