@thezelijah/majik-message 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +67 -0
- package/README.md +265 -0
- package/dist/core/contacts/majik-contact-directory.d.ts +34 -0
- package/dist/core/contacts/majik-contact-directory.js +165 -0
- package/dist/core/contacts/majik-contact.d.ts +53 -0
- package/dist/core/contacts/majik-contact.js +135 -0
- package/dist/core/crypto/constants.d.ts +7 -0
- package/dist/core/crypto/constants.js +6 -0
- package/dist/core/crypto/crypto-provider.d.ts +20 -0
- package/dist/core/crypto/crypto-provider.js +70 -0
- package/dist/core/crypto/encryption-engine.d.ts +59 -0
- package/dist/core/crypto/encryption-engine.js +257 -0
- package/dist/core/crypto/keystore.d.ts +126 -0
- package/dist/core/crypto/keystore.js +575 -0
- package/dist/core/messages/envelope-cache.d.ts +51 -0
- package/dist/core/messages/envelope-cache.js +375 -0
- package/dist/core/messages/message-envelope.d.ts +36 -0
- package/dist/core/messages/message-envelope.js +161 -0
- package/dist/core/scanner/scanner-engine.d.ts +27 -0
- package/dist/core/scanner/scanner-engine.js +120 -0
- package/dist/core/types.d.ts +23 -0
- package/dist/core/types.js +1 -0
- package/dist/core/utils/APITranscoder.d.ts +114 -0
- package/dist/core/utils/APITranscoder.js +305 -0
- package/dist/core/utils/idb-majik-system.d.ts +15 -0
- package/dist/core/utils/idb-majik-system.js +37 -0
- package/dist/core/utils/majik-file-utils.d.ts +16 -0
- package/dist/core/utils/majik-file-utils.js +153 -0
- package/dist/core/utils/utilities.d.ts +22 -0
- package/dist/core/utils/utilities.js +80 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/majik-message.d.ts +202 -0
- package/dist/majik-message.js +940 -0
- package/package.json +97 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { MessageEnvelope, MessageEnvelopeError, } from "../messages/message-envelope";
|
|
2
|
+
/* -------------------------------
|
|
3
|
+
* ScannerEngine
|
|
4
|
+
* ------------------------------- */
|
|
5
|
+
export class ScannerEngine {
|
|
6
|
+
observer;
|
|
7
|
+
contactDirectory;
|
|
8
|
+
envelopeCache;
|
|
9
|
+
onEnvelopeFound;
|
|
10
|
+
onUntrusted;
|
|
11
|
+
onError;
|
|
12
|
+
processedNodes = new WeakSet();
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.contactDirectory = config.contactDirectory;
|
|
15
|
+
this.envelopeCache = config.envelopeCache;
|
|
16
|
+
this.onEnvelopeFound = config.onEnvelopeFound;
|
|
17
|
+
this.onUntrusted = config.onUntrusted;
|
|
18
|
+
this.onError = config.onError;
|
|
19
|
+
}
|
|
20
|
+
/* -------------------------------
|
|
21
|
+
* Scan a single string
|
|
22
|
+
* ------------------------------- */
|
|
23
|
+
async scanText(text) {
|
|
24
|
+
if (!text || typeof text !== "string")
|
|
25
|
+
return;
|
|
26
|
+
const lines = text.split(/\s+/);
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
if (!MessageEnvelope.isEnvelopeCandidate(line))
|
|
29
|
+
continue;
|
|
30
|
+
try {
|
|
31
|
+
const envelope = MessageEnvelope.fromMatchedString(line);
|
|
32
|
+
// 1️⃣ Check cache first
|
|
33
|
+
if (this.envelopeCache && (await this.envelopeCache.has(envelope))) {
|
|
34
|
+
continue; // already processed
|
|
35
|
+
}
|
|
36
|
+
// 2️⃣ Check contact trust
|
|
37
|
+
const isTrusted = this.contactDirectory.hasContactForEnvelope(envelope);
|
|
38
|
+
if (!isTrusted) {
|
|
39
|
+
this.onUntrusted?.(line);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// 3️⃣ Check if contact is blocked
|
|
43
|
+
const contact = this.contactDirectory.getContactByFingerprint(envelope.extractFingerprint());
|
|
44
|
+
if (contact?.isBlocked()) {
|
|
45
|
+
this.onUntrusted?.(line);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// 4️⃣ Call callback
|
|
49
|
+
this.onEnvelopeFound?.(envelope);
|
|
50
|
+
// 5️⃣ Store in cache
|
|
51
|
+
await this.envelopeCache?.set(envelope);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
if (err instanceof MessageEnvelopeError) {
|
|
55
|
+
this.onUntrusted?.(err.raw ?? String(line));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
this.onError?.(err, { raw: line });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/* -------------------------------
|
|
64
|
+
* Scan all text nodes under a DOM node
|
|
65
|
+
* ------------------------------- */
|
|
66
|
+
scanDOM(rootNode) {
|
|
67
|
+
const walker = document.createTreeWalker(rootNode, NodeFilter.SHOW_TEXT, null);
|
|
68
|
+
let node = walker.nextNode();
|
|
69
|
+
while (node) {
|
|
70
|
+
// Skip text nodes that belong to input-like elements to avoid collisions
|
|
71
|
+
const parent = node.parentElement;
|
|
72
|
+
const isInputLike = parent
|
|
73
|
+
? ["INPUT", "TEXTAREA", "SELECT"].includes(parent.tagName) ||
|
|
74
|
+
parent.isContentEditable
|
|
75
|
+
: false;
|
|
76
|
+
if (!isInputLike && !this.processedNodes.has(node)) {
|
|
77
|
+
this.scanText(node.textContent ?? "");
|
|
78
|
+
this.processedNodes.add(node);
|
|
79
|
+
}
|
|
80
|
+
node = walker.nextNode();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/* -------------------------------
|
|
84
|
+
* Observe DOM changes and scan dynamically
|
|
85
|
+
* ------------------------------- */
|
|
86
|
+
startDOMObserver(rootNode) {
|
|
87
|
+
if (this.observer)
|
|
88
|
+
return; // already observing
|
|
89
|
+
this.observer = new MutationObserver((mutations) => {
|
|
90
|
+
for (const mutation of mutations) {
|
|
91
|
+
if (mutation.type === "characterData" && mutation.target) {
|
|
92
|
+
const targetNode = mutation.target;
|
|
93
|
+
const parent = targetNode.parentElement || null;
|
|
94
|
+
const isInputLike = parent
|
|
95
|
+
? ["INPUT", "TEXTAREA", "SELECT"].includes(parent.tagName) ||
|
|
96
|
+
parent.isContentEditable
|
|
97
|
+
: false;
|
|
98
|
+
if (!isInputLike && !this.processedNodes.has(targetNode)) {
|
|
99
|
+
this.scanText(targetNode.textContent ?? "");
|
|
100
|
+
this.processedNodes.add(targetNode);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (mutation.type === "childList") {
|
|
104
|
+
mutation.addedNodes.forEach((node) => this.scanDOM(node));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
this.observer.observe(rootNode, {
|
|
109
|
+
childList: true,
|
|
110
|
+
subtree: true,
|
|
111
|
+
characterData: true,
|
|
112
|
+
});
|
|
113
|
+
// Initial scan
|
|
114
|
+
this.scanDOM(rootNode);
|
|
115
|
+
}
|
|
116
|
+
stopDOMObserver() {
|
|
117
|
+
this.observer?.disconnect();
|
|
118
|
+
this.observer = undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type ISODateString = string;
|
|
2
|
+
export interface MAJIK_API_RESPONSE {
|
|
3
|
+
success: boolean;
|
|
4
|
+
message: string;
|
|
5
|
+
code?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SingleRecipientPayload {
|
|
8
|
+
iv: string;
|
|
9
|
+
ciphertext: string;
|
|
10
|
+
ephemeralPublicKey: string;
|
|
11
|
+
}
|
|
12
|
+
export interface MultiRecipientPayload {
|
|
13
|
+
iv: string;
|
|
14
|
+
ciphertext: string;
|
|
15
|
+
ephemeralPublicKey: string;
|
|
16
|
+
keys: RecipientKeys[];
|
|
17
|
+
}
|
|
18
|
+
export interface RecipientKeys {
|
|
19
|
+
fingerprint: string;
|
|
20
|
+
ephemeralEncryptedKey: string;
|
|
21
|
+
nonce: string;
|
|
22
|
+
}
|
|
23
|
+
export type EnvelopePayload = SingleRecipientPayload | MultiRecipientPayload;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export interface EncryptedData {
|
|
2
|
+
rqc: Uint8Array;
|
|
3
|
+
iv: Uint8Array;
|
|
4
|
+
cipher: Uint8Array;
|
|
5
|
+
}
|
|
6
|
+
export interface SerializedEncryptedData {
|
|
7
|
+
rqc: string;
|
|
8
|
+
iv: string;
|
|
9
|
+
cipher: string;
|
|
10
|
+
}
|
|
11
|
+
declare class APITranscoder {
|
|
12
|
+
/**
|
|
13
|
+
* Generates a 32-byte cipher key encoded as a base64 string, suitable for use with Fernet encryption.
|
|
14
|
+
* @returns {string} The generated cipher key ("rqc") in base64 format.
|
|
15
|
+
*/
|
|
16
|
+
static generateRQC(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Generates a transformed version of the rqc by reversing and interleaving the bytes.
|
|
19
|
+
* If rqc is not provided, a new one will be generated automatically.
|
|
20
|
+
* @param {string} [rqc] - Optional. The original 32-byte cipher key in base64 format.
|
|
21
|
+
* @returns {string} The transformed key as a base64 string.
|
|
22
|
+
*/
|
|
23
|
+
static generateRQX(rqc?: string | null): string;
|
|
24
|
+
/**
|
|
25
|
+
* Decodes the transformed rqx back to the original rqc base64 string.
|
|
26
|
+
* @param {string} rqx - The transformed key in base64 format.
|
|
27
|
+
* @returns {string} The original rqc in base64 format.
|
|
28
|
+
*/
|
|
29
|
+
static decodeRQX(rqx: string): string;
|
|
30
|
+
static hashData(inputJson: Record<string, unknown>): string;
|
|
31
|
+
/**
|
|
32
|
+
* Generates a SHA-256 hash for a given string.
|
|
33
|
+
* Validates that the input is a non-empty string.
|
|
34
|
+
* @param {string} string - The string to hash.
|
|
35
|
+
* @returns {string} The hash of the string in hexadecimal format.
|
|
36
|
+
* @throws {Error} If the input is not a valid non-empty string.
|
|
37
|
+
*/
|
|
38
|
+
static hashString(string: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Verifies that the hash of a decoded JSON object matches a provided hash.
|
|
41
|
+
* @param {Object} decodedJson - The JSON object to verify.
|
|
42
|
+
* @param {string} providedHash - The hash to compare against.
|
|
43
|
+
* @returns {boolean} True if the hashes match, false otherwise.
|
|
44
|
+
* @throws {Error} If inputs are invalid.
|
|
45
|
+
*/
|
|
46
|
+
static verifyHashJSON(decodedJson: Record<string, unknown>, providedHash: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Verifies that the hash of a given string matches a provided hash.
|
|
49
|
+
* @param {string} string - The string to verify.
|
|
50
|
+
* @param {string} providedHash - The hash to compare against.
|
|
51
|
+
* @returns {boolean} True if the hashes match, false otherwise.
|
|
52
|
+
* @throws {Error} If inputs are invalid.
|
|
53
|
+
*/
|
|
54
|
+
static verifyHashString(string: string, providedHash: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Decrypts an encrypted payload using AES-GCM encryption and returns the original JSON.
|
|
57
|
+
* @param {string} encrypted - The encrypted object data.
|
|
58
|
+
* @returns {Object} The decrypted and parsed JSON object.
|
|
59
|
+
* @throws {Error} Throws an error if decryption fails.
|
|
60
|
+
*/
|
|
61
|
+
static decryptPayload(encrypted: SerializedEncryptedData): Record<string, unknown>;
|
|
62
|
+
/**
|
|
63
|
+
* Encrypts a JSON object using AES-GCM encryption with the provided cipher key.
|
|
64
|
+
* @param {Object} json - The JSON object to encrypt.
|
|
65
|
+
* @param {string} rqc - The 32-byte cipher key in base64 format.
|
|
66
|
+
* @returns {Object} An object containing the encrypted payload and the transformed key, potentially URL-encoded.
|
|
67
|
+
* @throws {Error} Throws an error if encryption fails.
|
|
68
|
+
*/
|
|
69
|
+
static encryptPayload(json: Record<string, unknown>, rqc: string): SerializedEncryptedData;
|
|
70
|
+
}
|
|
71
|
+
export default APITranscoder;
|
|
72
|
+
export declare function generateRQKey(auth: string): string;
|
|
73
|
+
export declare function getAuthFromRQKey(rqkey: string): string;
|
|
74
|
+
export declare function validateRQKey(rqkey: string): boolean;
|
|
75
|
+
export declare function getDecodedRQKey(rqkey: string): string;
|
|
76
|
+
export declare function getSecureKeyFromRQKey(rqkey: string): string | number | object;
|
|
77
|
+
/**
|
|
78
|
+
* Converts input to a string and reverses it.
|
|
79
|
+
* If the input is a number, it is converted to a string.
|
|
80
|
+
* If the input is an object (JSON), it is stringified.
|
|
81
|
+
* If secure is true, it returns a Base64 encoded result.
|
|
82
|
+
* @param {any} input - The input to reverse.
|
|
83
|
+
* @param {boolean} secure - Whether to Base64 encode the result.
|
|
84
|
+
* @returns {string} - The reversed string, possibly encoded.
|
|
85
|
+
*/
|
|
86
|
+
export declare function secureReverse(input: string | object | number, secure?: boolean): string;
|
|
87
|
+
/**
|
|
88
|
+
* Decodes the reversed string based on the mode provided.
|
|
89
|
+
* If secure is true, the reversed string is decoded from Base64 first.
|
|
90
|
+
* @param {string} reversedString - The reversed string (possibly Base64 encoded).
|
|
91
|
+
* @param {string|null} mode - The mode to decode ('json', 'number', or 'string').
|
|
92
|
+
* @param {boolean} secure - Whether the input is Base64 encoded.
|
|
93
|
+
* @returns {any} - The decoded result based on the mode.
|
|
94
|
+
*/
|
|
95
|
+
export declare function decodeReverse(reversedString: string, mode?: string, secure?: boolean): string | object | number;
|
|
96
|
+
/**
|
|
97
|
+
* Converts a stringified JSON into a Uint8Array (UTF-8 bytes)
|
|
98
|
+
*/
|
|
99
|
+
export declare function jsonStringToBytes(jsonString: string): Uint8Array;
|
|
100
|
+
/**
|
|
101
|
+
* Converts a Uint8Array (UTF-8 bytes) back into a stringified JSON
|
|
102
|
+
*/
|
|
103
|
+
export declare function bytesToJsonString(bytes: Uint8Array): string;
|
|
104
|
+
/**
|
|
105
|
+
* Converts EncryptedData (Uint8Array fields) into base64 strings
|
|
106
|
+
* for safe transport / storage (JSON, URLs, APIs, etc.)
|
|
107
|
+
*/
|
|
108
|
+
export declare function encryptedDataToBase64(data: EncryptedData): SerializedEncryptedData;
|
|
109
|
+
/**
|
|
110
|
+
* Converts SerializedEncryptedData (base64 fields)
|
|
111
|
+
* back into EncryptedData (Uint8Array fields)
|
|
112
|
+
*/
|
|
113
|
+
export declare function encryptedDataFromBase64(data: SerializedEncryptedData): EncryptedData;
|
|
114
|
+
export declare function base64ToJson<T = unknown>(base64: string): T;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { hash } from "@stablelib/sha256";
|
|
2
|
+
import { AES } from "@stablelib/aes";
|
|
3
|
+
import { GCM } from "@stablelib/gcm";
|
|
4
|
+
import { randomBytes } from "@stablelib/random";
|
|
5
|
+
import { arrayToBase64, base64ToUint8Array } from "./utilities";
|
|
6
|
+
class APITranscoder {
|
|
7
|
+
/**
|
|
8
|
+
* Generates a 32-byte cipher key encoded as a base64 string, suitable for use with Fernet encryption.
|
|
9
|
+
* @returns {string} The generated cipher key ("rqc") in base64 format.
|
|
10
|
+
*/
|
|
11
|
+
static generateRQC() {
|
|
12
|
+
const bytes = randomBytes(32); // 32 bytes = 256 bits
|
|
13
|
+
const fKey = arrayToBase64(bytes);
|
|
14
|
+
return fKey; // Converts the byte array to a base64 string
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generates a transformed version of the rqc by reversing and interleaving the bytes.
|
|
18
|
+
* If rqc is not provided, a new one will be generated automatically.
|
|
19
|
+
* @param {string} [rqc] - Optional. The original 32-byte cipher key in base64 format.
|
|
20
|
+
* @returns {string} The transformed key as a base64 string.
|
|
21
|
+
*/
|
|
22
|
+
static generateRQX(rqc = null) {
|
|
23
|
+
// Auto-generate rqc if it's empty or null
|
|
24
|
+
if (!rqc) {
|
|
25
|
+
rqc = this.generateRQC();
|
|
26
|
+
}
|
|
27
|
+
// Decode the input rqc to bytes
|
|
28
|
+
const rqcBytes = Uint8Array.from(atob(rqc), (c) => c.charCodeAt(0)); // Decode base64 to byte array
|
|
29
|
+
const intArray = Array.from(rqcBytes);
|
|
30
|
+
// Reverse the array
|
|
31
|
+
const reversedArray = [...intArray].reverse();
|
|
32
|
+
// Interleave original and reversed arrays
|
|
33
|
+
const interleavedArray = [];
|
|
34
|
+
for (let i = 0; i < intArray.length; i++) {
|
|
35
|
+
interleavedArray.push(intArray[i], reversedArray[i]);
|
|
36
|
+
}
|
|
37
|
+
// Convert the interleaved array to a base64 string and return
|
|
38
|
+
return btoa(String.fromCharCode(...interleavedArray)); // Convert the byte array to base64 string
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Decodes the transformed rqx back to the original rqc base64 string.
|
|
42
|
+
* @param {string} rqx - The transformed key in base64 format.
|
|
43
|
+
* @returns {string} The original rqc in base64 format.
|
|
44
|
+
*/
|
|
45
|
+
static decodeRQX(rqx) {
|
|
46
|
+
const interleavedArray = Uint8Array.from(atob(rqx), (c) => c.charCodeAt(0)); // Decode base64 to byte array
|
|
47
|
+
// Separate the original and reversed arrays from the interleaved array
|
|
48
|
+
const originalArray = [];
|
|
49
|
+
const reversedArray = [];
|
|
50
|
+
for (let i = 0; i < interleavedArray.length; i += 2) {
|
|
51
|
+
originalArray.push(interleavedArray[i]);
|
|
52
|
+
reversedArray.push(interleavedArray[i + 1]);
|
|
53
|
+
}
|
|
54
|
+
// Verify reversedArray matches the reverse of originalArray (optional, for validation)
|
|
55
|
+
if (reversedArray.join() !== [...originalArray].reverse().join()) {
|
|
56
|
+
throw new Error("Decoded rqx does not match original rqc format.");
|
|
57
|
+
}
|
|
58
|
+
// Convert the original array back to a base64 string representing the original rqc
|
|
59
|
+
return btoa(String.fromCharCode(...originalArray)); // Convert to base64 string
|
|
60
|
+
}
|
|
61
|
+
static hashData(inputJson) {
|
|
62
|
+
const jsonString = JSON.stringify(inputJson, Object.keys(inputJson).sort());
|
|
63
|
+
const hashString = hash(jsonStringToBytes(jsonString));
|
|
64
|
+
return hashString.toString();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Generates a SHA-256 hash for a given string.
|
|
68
|
+
* Validates that the input is a non-empty string.
|
|
69
|
+
* @param {string} string - The string to hash.
|
|
70
|
+
* @returns {string} The hash of the string in hexadecimal format.
|
|
71
|
+
* @throws {Error} If the input is not a valid non-empty string.
|
|
72
|
+
*/
|
|
73
|
+
static hashString(string) {
|
|
74
|
+
const hashed = hash(new TextEncoder().encode(string));
|
|
75
|
+
return hashed.toString();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Verifies that the hash of a decoded JSON object matches a provided hash.
|
|
79
|
+
* @param {Object} decodedJson - The JSON object to verify.
|
|
80
|
+
* @param {string} providedHash - The hash to compare against.
|
|
81
|
+
* @returns {boolean} True if the hashes match, false otherwise.
|
|
82
|
+
* @throws {Error} If inputs are invalid.
|
|
83
|
+
*/
|
|
84
|
+
static verifyHashJSON(decodedJson, providedHash) {
|
|
85
|
+
const generatedHash = this.hashData(decodedJson);
|
|
86
|
+
return generatedHash === providedHash;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Verifies that the hash of a given string matches a provided hash.
|
|
90
|
+
* @param {string} string - The string to verify.
|
|
91
|
+
* @param {string} providedHash - The hash to compare against.
|
|
92
|
+
* @returns {boolean} True if the hashes match, false otherwise.
|
|
93
|
+
* @throws {Error} If inputs are invalid.
|
|
94
|
+
*/
|
|
95
|
+
static verifyHashString(string, providedHash) {
|
|
96
|
+
if (typeof string !== "string" || string.trim() === "") {
|
|
97
|
+
throw new Error("Invalid input: 'string' must be a non-empty string.");
|
|
98
|
+
}
|
|
99
|
+
if (typeof providedHash !== "string" || providedHash.trim() === "") {
|
|
100
|
+
throw new Error("Invalid input: 'providedHash' must be a non-empty string.");
|
|
101
|
+
}
|
|
102
|
+
const generatedHash = this.hashString(string);
|
|
103
|
+
return generatedHash === providedHash;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Decrypts an encrypted payload using AES-GCM encryption and returns the original JSON.
|
|
107
|
+
* @param {string} encrypted - The encrypted object data.
|
|
108
|
+
* @returns {Object} The decrypted and parsed JSON object.
|
|
109
|
+
* @throws {Error} Throws an error if decryption fails.
|
|
110
|
+
*/
|
|
111
|
+
static decryptPayload(encrypted) {
|
|
112
|
+
try {
|
|
113
|
+
const serialized = encryptedDataFromBase64(encrypted);
|
|
114
|
+
const { rqc, iv, cipher } = serialized;
|
|
115
|
+
const aes = new AES(rqc);
|
|
116
|
+
const gcm = new GCM(aes);
|
|
117
|
+
const decrypted = gcm.open(iv, cipher);
|
|
118
|
+
if (!decrypted) {
|
|
119
|
+
throw new Error("Decryption failed or payload was tampered.");
|
|
120
|
+
}
|
|
121
|
+
const base64String = arrayToBase64(decrypted);
|
|
122
|
+
const parsedData = base64ToJson(base64String);
|
|
123
|
+
return parsedData;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
throw new Error(`Decryption failed: ${error}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Encrypts a JSON object using AES-GCM encryption with the provided cipher key.
|
|
131
|
+
* @param {Object} json - The JSON object to encrypt.
|
|
132
|
+
* @param {string} rqc - The 32-byte cipher key in base64 format.
|
|
133
|
+
* @returns {Object} An object containing the encrypted payload and the transformed key, potentially URL-encoded.
|
|
134
|
+
* @throws {Error} Throws an error if encryption fails.
|
|
135
|
+
*/
|
|
136
|
+
static encryptPayload(json, rqc) {
|
|
137
|
+
try {
|
|
138
|
+
const jsonString = JSON.stringify(json);
|
|
139
|
+
const data = jsonStringToBytes(jsonString);
|
|
140
|
+
const bufferRQC = base64ToUint8Array(rqc);
|
|
141
|
+
const iv = randomBytes(12);
|
|
142
|
+
const aes = new AES(bufferRQC);
|
|
143
|
+
const gcm = new GCM(aes);
|
|
144
|
+
const cipher = gcm.seal(iv, data);
|
|
145
|
+
const encrypted = { rqc: bufferRQC, iv, cipher };
|
|
146
|
+
const serialized = encryptedDataToBase64(encrypted);
|
|
147
|
+
return serialized;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
throw new Error(`Encryption failed: ${error}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export default APITranscoder;
|
|
155
|
+
export function generateRQKey(auth) {
|
|
156
|
+
const now = Math.floor(Date.now() / 1000);
|
|
157
|
+
const appendedKey = now + ":" + auth + ":" + secureReverse(auth, true);
|
|
158
|
+
return btoa(appendedKey);
|
|
159
|
+
}
|
|
160
|
+
export function getAuthFromRQKey(rqkey) {
|
|
161
|
+
const decodedKey = getDecodedRQKey(rqkey);
|
|
162
|
+
const auth = decodedKey.split(":")[1];
|
|
163
|
+
return auth;
|
|
164
|
+
}
|
|
165
|
+
export function validateRQKey(rqkey) {
|
|
166
|
+
const auth = getAuthFromRQKey(rqkey);
|
|
167
|
+
const secureKey = getSecureKeyFromRQKey(rqkey);
|
|
168
|
+
if (auth !== secureKey) {
|
|
169
|
+
console.error("Access denied. This key is not valid.");
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
export function getDecodedRQKey(rqkey) {
|
|
175
|
+
return atob(rqkey);
|
|
176
|
+
}
|
|
177
|
+
export function getSecureKeyFromRQKey(rqkey) {
|
|
178
|
+
const decodedKey = getDecodedRQKey(rqkey);
|
|
179
|
+
const secureKey = decodeReverse(decodedKey.split(":")[2], "string", true);
|
|
180
|
+
return secureKey;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Converts input to a string and reverses it.
|
|
184
|
+
* If the input is a number, it is converted to a string.
|
|
185
|
+
* If the input is an object (JSON), it is stringified.
|
|
186
|
+
* If secure is true, it returns a Base64 encoded result.
|
|
187
|
+
* @param {any} input - The input to reverse.
|
|
188
|
+
* @param {boolean} secure - Whether to Base64 encode the result.
|
|
189
|
+
* @returns {string} - The reversed string, possibly encoded.
|
|
190
|
+
*/
|
|
191
|
+
export function secureReverse(input, secure = true) {
|
|
192
|
+
let str;
|
|
193
|
+
if (typeof input === "number") {
|
|
194
|
+
str = input.toString();
|
|
195
|
+
}
|
|
196
|
+
else if (typeof input === "object") {
|
|
197
|
+
str = JSON.stringify(input);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
str = input;
|
|
201
|
+
}
|
|
202
|
+
let reversedString = str.split("").reverse().join("");
|
|
203
|
+
if (secure) {
|
|
204
|
+
// reversedString = btoa(reversedString);
|
|
205
|
+
reversedString = Buffer.from(reversedString).toString("base64");
|
|
206
|
+
}
|
|
207
|
+
return reversedString;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Decodes the reversed string based on the mode provided.
|
|
211
|
+
* If secure is true, the reversed string is decoded from Base64 first.
|
|
212
|
+
* @param {string} reversedString - The reversed string (possibly Base64 encoded).
|
|
213
|
+
* @param {string|null} mode - The mode to decode ('json', 'number', or 'string').
|
|
214
|
+
* @param {boolean} secure - Whether the input is Base64 encoded.
|
|
215
|
+
* @returns {any} - The decoded result based on the mode.
|
|
216
|
+
*/
|
|
217
|
+
export function decodeReverse(reversedString, mode = "string", secure = true) {
|
|
218
|
+
// If secure is true, decode the reversed string from Base64
|
|
219
|
+
if (secure) {
|
|
220
|
+
reversedString = atob(reversedString);
|
|
221
|
+
}
|
|
222
|
+
const restoredString = reversedString.split("").reverse().join("");
|
|
223
|
+
if (mode === "json") {
|
|
224
|
+
try {
|
|
225
|
+
return JSON.parse(restoredString);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
throw new Error(`Invalid JSON format. ${error}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if (mode === "number") {
|
|
232
|
+
const number = parseFloat(restoredString);
|
|
233
|
+
if (!number) {
|
|
234
|
+
throw new Error("Reversed string is not a valid number");
|
|
235
|
+
}
|
|
236
|
+
return number;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
return restoredString; // Treat as string if mode is 'string' or null
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/* ---------------------------------
|
|
243
|
+
* JSON <-> Bytes Utilities
|
|
244
|
+
* --------------------------------- */
|
|
245
|
+
/**
|
|
246
|
+
* Converts a stringified JSON into a Uint8Array (UTF-8 bytes)
|
|
247
|
+
*/
|
|
248
|
+
export function jsonStringToBytes(jsonString) {
|
|
249
|
+
return new TextEncoder().encode(jsonString);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Converts a Uint8Array (UTF-8 bytes) back into a stringified JSON
|
|
253
|
+
*/
|
|
254
|
+
export function bytesToJsonString(bytes) {
|
|
255
|
+
return new TextDecoder().decode(bytes);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Converts EncryptedData (Uint8Array fields) into base64 strings
|
|
259
|
+
* for safe transport / storage (JSON, URLs, APIs, etc.)
|
|
260
|
+
*/
|
|
261
|
+
export function encryptedDataToBase64(data) {
|
|
262
|
+
return {
|
|
263
|
+
rqc: arrayToBase64(data.rqc),
|
|
264
|
+
iv: arrayToBase64(data.iv),
|
|
265
|
+
cipher: arrayToBase64(data.cipher),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Converts SerializedEncryptedData (base64 fields)
|
|
270
|
+
* back into EncryptedData (Uint8Array fields)
|
|
271
|
+
*/
|
|
272
|
+
export function encryptedDataFromBase64(data) {
|
|
273
|
+
return {
|
|
274
|
+
rqc: base64ToUint8Array(data.rqc),
|
|
275
|
+
iv: base64ToUint8Array(data.iv),
|
|
276
|
+
cipher: base64ToUint8Array(data.cipher),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
export function base64ToJson(base64) {
|
|
280
|
+
try {
|
|
281
|
+
// 🔹 Step 0: Clean the string (remove whitespace / newlines)
|
|
282
|
+
const cleanBase64 = base64.replace(/\s+/g, "");
|
|
283
|
+
let jsonString;
|
|
284
|
+
if (typeof atob === "function") {
|
|
285
|
+
// 🔹 Browser / Service Worker (atob is available globally)
|
|
286
|
+
jsonString = decodeURIComponent(atob(cleanBase64)
|
|
287
|
+
.split("")
|
|
288
|
+
.map((c) => `%${c.charCodeAt(0).toString(16).padStart(2, "0")}`)
|
|
289
|
+
.join(""));
|
|
290
|
+
}
|
|
291
|
+
else if (typeof Buffer !== "undefined") {
|
|
292
|
+
// 🔹 Node.js fallback (Buffer is available)
|
|
293
|
+
jsonString = Buffer.from(cleanBase64, "base64").toString("utf-8");
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
throw new Error("No base64 decode available in this environment");
|
|
297
|
+
}
|
|
298
|
+
// 🔹 Parse JSON
|
|
299
|
+
return JSON.parse(jsonString);
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
console.error("base64ToJson error:", err);
|
|
303
|
+
throw new Error("Failed to decode Base64 JSON");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type IDBPDatabase } from "idb";
|
|
2
|
+
export interface ZWorldIDBSaveData {
|
|
3
|
+
id: string;
|
|
4
|
+
data: Blob;
|
|
5
|
+
savedAt: number;
|
|
6
|
+
}
|
|
7
|
+
interface ZWorldAutosaveSchema {
|
|
8
|
+
majikdata: ZWorldIDBSaveData;
|
|
9
|
+
}
|
|
10
|
+
export declare function initDB(): Promise<IDBPDatabase<ZWorldAutosaveSchema>>;
|
|
11
|
+
export declare function idbSaveBlob(id: string, data: Blob): Promise<void>;
|
|
12
|
+
export declare function idbLoadBlob(id: string): Promise<ZWorldIDBSaveData | undefined>;
|
|
13
|
+
export declare function deleteBlob(id: string): Promise<void>;
|
|
14
|
+
export declare function clearAllBlobs(): Promise<void>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// lib/indexedDB.ts
|
|
2
|
+
import { openDB } from "idb";
|
|
3
|
+
let dbPromise;
|
|
4
|
+
export function initDB() {
|
|
5
|
+
if (!dbPromise) {
|
|
6
|
+
dbPromise = openDB("MajikAutosaveDB", 1, {
|
|
7
|
+
upgrade(db) {
|
|
8
|
+
if (!db.objectStoreNames.contains("majikdata")) {
|
|
9
|
+
db.createObjectStore("majikdata", { keyPath: "id" });
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
return dbPromise;
|
|
15
|
+
}
|
|
16
|
+
export async function idbSaveBlob(id, data) {
|
|
17
|
+
const db = await initDB();
|
|
18
|
+
await db.put("majikdata", { id, data, savedAt: Date.now() });
|
|
19
|
+
}
|
|
20
|
+
export async function idbLoadBlob(id) {
|
|
21
|
+
try {
|
|
22
|
+
const db = await initDB();
|
|
23
|
+
return await db.get("majikdata", id);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
console.error(`Failed to load blob with id "${id}":`, err);
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function deleteBlob(id) {
|
|
31
|
+
const db = await initDB();
|
|
32
|
+
return db.delete("majikdata", id);
|
|
33
|
+
}
|
|
34
|
+
export async function clearAllBlobs() {
|
|
35
|
+
const db = await initDB();
|
|
36
|
+
return db.clear("majikdata");
|
|
37
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface MajikFileData {
|
|
2
|
+
/** JSON object */
|
|
3
|
+
j: unknown;
|
|
4
|
+
/** STX Timestamp */
|
|
5
|
+
s: string;
|
|
6
|
+
/** Version */
|
|
7
|
+
v: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function importMajikFileData(file: File): Promise<MajikFileData>;
|
|
10
|
+
export declare function loadSavedMajikFileData(file: Blob): Promise<MajikFileData>;
|
|
11
|
+
export declare function autoSaveMajikFileData(json: unknown, version?: string): Blob;
|
|
12
|
+
export declare function exportMajikFileData(json: unknown, name: string, version?: string): void;
|
|
13
|
+
export declare function prepareToBlobFile(data: string): Blob;
|
|
14
|
+
export declare function base64EncodeUtf8(str: string): string;
|
|
15
|
+
export declare function base64DecodeUtf8(base64: string): string;
|
|
16
|
+
export {};
|