@majikah/majik-universal-id-client 0.0.1
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/dist/core/contacts/majik-contact-directory.d.ts +37 -0
- package/dist/core/contacts/majik-contact-directory.js +191 -0
- package/dist/core/contacts/majik-contact.d.ts +89 -0
- package/dist/core/contacts/majik-contact.js +212 -0
- package/dist/core/crypto/constants.d.ts +56 -0
- package/dist/core/crypto/constants.js +51 -0
- package/dist/core/crypto/keystore.d.ts +228 -0
- package/dist/core/crypto/keystore.js +575 -0
- package/dist/core/identity.d.ts +63 -0
- package/dist/core/identity.js +177 -0
- package/dist/core/types.d.ts +86 -0
- package/dist/core/types.js +7 -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 +44 -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 +18 -0
- package/dist/core/utils/utilities.js +80 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/majik-universal-id-client.d.ts +757 -0
- package/dist/majik-universal-id-client.js +1618 -0
- package/package.json +55 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { hash } from "@stablelib/sha256";
|
|
2
|
+
import { arrayToBase64 } from "./utils/utilities";
|
|
3
|
+
/**
|
|
4
|
+
* Utility assertions
|
|
5
|
+
*/
|
|
6
|
+
function assert(condition, message) {
|
|
7
|
+
if (!condition)
|
|
8
|
+
throw new Error(message);
|
|
9
|
+
}
|
|
10
|
+
function assertString(value, field) {
|
|
11
|
+
assert(typeof value === "string" && value.trim().length > 0, `${field} must be a non-empty string`);
|
|
12
|
+
}
|
|
13
|
+
function assertISODate(value, field) {
|
|
14
|
+
const date = new Date(value);
|
|
15
|
+
assert(!isNaN(date.getTime()), `${field} must be a valid ISO timestamp`);
|
|
16
|
+
}
|
|
17
|
+
function sha256(input) {
|
|
18
|
+
const hashed = hash(new TextEncoder().encode(input));
|
|
19
|
+
return arrayToBase64(hashed);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* MajikMessageIdentity
|
|
23
|
+
* Immutable identity container with integrity verification
|
|
24
|
+
*/
|
|
25
|
+
export class MajikMessageIdentity {
|
|
26
|
+
// 🔒 Private backing fields
|
|
27
|
+
_id;
|
|
28
|
+
_userId;
|
|
29
|
+
_publicKey;
|
|
30
|
+
_mlKey;
|
|
31
|
+
_phash;
|
|
32
|
+
_label;
|
|
33
|
+
_timestamp;
|
|
34
|
+
_restricted;
|
|
35
|
+
/**
|
|
36
|
+
* Constructor is private to enforce controlled creation
|
|
37
|
+
*/
|
|
38
|
+
constructor(params) {
|
|
39
|
+
assertString(params.id, "id");
|
|
40
|
+
assertString(params.userId, "user_id");
|
|
41
|
+
assertString(params.publicKey, "public_key");
|
|
42
|
+
assertString(params.mlKey, "ml_key");
|
|
43
|
+
assertString(params.phash, "phash");
|
|
44
|
+
assertString(params.label, "label");
|
|
45
|
+
assertISODate(params.timestamp, "timestamp");
|
|
46
|
+
assert(typeof params.restricted === "boolean", "restricted must be boolean");
|
|
47
|
+
this._id = params.id;
|
|
48
|
+
this._userId = params.userId;
|
|
49
|
+
this._publicKey = params.publicKey;
|
|
50
|
+
this._mlKey = params.mlKey;
|
|
51
|
+
this._phash = params.phash;
|
|
52
|
+
this._label = params.label;
|
|
53
|
+
this._timestamp = params.timestamp;
|
|
54
|
+
this._restricted = params.restricted;
|
|
55
|
+
// Final integrity check at construction
|
|
56
|
+
assert(this.validateIntegrity(), "Identity integrity validation failed");
|
|
57
|
+
}
|
|
58
|
+
// ─────────────────────────────
|
|
59
|
+
// Static factory
|
|
60
|
+
// ─────────────────────────────
|
|
61
|
+
/**
|
|
62
|
+
* Create a new immutable identity from MajikUser
|
|
63
|
+
*/
|
|
64
|
+
static create(user, account, options) {
|
|
65
|
+
assert(user, "MajikUser is required");
|
|
66
|
+
const userValidResult = user.validate();
|
|
67
|
+
if (!userValidResult.isValid) {
|
|
68
|
+
throw new Error(`Invalid MajikUser: ${userValidResult.errors.join(", ")}`);
|
|
69
|
+
}
|
|
70
|
+
const label = options?.label || account?.meta?.label || user.displayName;
|
|
71
|
+
assertString(label, "label");
|
|
72
|
+
const timestamp = new Date().toISOString();
|
|
73
|
+
const publicKey = account.publicKeyBase64;
|
|
74
|
+
const phash = sha256(`${user.id}:${publicKey}:${account.id}:${account.mlKey}`);
|
|
75
|
+
return new MajikMessageIdentity({
|
|
76
|
+
id: account.id,
|
|
77
|
+
userId: user.id,
|
|
78
|
+
publicKey: publicKey,
|
|
79
|
+
mlKey: account.mlKey,
|
|
80
|
+
phash,
|
|
81
|
+
label,
|
|
82
|
+
timestamp,
|
|
83
|
+
restricted: options?.restricted ?? false,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// ─────────────────────────────
|
|
87
|
+
// Getters (safe, read-only)
|
|
88
|
+
// ─────────────────────────────
|
|
89
|
+
get id() {
|
|
90
|
+
return this._id;
|
|
91
|
+
}
|
|
92
|
+
get userID() {
|
|
93
|
+
return this._userId;
|
|
94
|
+
}
|
|
95
|
+
get publicKey() {
|
|
96
|
+
return this._publicKey;
|
|
97
|
+
}
|
|
98
|
+
get phash() {
|
|
99
|
+
return this._phash;
|
|
100
|
+
}
|
|
101
|
+
get label() {
|
|
102
|
+
return this._label;
|
|
103
|
+
}
|
|
104
|
+
get timestamp() {
|
|
105
|
+
return this._timestamp;
|
|
106
|
+
}
|
|
107
|
+
get restricted() {
|
|
108
|
+
return this._restricted;
|
|
109
|
+
}
|
|
110
|
+
// ─────────────────────────────
|
|
111
|
+
// Mutators (restricted)
|
|
112
|
+
// ─────────────────────────────
|
|
113
|
+
/**
|
|
114
|
+
* Only mutable field
|
|
115
|
+
*/
|
|
116
|
+
set label(label) {
|
|
117
|
+
assertString(label, "label");
|
|
118
|
+
this._label = label;
|
|
119
|
+
}
|
|
120
|
+
// ─────────────────────────────
|
|
121
|
+
// Identity checks
|
|
122
|
+
// ─────────────────────────────
|
|
123
|
+
/**
|
|
124
|
+
* Returns true if identity is restricted
|
|
125
|
+
*/
|
|
126
|
+
isRestricted() {
|
|
127
|
+
return this._restricted === true;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Verify identity integrity
|
|
131
|
+
* Detects tampering of id/public_key
|
|
132
|
+
*/
|
|
133
|
+
validateIntegrity() {
|
|
134
|
+
const expected = sha256(`${this._userId}:${this._publicKey}:${this._id}:${this._mlKey}`);
|
|
135
|
+
return expected === this._phash;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Explicit verification helper
|
|
139
|
+
*/
|
|
140
|
+
matches(userId, publicKey) {
|
|
141
|
+
assertString(userId, "userId");
|
|
142
|
+
assertString(publicKey, "publicKey");
|
|
143
|
+
const hash = sha256(`${userId}:${publicKey}:${this._id}:${this._mlKey}`);
|
|
144
|
+
return hash === this._phash;
|
|
145
|
+
}
|
|
146
|
+
// ─────────────────────────────
|
|
147
|
+
// Serialization
|
|
148
|
+
// ─────────────────────────────
|
|
149
|
+
toJSON() {
|
|
150
|
+
return {
|
|
151
|
+
id: this._id,
|
|
152
|
+
user_id: this._userId,
|
|
153
|
+
public_key: this._publicKey,
|
|
154
|
+
ml_key: this._mlKey,
|
|
155
|
+
phash: this._phash,
|
|
156
|
+
label: this._label,
|
|
157
|
+
timestamp: this._timestamp,
|
|
158
|
+
restricted: this._restricted,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
static fromJSON(json) {
|
|
162
|
+
const obj = typeof json === "string" ? JSON.parse(json) : json;
|
|
163
|
+
assert(typeof obj === "object" && obj !== null, "Invalid JSON object");
|
|
164
|
+
const identity = new MajikMessageIdentity({
|
|
165
|
+
id: obj.id,
|
|
166
|
+
userId: obj.user_id,
|
|
167
|
+
publicKey: obj.public_key,
|
|
168
|
+
mlKey: obj.ml_key,
|
|
169
|
+
phash: obj.phash,
|
|
170
|
+
label: obj.label,
|
|
171
|
+
timestamp: obj.timestamp,
|
|
172
|
+
restricted: obj.restricted,
|
|
173
|
+
});
|
|
174
|
+
assert(identity.validateIntegrity(), "Invalid phash in JSON");
|
|
175
|
+
return identity;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export type ISODateString = string;
|
|
2
|
+
export type MajikMessageAccountID = string;
|
|
3
|
+
export type MajikMessagePublicKey = string;
|
|
4
|
+
export type MajikMessageChatID = string;
|
|
5
|
+
export type MajikMessageThreadID = string;
|
|
6
|
+
export type MajikMessageMailID = string;
|
|
7
|
+
export interface MAJIK_API_RESPONSE {
|
|
8
|
+
success: boolean;
|
|
9
|
+
message: string;
|
|
10
|
+
code?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* types.ts — @majikah/majik-envelope
|
|
14
|
+
*
|
|
15
|
+
* ML-KEM-768 (v3) envelope types only.
|
|
16
|
+
* v1 (X25519 solo) and v2 (X25519 group) have been removed.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Single-recipient envelope payload.
|
|
20
|
+
* The ML-KEM shared secret is used directly as the AES-256-GCM key.
|
|
21
|
+
*/
|
|
22
|
+
export interface SinglePayload {
|
|
23
|
+
iv: string;
|
|
24
|
+
ciphertext: string;
|
|
25
|
+
mlKemCipherText: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Per-recipient key entry in a group envelope.
|
|
29
|
+
* encryptedAesKey = groupAesKey XOR mlKemSharedSecret (32-byte XOR one-time-pad).
|
|
30
|
+
*/
|
|
31
|
+
export interface GroupKey {
|
|
32
|
+
fingerprint: string;
|
|
33
|
+
mlKemCipherText: string;
|
|
34
|
+
encryptedAesKey: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Multi-recipient envelope payload.
|
|
38
|
+
* Message is encrypted once with a random AES key.
|
|
39
|
+
* Each recipient gets their own ML-KEM encapsulation of that AES key.
|
|
40
|
+
*/
|
|
41
|
+
export interface GroupPayload {
|
|
42
|
+
iv: string;
|
|
43
|
+
ciphertext: string;
|
|
44
|
+
keys: GroupKey[];
|
|
45
|
+
}
|
|
46
|
+
export type EnvelopePayload = SinglePayload | GroupPayload;
|
|
47
|
+
export declare function isSinglePayload(p: EnvelopePayload): p is SinglePayload;
|
|
48
|
+
export declare function isGroupPayload(p: EnvelopePayload): p is GroupPayload;
|
|
49
|
+
export interface MajikEnvelopeJSON {
|
|
50
|
+
version: 3;
|
|
51
|
+
fingerprint: string;
|
|
52
|
+
payload: EnvelopePayload;
|
|
53
|
+
plaintext?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface MAJIK_API_RESPONSE {
|
|
56
|
+
success: boolean;
|
|
57
|
+
message: string;
|
|
58
|
+
data?: unknown;
|
|
59
|
+
}
|
|
60
|
+
export interface MnemonicJSON {
|
|
61
|
+
id: string;
|
|
62
|
+
seed: string[];
|
|
63
|
+
phrase?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface MajikKeyJSON {
|
|
66
|
+
id: string;
|
|
67
|
+
label: string;
|
|
68
|
+
publicKey: string;
|
|
69
|
+
fingerprint: string;
|
|
70
|
+
encryptedPrivateKey: string;
|
|
71
|
+
salt: string;
|
|
72
|
+
backup: string;
|
|
73
|
+
timestamp: string;
|
|
74
|
+
kdfVersion: number;
|
|
75
|
+
mlKemPublicKey?: string;
|
|
76
|
+
encryptedMlKemSecretKey?: string;
|
|
77
|
+
}
|
|
78
|
+
export interface MajikKeyMetadata {
|
|
79
|
+
id: string;
|
|
80
|
+
fingerprint: string;
|
|
81
|
+
label: string;
|
|
82
|
+
timestamp: Date;
|
|
83
|
+
isLocked: boolean;
|
|
84
|
+
kdfVersion: number;
|
|
85
|
+
hasMlKem: boolean;
|
|
86
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// ─── Type Guards ──────────────────────────────────────────────────────────────
|
|
2
|
+
export function isSinglePayload(p) {
|
|
3
|
+
return "mlKemCipherText" in p && !("keys" in p);
|
|
4
|
+
}
|
|
5
|
+
export function isGroupPayload(p) {
|
|
6
|
+
return "keys" in p && Array.isArray(p.keys);
|
|
7
|
+
}
|
|
@@ -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
|
+
}
|