@lumiapassport/ui-kit 1.2.0 → 1.3.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/README.md +26 -4
- package/dist/iframe/main.js +1251 -2
- package/dist/iframe/main.js.map +1 -1
- package/dist/index.cjs +901 -701
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +962 -762
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/dist/iframe/main.js
CHANGED
|
@@ -678,6 +678,546 @@ var init_keccak256 = __esm({
|
|
|
678
678
|
}
|
|
679
679
|
});
|
|
680
680
|
|
|
681
|
+
// src/iframe/lib/backup/crypto-utils.ts
|
|
682
|
+
var crypto_utils_exports = {};
|
|
683
|
+
__export(crypto_utils_exports, {
|
|
684
|
+
base64ToBytes: () => base64ToBytes,
|
|
685
|
+
bytesToBase64: () => bytesToBase64,
|
|
686
|
+
bytesToHex: () => bytesToHex2,
|
|
687
|
+
decryptKeyshare: () => decryptKeyshare,
|
|
688
|
+
deriveBackupPasswordFromPasskey: () => deriveBackupPasswordFromPasskey,
|
|
689
|
+
deriveKEKFromPasskey: () => deriveKEKFromPasskey,
|
|
690
|
+
deriveKeyFromPassword: () => deriveKeyFromPassword,
|
|
691
|
+
encryptKeyshare: () => encryptKeyshare,
|
|
692
|
+
envelopeDecryptKeyshare: () => envelopeDecryptKeyshare,
|
|
693
|
+
envelopeDecryptKeyshareWithPassword: () => envelopeDecryptKeyshareWithPassword,
|
|
694
|
+
envelopeEncryptKeyshare: () => envelopeEncryptKeyshare,
|
|
695
|
+
envelopeEncryptKeyshareWithPassword: () => envelopeEncryptKeyshareWithPassword
|
|
696
|
+
});
|
|
697
|
+
function base64ToBytes(base64) {
|
|
698
|
+
const binaryString = atob(base64);
|
|
699
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
700
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
701
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
702
|
+
}
|
|
703
|
+
return bytes;
|
|
704
|
+
}
|
|
705
|
+
function bytesToBase64(bytes) {
|
|
706
|
+
let binary = "";
|
|
707
|
+
const len = bytes.byteLength;
|
|
708
|
+
for (let i = 0; i < len; i++) {
|
|
709
|
+
binary += String.fromCharCode(bytes[i]);
|
|
710
|
+
}
|
|
711
|
+
return btoa(binary);
|
|
712
|
+
}
|
|
713
|
+
function bytesToHex2(bytes) {
|
|
714
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
715
|
+
}
|
|
716
|
+
async function deriveKeyFromPassword(password, salt) {
|
|
717
|
+
const encoder3 = new TextEncoder();
|
|
718
|
+
const passwordKey = await crypto.subtle.importKey(
|
|
719
|
+
"raw",
|
|
720
|
+
encoder3.encode(password),
|
|
721
|
+
{ name: "PBKDF2" },
|
|
722
|
+
false,
|
|
723
|
+
["deriveBits", "deriveKey"]
|
|
724
|
+
);
|
|
725
|
+
return crypto.subtle.deriveKey(
|
|
726
|
+
{
|
|
727
|
+
name: "PBKDF2",
|
|
728
|
+
salt,
|
|
729
|
+
iterations: 15e4,
|
|
730
|
+
hash: "SHA-256"
|
|
731
|
+
},
|
|
732
|
+
passwordKey,
|
|
733
|
+
{ name: "AES-GCM", length: 256 },
|
|
734
|
+
false,
|
|
735
|
+
["encrypt", "decrypt"]
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
async function deriveKEKFromPasskey(userId, requiredCredentialId) {
|
|
739
|
+
const challenge = crypto.getRandomValues(new Uint8Array(32));
|
|
740
|
+
const allowCredentials = requiredCredentialId ? [
|
|
741
|
+
{
|
|
742
|
+
type: "public-key",
|
|
743
|
+
id: base64ToBytes(requiredCredentialId),
|
|
744
|
+
transports: ["internal"]
|
|
745
|
+
}
|
|
746
|
+
] : void 0;
|
|
747
|
+
const credential = await navigator.credentials.get({
|
|
748
|
+
publicKey: {
|
|
749
|
+
challenge,
|
|
750
|
+
rpId: window.location.hostname,
|
|
751
|
+
userVerification: "required",
|
|
752
|
+
allowCredentials,
|
|
753
|
+
extensions: {
|
|
754
|
+
prf: {
|
|
755
|
+
eval: {
|
|
756
|
+
first: new TextEncoder().encode(`backup-kek-${userId}`)
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
if (!credential?.getClientExtensionResults) {
|
|
763
|
+
throw new Error("Passkey authentication failed");
|
|
764
|
+
}
|
|
765
|
+
const prfResults = credential.getClientExtensionResults();
|
|
766
|
+
if (!prfResults?.prf?.results?.first) {
|
|
767
|
+
throw new Error("PRF extension not supported or failed");
|
|
768
|
+
}
|
|
769
|
+
const resultCredentialId = bytesToBase64(new Uint8Array(credential.rawId));
|
|
770
|
+
return {
|
|
771
|
+
kek: prfResults.prf.results.first,
|
|
772
|
+
credentialId: resultCredentialId
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
async function deriveBackupPasswordFromPasskey(userId, credentialId) {
|
|
776
|
+
const challenge = crypto.getRandomValues(new Uint8Array(32));
|
|
777
|
+
const allowCredentials = credentialId ? [
|
|
778
|
+
{
|
|
779
|
+
type: "public-key",
|
|
780
|
+
id: base64ToBytes(credentialId),
|
|
781
|
+
transports: ["internal"]
|
|
782
|
+
}
|
|
783
|
+
] : void 0;
|
|
784
|
+
const credential = await navigator.credentials.get({
|
|
785
|
+
publicKey: {
|
|
786
|
+
challenge,
|
|
787
|
+
rpId: window.location.hostname,
|
|
788
|
+
userVerification: "required",
|
|
789
|
+
allowCredentials,
|
|
790
|
+
extensions: {
|
|
791
|
+
prf: {
|
|
792
|
+
eval: {
|
|
793
|
+
first: new TextEncoder().encode(`backup-password-${userId}`)
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
if (!credential?.getClientExtensionResults) {
|
|
800
|
+
throw new Error("Passkey authentication failed");
|
|
801
|
+
}
|
|
802
|
+
const prfResults = credential.getClientExtensionResults();
|
|
803
|
+
if (!prfResults?.prf?.results?.first) {
|
|
804
|
+
throw new Error("PRF extension not supported or failed");
|
|
805
|
+
}
|
|
806
|
+
const prfOutput = new Uint8Array(prfResults.prf.results.first);
|
|
807
|
+
const password = bytesToBase64(prfOutput);
|
|
808
|
+
const resultCredentialId = bytesToBase64(
|
|
809
|
+
new Uint8Array(credential.rawId)
|
|
810
|
+
);
|
|
811
|
+
return { password, credentialId: resultCredentialId };
|
|
812
|
+
}
|
|
813
|
+
async function encryptKeyshare(data, password, encryptionMethod, credentialId) {
|
|
814
|
+
const encoder3 = new TextEncoder();
|
|
815
|
+
const plaintext = JSON.stringify(data);
|
|
816
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
817
|
+
const passwordKey = await crypto.subtle.importKey("raw", encoder3.encode(password), "PBKDF2", false, ["deriveBits"]);
|
|
818
|
+
const keyMaterial = await crypto.subtle.deriveBits({ name: "PBKDF2", salt, iterations: 1e5, hash: "SHA-256" }, passwordKey, 256);
|
|
819
|
+
const key = await crypto.subtle.importKey("raw", keyMaterial, "AES-GCM", false, ["encrypt"]);
|
|
820
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
821
|
+
const encryptedData = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoder3.encode(plaintext));
|
|
822
|
+
const checksumData = `${data.userId}:${data.sessionId}:${encryptionMethod}`;
|
|
823
|
+
const checksumBuffer = await crypto.subtle.digest("SHA-256", encoder3.encode(checksumData));
|
|
824
|
+
const checksum = bytesToBase64(new Uint8Array(checksumBuffer));
|
|
825
|
+
const timestamp = Date.now();
|
|
826
|
+
return {
|
|
827
|
+
data: bytesToBase64(new Uint8Array(encryptedData)),
|
|
828
|
+
iv: bytesToBase64(iv),
|
|
829
|
+
salt: bytesToBase64(salt),
|
|
830
|
+
version: "1.0",
|
|
831
|
+
checksum,
|
|
832
|
+
magic: "LUMIA_BACKUP_V1",
|
|
833
|
+
encryptionMethod,
|
|
834
|
+
createdAt: timestamp,
|
|
835
|
+
credentialId: encryptionMethod === "passkey" ? credentialId : void 0
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
async function envelopeEncryptKeyshare(data, userId) {
|
|
839
|
+
const { kek: kekBytes, credentialId } = await deriveKEKFromPasskey(userId);
|
|
840
|
+
console.log("[envelopeEncryptKeyshare] Using credential ID for encryption:", credentialId);
|
|
841
|
+
const dek = crypto.getRandomValues(new Uint8Array(32));
|
|
842
|
+
const kek = await crypto.subtle.importKey("raw", kekBytes, "AES-GCM", false, ["encrypt"]);
|
|
843
|
+
const plaintext = JSON.stringify(data);
|
|
844
|
+
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
845
|
+
const dekCryptoKey = await crypto.subtle.importKey("raw", dek, "AES-GCM", false, ["encrypt"]);
|
|
846
|
+
const dataIv = crypto.getRandomValues(new Uint8Array(12));
|
|
847
|
+
const encryptedData = await crypto.subtle.encrypt({ name: "AES-GCM", iv: dataIv }, dekCryptoKey, plaintextBytes);
|
|
848
|
+
const wrapIv = crypto.getRandomValues(new Uint8Array(12));
|
|
849
|
+
const wrappedDekData = await crypto.subtle.encrypt({ name: "AES-GCM", iv: wrapIv }, kek, dek);
|
|
850
|
+
const ciphertextWithIv = new Uint8Array(dataIv.length + encryptedData.byteLength);
|
|
851
|
+
ciphertextWithIv.set(dataIv, 0);
|
|
852
|
+
ciphertextWithIv.set(new Uint8Array(encryptedData), dataIv.length);
|
|
853
|
+
const wrappedDekWithIv = new Uint8Array(wrapIv.length + wrappedDekData.byteLength);
|
|
854
|
+
wrappedDekWithIv.set(wrapIv, 0);
|
|
855
|
+
wrappedDekWithIv.set(new Uint8Array(wrappedDekData), wrapIv.length);
|
|
856
|
+
return {
|
|
857
|
+
ciphertext_share: bytesToBase64(ciphertextWithIv),
|
|
858
|
+
wrapped_dek: bytesToBase64(wrappedDekWithIv),
|
|
859
|
+
kdf: { prf: "webauthn-prf", hkdf: "hkdf-sha256", info: "client-share" },
|
|
860
|
+
alg: { data: "aes-256-gcm", wrap: "aes-256-gcm" },
|
|
861
|
+
aad: { userId: data.userId, version: 1, deviceId: "lumia-ui-kit" },
|
|
862
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
863
|
+
expiresAt: null,
|
|
864
|
+
encryptionMethod: "passkey",
|
|
865
|
+
credentialId: credentialId || void 0
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
async function envelopeEncryptKeyshareWithPassword(data, password) {
|
|
869
|
+
console.log("[envelopeEncryptKeyshareWithPassword] Encrypting with password");
|
|
870
|
+
const dek = crypto.getRandomValues(new Uint8Array(32));
|
|
871
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
872
|
+
const kekKey = await deriveKeyFromPassword(password, salt);
|
|
873
|
+
const plaintext = JSON.stringify(data);
|
|
874
|
+
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
875
|
+
const dekCryptoKey = await crypto.subtle.importKey("raw", dek, "AES-GCM", false, ["encrypt"]);
|
|
876
|
+
const dataIv = crypto.getRandomValues(new Uint8Array(12));
|
|
877
|
+
const encryptedData = await crypto.subtle.encrypt({ name: "AES-GCM", iv: dataIv }, dekCryptoKey, plaintextBytes);
|
|
878
|
+
const wrapIv = crypto.getRandomValues(new Uint8Array(12));
|
|
879
|
+
const wrappedDekData = await crypto.subtle.encrypt({ name: "AES-GCM", iv: wrapIv }, kekKey, dek);
|
|
880
|
+
const ciphertextWithIv = new Uint8Array(dataIv.length + encryptedData.byteLength);
|
|
881
|
+
ciphertextWithIv.set(dataIv, 0);
|
|
882
|
+
ciphertextWithIv.set(new Uint8Array(encryptedData), dataIv.length);
|
|
883
|
+
const wrappedDekWithIv = new Uint8Array(salt.length + wrapIv.length + wrappedDekData.byteLength);
|
|
884
|
+
wrappedDekWithIv.set(salt, 0);
|
|
885
|
+
wrappedDekWithIv.set(wrapIv, salt.length);
|
|
886
|
+
wrappedDekWithIv.set(new Uint8Array(wrappedDekData), salt.length + wrapIv.length);
|
|
887
|
+
return {
|
|
888
|
+
ciphertext_share: bytesToBase64(ciphertextWithIv),
|
|
889
|
+
wrapped_dek: bytesToBase64(wrappedDekWithIv),
|
|
890
|
+
kdf: { prf: "pbkdf2", hkdf: "hkdf-sha256", info: "client-share" },
|
|
891
|
+
alg: { data: "aes-256-gcm", wrap: "aes-256-gcm" },
|
|
892
|
+
aad: { userId: data.userId, version: 1, deviceId: "lumia-ui-kit" },
|
|
893
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
894
|
+
expiresAt: null,
|
|
895
|
+
encryptionMethod: "password"
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
async function envelopeDecryptKeyshare(envelope, userId) {
|
|
899
|
+
console.log("[iframe][envelopeDecryptKeyshare] Starting decryption process");
|
|
900
|
+
console.log("[iframe][envelopeDecryptKeyshare] Envelope credential ID:", envelope.credentialId);
|
|
901
|
+
console.log("[iframe][envelopeDecryptKeyshare] Deriving KEK from passkey...");
|
|
902
|
+
const { kek: kekBytes } = await deriveKEKFromPasskey(userId, envelope.credentialId);
|
|
903
|
+
console.log("[iframe][envelopeDecryptKeyshare] KEK bytes length:", kekBytes.byteLength);
|
|
904
|
+
console.log("[iframe][envelopeDecryptKeyshare] Importing KEK as crypto key...");
|
|
905
|
+
const kek = await crypto.subtle.importKey("raw", kekBytes, "AES-GCM", false, ["decrypt"]);
|
|
906
|
+
console.log("[iframe][envelopeDecryptKeyshare] Decrypting wrapped DEK...");
|
|
907
|
+
const wrappedDekWithIv = base64ToBytes(envelope.wrapped_dek);
|
|
908
|
+
console.log("[iframe][envelopeDecryptKeyshare] Wrapped DEK with IV length:", wrappedDekWithIv.length);
|
|
909
|
+
const wrapIv = wrappedDekWithIv.slice(0, 12);
|
|
910
|
+
const wrappedDekData = wrappedDekWithIv.slice(12);
|
|
911
|
+
console.log("[iframe][envelopeDecryptKeyshare] Wrap IV length:", wrapIv.length, "Wrapped DEK data length:", wrappedDekData.length);
|
|
912
|
+
try {
|
|
913
|
+
const dekBytes = await crypto.subtle.decrypt({ name: "AES-GCM", iv: wrapIv }, kek, wrappedDekData);
|
|
914
|
+
console.log("[iframe][envelopeDecryptKeyshare] DEK decrypted successfully, length:", dekBytes.byteLength);
|
|
915
|
+
console.log("[iframe][envelopeDecryptKeyshare] Importing DEK as crypto key...");
|
|
916
|
+
const dekCryptoKey = await crypto.subtle.importKey("raw", dekBytes, "AES-GCM", false, ["decrypt"]);
|
|
917
|
+
console.log("[iframe][envelopeDecryptKeyshare] Decrypting main data...");
|
|
918
|
+
const ciphertextWithIv = base64ToBytes(envelope.ciphertext_share);
|
|
919
|
+
console.log("[iframe][envelopeDecryptKeyshare] Ciphertext with IV length:", ciphertextWithIv.length);
|
|
920
|
+
const dataIv = ciphertextWithIv.slice(0, 12);
|
|
921
|
+
const encryptedData = ciphertextWithIv.slice(12);
|
|
922
|
+
console.log("[iframe][envelopeDecryptKeyshare] Data IV length:", dataIv.length, "Encrypted data length:", encryptedData.length);
|
|
923
|
+
const decryptedData = await crypto.subtle.decrypt({ name: "AES-GCM", iv: dataIv }, dekCryptoKey, encryptedData);
|
|
924
|
+
console.log("[iframe][envelopeDecryptKeyshare] Main data decrypted successfully, length:", decryptedData.byteLength);
|
|
925
|
+
console.log("[iframe][envelopeDecryptKeyshare] Parsing JSON...");
|
|
926
|
+
const plaintext = new TextDecoder().decode(decryptedData);
|
|
927
|
+
const result = JSON.parse(plaintext);
|
|
928
|
+
console.log("[iframe][envelopeDecryptKeyshare] Decryption completed successfully");
|
|
929
|
+
return result;
|
|
930
|
+
} catch (error) {
|
|
931
|
+
console.error("[iframe][envelopeDecryptKeyshare] Decryption failed:", error);
|
|
932
|
+
if (error instanceof Error && error.name === "OperationError") {
|
|
933
|
+
const credentialHint = envelope.credentialId ? `The backup requires a specific passkey (ID: ${envelope.credentialId.slice(-8)}...)` : "The backup was encrypted with a different passkey than the one you selected";
|
|
934
|
+
throw new Error(`Passkey mismatch: ${credentialHint}. If you have multiple passkeys, please try again and select the correct passkey that was used when creating this backup.`);
|
|
935
|
+
}
|
|
936
|
+
throw new Error("Failed to decrypt keyshare: " + (error instanceof Error ? error.message : String(error)));
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
async function envelopeDecryptKeyshareWithPassword(envelope, password) {
|
|
940
|
+
console.log("[iframe][envelopeDecryptKeyshareWithPassword] Starting password-based decryption");
|
|
941
|
+
const wrappedDekWithSaltAndIv = base64ToBytes(envelope.wrapped_dek);
|
|
942
|
+
const salt = wrappedDekWithSaltAndIv.slice(0, 16);
|
|
943
|
+
const wrapIv = wrappedDekWithSaltAndIv.slice(16, 28);
|
|
944
|
+
const wrappedDekData = wrappedDekWithSaltAndIv.slice(28);
|
|
945
|
+
console.log("[iframe][envelopeDecryptKeyshareWithPassword] Salt length:", salt.length, "Wrap IV length:", wrapIv.length, "Wrapped DEK data length:", wrappedDekData.length);
|
|
946
|
+
try {
|
|
947
|
+
const kekKey = await deriveKeyFromPassword(password, salt);
|
|
948
|
+
const dekBytes = await crypto.subtle.decrypt({ name: "AES-GCM", iv: wrapIv }, kekKey, wrappedDekData);
|
|
949
|
+
console.log("[iframe][envelopeDecryptKeyshareWithPassword] DEK decrypted successfully, length:", dekBytes.byteLength);
|
|
950
|
+
const dekCryptoKey = await crypto.subtle.importKey("raw", dekBytes, "AES-GCM", false, ["decrypt"]);
|
|
951
|
+
const ciphertextWithIv = base64ToBytes(envelope.ciphertext_share);
|
|
952
|
+
const dataIv = ciphertextWithIv.slice(0, 12);
|
|
953
|
+
const encryptedData = ciphertextWithIv.slice(12);
|
|
954
|
+
const decryptedData = await crypto.subtle.decrypt({ name: "AES-GCM", iv: dataIv }, dekCryptoKey, encryptedData);
|
|
955
|
+
console.log("[iframe][envelopeDecryptKeyshareWithPassword] Main data decrypted successfully");
|
|
956
|
+
const plaintext = new TextDecoder().decode(decryptedData);
|
|
957
|
+
const result = JSON.parse(plaintext);
|
|
958
|
+
console.log("[iframe][envelopeDecryptKeyshareWithPassword] Decryption completed successfully");
|
|
959
|
+
return result;
|
|
960
|
+
} catch (error) {
|
|
961
|
+
console.error("[iframe][envelopeDecryptKeyshareWithPassword] Decryption failed:", error);
|
|
962
|
+
if (error instanceof Error && error.name === "OperationError") {
|
|
963
|
+
throw new Error("Incorrect password: Unable to decrypt the backup with the provided password.");
|
|
964
|
+
}
|
|
965
|
+
throw new Error("Failed to decrypt keyshare with password: " + (error instanceof Error ? error.message : String(error)));
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
async function decryptKeyshare(encryptedBackup, password) {
|
|
969
|
+
console.log("[iframe][decryptKeyshare] Starting file backup decryption");
|
|
970
|
+
if (encryptedBackup.magic && encryptedBackup.magic !== "LUMIA_BACKUP_V1") {
|
|
971
|
+
throw new Error("Invalid backup file format");
|
|
972
|
+
}
|
|
973
|
+
if (encryptedBackup.version && encryptedBackup.version !== "1.0") {
|
|
974
|
+
throw new Error(`Unsupported backup version: ${encryptedBackup.version}`);
|
|
975
|
+
}
|
|
976
|
+
try {
|
|
977
|
+
const decoder = new TextDecoder();
|
|
978
|
+
const dataField = encryptedBackup.data || encryptedBackup.encryptedData;
|
|
979
|
+
if (!encryptedBackup.salt || !encryptedBackup.iv || !dataField) {
|
|
980
|
+
console.error("[iframe][decryptKeyshare] Backup structure:", Object.keys(encryptedBackup));
|
|
981
|
+
throw new Error("Missing required encrypted backup fields (salt, iv, data/encryptedData)");
|
|
982
|
+
}
|
|
983
|
+
const salt = base64ToBytes(encryptedBackup.salt);
|
|
984
|
+
const iv = base64ToBytes(encryptedBackup.iv);
|
|
985
|
+
const encryptedData = base64ToBytes(dataField);
|
|
986
|
+
const encoder3 = new TextEncoder();
|
|
987
|
+
const passwordKey = await crypto.subtle.importKey(
|
|
988
|
+
"raw",
|
|
989
|
+
encoder3.encode(password),
|
|
990
|
+
"PBKDF2",
|
|
991
|
+
false,
|
|
992
|
+
["deriveBits"]
|
|
993
|
+
);
|
|
994
|
+
const keyMaterial = await crypto.subtle.deriveBits(
|
|
995
|
+
{
|
|
996
|
+
name: "PBKDF2",
|
|
997
|
+
salt,
|
|
998
|
+
iterations: 1e5,
|
|
999
|
+
// File backups use 100,000 iterations
|
|
1000
|
+
hash: "SHA-256"
|
|
1001
|
+
},
|
|
1002
|
+
passwordKey,
|
|
1003
|
+
256
|
|
1004
|
+
);
|
|
1005
|
+
const key = await crypto.subtle.importKey(
|
|
1006
|
+
"raw",
|
|
1007
|
+
keyMaterial,
|
|
1008
|
+
"AES-GCM",
|
|
1009
|
+
false,
|
|
1010
|
+
["decrypt"]
|
|
1011
|
+
);
|
|
1012
|
+
const decryptedData = await crypto.subtle.decrypt(
|
|
1013
|
+
{ name: "AES-GCM", iv },
|
|
1014
|
+
key,
|
|
1015
|
+
encryptedData
|
|
1016
|
+
);
|
|
1017
|
+
const plaintext = decoder.decode(decryptedData);
|
|
1018
|
+
const backupData = JSON.parse(plaintext);
|
|
1019
|
+
if (encryptedBackup.checksum) {
|
|
1020
|
+
const expected = `${backupData.userId}:${backupData.sessionId}:${encryptedBackup.encryptionMethod || "password"}`;
|
|
1021
|
+
const expectedBuf = await crypto.subtle.digest("SHA-256", encoder3.encode(expected));
|
|
1022
|
+
const expectedChecksum = bytesToBase64(new Uint8Array(expectedBuf));
|
|
1023
|
+
if (expectedChecksum !== encryptedBackup.checksum) {
|
|
1024
|
+
throw new Error("Backup integrity check failed - file may be corrupted or tampered with");
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
console.log("[iframe][decryptKeyshare] File backup decrypted successfully");
|
|
1028
|
+
return backupData;
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
const message = error?.message || String(error);
|
|
1031
|
+
const name = error?.name || "Error";
|
|
1032
|
+
if (name === "OperationError" || message.includes("decrypt")) {
|
|
1033
|
+
throw new Error("Incorrect password - unable to decrypt backup file");
|
|
1034
|
+
}
|
|
1035
|
+
if (error instanceof SyntaxError || message.includes("JSON")) {
|
|
1036
|
+
throw new Error("Corrupted backup file - decryption succeeded but data is invalid");
|
|
1037
|
+
}
|
|
1038
|
+
throw new Error(`Failed to decrypt backup: ${message}`);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
var init_crypto_utils = __esm({
|
|
1042
|
+
"src/iframe/lib/backup/crypto-utils.ts"() {
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
// src/iframe/lib/backup/server-backup.ts
|
|
1047
|
+
var server_backup_exports = {};
|
|
1048
|
+
__export(server_backup_exports, {
|
|
1049
|
+
backupToServer: () => backupToServer,
|
|
1050
|
+
restoreFromServer: () => restoreFromServer
|
|
1051
|
+
});
|
|
1052
|
+
async function getShareVaultToken(scopes, accessToken) {
|
|
1053
|
+
const headers = {
|
|
1054
|
+
"Content-Type": "application/json"
|
|
1055
|
+
};
|
|
1056
|
+
if (accessToken) {
|
|
1057
|
+
headers["Authorization"] = `Bearer ${accessToken}`;
|
|
1058
|
+
}
|
|
1059
|
+
const response = await fetch(`${TSS_URL}/api/auth/token/exchange`, {
|
|
1060
|
+
method: "POST",
|
|
1061
|
+
headers,
|
|
1062
|
+
credentials: "include",
|
|
1063
|
+
body: JSON.stringify({
|
|
1064
|
+
scopes,
|
|
1065
|
+
audience: "lumia-passport-share-vault-service",
|
|
1066
|
+
ttl: 300
|
|
1067
|
+
})
|
|
1068
|
+
});
|
|
1069
|
+
if (!response.ok) {
|
|
1070
|
+
throw new Error(`Failed to get Share Vault token: ${response.status}`);
|
|
1071
|
+
}
|
|
1072
|
+
const data = await response.json();
|
|
1073
|
+
if (!data?.resourceToken) {
|
|
1074
|
+
throw new Error("Invalid token response");
|
|
1075
|
+
}
|
|
1076
|
+
return data.resourceToken;
|
|
1077
|
+
}
|
|
1078
|
+
async function uploadShareToVault(encryptedShare, accessToken) {
|
|
1079
|
+
const token = await getShareVaultToken(["share:put"], accessToken);
|
|
1080
|
+
const idempotencyKey = crypto.randomUUID ? crypto.randomUUID() : `backup-${Date.now()}`;
|
|
1081
|
+
const response = await fetch(`${SHARE_VAULT_URL}/v1/shares/me`, {
|
|
1082
|
+
method: "PUT",
|
|
1083
|
+
headers: {
|
|
1084
|
+
"Content-Type": "application/json",
|
|
1085
|
+
"Authorization": `Bearer ${token}`,
|
|
1086
|
+
"Idempotency-Key": idempotencyKey
|
|
1087
|
+
},
|
|
1088
|
+
body: JSON.stringify(encryptedShare)
|
|
1089
|
+
});
|
|
1090
|
+
if (!response.ok) {
|
|
1091
|
+
const errorText = await response.text();
|
|
1092
|
+
throw new Error(`Failed to upload share: ${response.status} ${response.statusText} - ${errorText}`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
async function backupToServer(data, tokenClient, password, accessToken) {
|
|
1096
|
+
let encryptedShare;
|
|
1097
|
+
if (password) {
|
|
1098
|
+
encryptedShare = await envelopeEncryptKeyshareWithPassword(data, password);
|
|
1099
|
+
} else {
|
|
1100
|
+
encryptedShare = await envelopeEncryptKeyshare(data, data.userId);
|
|
1101
|
+
}
|
|
1102
|
+
await uploadShareToVault(encryptedShare, accessToken);
|
|
1103
|
+
}
|
|
1104
|
+
async function downloadShareFromVault(accessToken) {
|
|
1105
|
+
const token = await getShareVaultToken(["share:get"], accessToken);
|
|
1106
|
+
const response = await fetch(`${SHARE_VAULT_URL}/v1/shares/me`, {
|
|
1107
|
+
method: "GET",
|
|
1108
|
+
headers: {
|
|
1109
|
+
"Authorization": `Bearer ${token}`,
|
|
1110
|
+
"X-Client-Device-Id": "lumia-ui-kit",
|
|
1111
|
+
"X-Client-Device-Name": "Lumia UI Kit"
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
if (!response.ok) {
|
|
1115
|
+
if (response.status === 404) {
|
|
1116
|
+
throw new Error("No backup found on server for this user");
|
|
1117
|
+
}
|
|
1118
|
+
const errorText = await response.text();
|
|
1119
|
+
throw new Error(`Failed to download share: ${response.status} ${response.statusText} - ${errorText}`);
|
|
1120
|
+
}
|
|
1121
|
+
const envelope = await response.json();
|
|
1122
|
+
if (!envelope.ciphertext_share || !envelope.wrapped_dek || !envelope.kdf || !envelope.alg || !envelope.aad) {
|
|
1123
|
+
throw new Error("Invalid envelope structure received from Share Vault");
|
|
1124
|
+
}
|
|
1125
|
+
return envelope;
|
|
1126
|
+
}
|
|
1127
|
+
async function restoreFromServer(userId, tokenClient, password, accessToken) {
|
|
1128
|
+
console.log("[iframe][restoreFromServer] Starting restore for userId:", userId);
|
|
1129
|
+
console.log("[iframe][restoreFromServer] Using password:", !!password);
|
|
1130
|
+
console.log("[iframe][restoreFromServer] Downloading share from vault...");
|
|
1131
|
+
const envelope = await downloadShareFromVault(accessToken);
|
|
1132
|
+
console.log("[iframe][restoreFromServer] Envelope encryption method:", envelope.encryptionMethod);
|
|
1133
|
+
const { envelopeDecryptKeyshare: envelopeDecryptKeyshare2, envelopeDecryptKeyshareWithPassword: envelopeDecryptKeyshareWithPassword2 } = await Promise.resolve().then(() => (init_crypto_utils(), crypto_utils_exports));
|
|
1134
|
+
const backupData = password ? await envelopeDecryptKeyshareWithPassword2(envelope, password) : await envelopeDecryptKeyshare2(envelope, userId);
|
|
1135
|
+
if (backupData.userId !== userId) {
|
|
1136
|
+
throw new Error("Server backup does not match current user");
|
|
1137
|
+
}
|
|
1138
|
+
console.log("[iframe][restoreFromServer] Decryption successful, returning backup data");
|
|
1139
|
+
return backupData;
|
|
1140
|
+
}
|
|
1141
|
+
var SHARE_VAULT_URL, TSS_URL;
|
|
1142
|
+
var init_server_backup = __esm({
|
|
1143
|
+
"src/iframe/lib/backup/server-backup.ts"() {
|
|
1144
|
+
init_crypto_utils();
|
|
1145
|
+
SHARE_VAULT_URL = typeof window.__LUMIA_SERVICES__ !== "undefined" && window.__LUMIA_SERVICES__?.shareVaultUrl || "https://api.lumiapassport.com/vault";
|
|
1146
|
+
TSS_URL = typeof window.__LUMIA_SERVICES__ !== "undefined" && window.__LUMIA_SERVICES__?.tssUrl || "https://api.lumiapassport.com/tss";
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
// src/iframe/lib/backup/local-backup.ts
|
|
1151
|
+
var local_backup_exports = {};
|
|
1152
|
+
__export(local_backup_exports, {
|
|
1153
|
+
backupToLocalFile: () => backupToLocalFile,
|
|
1154
|
+
restoreFromLocalFile: () => restoreFromLocalFile
|
|
1155
|
+
});
|
|
1156
|
+
async function backupToLocalFile(data, password) {
|
|
1157
|
+
let encryptionPassword = password;
|
|
1158
|
+
let credentialId;
|
|
1159
|
+
if (!encryptionPassword) {
|
|
1160
|
+
const result = await deriveBackupPasswordFromPasskey(data.userId);
|
|
1161
|
+
encryptionPassword = result.password;
|
|
1162
|
+
credentialId = result.credentialId;
|
|
1163
|
+
}
|
|
1164
|
+
const encrypted = await encryptKeyshare(
|
|
1165
|
+
data,
|
|
1166
|
+
encryptionPassword,
|
|
1167
|
+
password ? "password" : "passkey",
|
|
1168
|
+
credentialId
|
|
1169
|
+
);
|
|
1170
|
+
const backupData = {
|
|
1171
|
+
...encrypted,
|
|
1172
|
+
userId: data.userId,
|
|
1173
|
+
createdAt: data.createdAt
|
|
1174
|
+
};
|
|
1175
|
+
const blob = new Blob([JSON.stringify(backupData, null, 2)], {
|
|
1176
|
+
type: "application/json"
|
|
1177
|
+
});
|
|
1178
|
+
const url = URL.createObjectURL(blob);
|
|
1179
|
+
const a = document.createElement("a");
|
|
1180
|
+
a.href = url;
|
|
1181
|
+
a.download = `lumia-passport-backup-${data.userId}-${Date.now()}.json`;
|
|
1182
|
+
document.body.appendChild(a);
|
|
1183
|
+
a.click();
|
|
1184
|
+
document.body.removeChild(a);
|
|
1185
|
+
URL.revokeObjectURL(url);
|
|
1186
|
+
}
|
|
1187
|
+
async function restoreFromLocalFile(fileContent, userId, password) {
|
|
1188
|
+
console.log("[iframe][restoreFromLocalFile] Starting restore from local file");
|
|
1189
|
+
const encryptedBackup = JSON.parse(fileContent);
|
|
1190
|
+
let decryptionPassword;
|
|
1191
|
+
let credentialId;
|
|
1192
|
+
if (password) {
|
|
1193
|
+
decryptionPassword = password;
|
|
1194
|
+
} else {
|
|
1195
|
+
const credentialIdFromBackup = encryptedBackup.encryptionMethod === "passkey" ? encryptedBackup.credentialId : void 0;
|
|
1196
|
+
const result = await deriveBackupPasswordFromPasskey(
|
|
1197
|
+
userId,
|
|
1198
|
+
credentialIdFromBackup
|
|
1199
|
+
).catch(() => {
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
"Restore requires either password or passkey authentication"
|
|
1202
|
+
);
|
|
1203
|
+
});
|
|
1204
|
+
decryptionPassword = result.password;
|
|
1205
|
+
credentialId = result.credentialId;
|
|
1206
|
+
}
|
|
1207
|
+
const { decryptKeyshare: decryptKeyshare2 } = await Promise.resolve().then(() => (init_crypto_utils(), crypto_utils_exports));
|
|
1208
|
+
const backupData = await decryptKeyshare2(encryptedBackup, decryptionPassword);
|
|
1209
|
+
if (backupData.userId !== userId) {
|
|
1210
|
+
throw new Error("Backup file does not match current user");
|
|
1211
|
+
}
|
|
1212
|
+
console.log("[iframe][restoreFromLocalFile] Restore successful");
|
|
1213
|
+
return backupData;
|
|
1214
|
+
}
|
|
1215
|
+
var init_local_backup = __esm({
|
|
1216
|
+
"src/iframe/lib/backup/local-backup.ts"() {
|
|
1217
|
+
init_crypto_utils();
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
|
|
681
1221
|
// src/iframe/lib/secure-messenger.ts
|
|
682
1222
|
var SecureMessenger = class {
|
|
683
1223
|
constructor() {
|
|
@@ -843,6 +1383,12 @@ var SecureMessenger = class {
|
|
|
843
1383
|
"CHECK_KEYSHARE",
|
|
844
1384
|
"GET_TRUSTED_APPS",
|
|
845
1385
|
"REMOVE_TRUSTED_APP",
|
|
1386
|
+
"CREATE_BACKUP",
|
|
1387
|
+
"ENCRYPT_BACKUP_DATA",
|
|
1388
|
+
"RESTORE_BACKUP",
|
|
1389
|
+
"RESTORE_FROM_FILE",
|
|
1390
|
+
"GET_BACKUP_STATUS",
|
|
1391
|
+
"GET_CLOUD_PROVIDERS",
|
|
846
1392
|
"REQUEST_NEW_TOKEN",
|
|
847
1393
|
"RESPONSE",
|
|
848
1394
|
"ERROR",
|
|
@@ -2622,11 +3168,477 @@ var AuthorizationManager = class {
|
|
|
2622
3168
|
}
|
|
2623
3169
|
};
|
|
2624
3170
|
|
|
3171
|
+
// src/iframe/lib/backup-manager.ts
|
|
3172
|
+
init_server_backup();
|
|
3173
|
+
init_local_backup();
|
|
3174
|
+
|
|
3175
|
+
// src/iframe/lib/backup/cloud-backup.ts
|
|
3176
|
+
init_crypto_utils();
|
|
3177
|
+
|
|
3178
|
+
// src/iframe/lib/backup/cloudStorage.ts
|
|
3179
|
+
var GoogleDriveProvider = class {
|
|
3180
|
+
constructor() {
|
|
3181
|
+
this.name = "Google Drive";
|
|
3182
|
+
this.id = "google-drive";
|
|
3183
|
+
this.icon = "\u{1F4C1}";
|
|
3184
|
+
// Can be replaced with proper icon
|
|
3185
|
+
this.accessToken = null;
|
|
3186
|
+
this.CLIENT_ID = typeof window.__GOOGLE_DRIVE_CLIENT_ID__ !== "undefined" && window.__GOOGLE_DRIVE_CLIENT_ID__ || "";
|
|
3187
|
+
this.DISCOVERY_DOC = "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest";
|
|
3188
|
+
this.SCOPES = "https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/drive.appdata";
|
|
3189
|
+
this.gapiInitialized = false;
|
|
3190
|
+
this.gisInitialized = false;
|
|
3191
|
+
this.initializeAPIs();
|
|
3192
|
+
}
|
|
3193
|
+
async initializeAPIs() {
|
|
3194
|
+
if (!window.gapi) {
|
|
3195
|
+
await this.loadScript("https://apis.google.com/js/api.js");
|
|
3196
|
+
}
|
|
3197
|
+
if (!window.google?.accounts) {
|
|
3198
|
+
await this.loadScript("https://accounts.google.com/gsi/client");
|
|
3199
|
+
}
|
|
3200
|
+
if (!this.gapiInitialized) {
|
|
3201
|
+
await new Promise((resolve) => {
|
|
3202
|
+
window.gapi.load("client", resolve);
|
|
3203
|
+
});
|
|
3204
|
+
await window.gapi.client.init({
|
|
3205
|
+
discoveryDocs: [this.DISCOVERY_DOC]
|
|
3206
|
+
});
|
|
3207
|
+
this.gapiInitialized = true;
|
|
3208
|
+
console.log("[GoogleDrive] Google API client initialized");
|
|
3209
|
+
}
|
|
3210
|
+
if (!this.gisInitialized) {
|
|
3211
|
+
this.gisInitialized = true;
|
|
3212
|
+
console.log("[GoogleDrive] Google Identity Services initialized");
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
loadScript(src) {
|
|
3216
|
+
return new Promise((resolve, reject) => {
|
|
3217
|
+
if (document.querySelector(`script[src="${src}"]`)) {
|
|
3218
|
+
resolve();
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
const script = document.createElement("script");
|
|
3222
|
+
script.src = src;
|
|
3223
|
+
script.onload = () => resolve();
|
|
3224
|
+
script.onerror = () => reject(new Error(`Failed to load ${src}`));
|
|
3225
|
+
document.head.appendChild(script);
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
isAvailable() {
|
|
3229
|
+
return !!this.CLIENT_ID && typeof window !== "undefined";
|
|
3230
|
+
}
|
|
3231
|
+
async authenticate() {
|
|
3232
|
+
if (!this.isAvailable()) {
|
|
3233
|
+
throw new Error(
|
|
3234
|
+
"Google Drive integration is not available in this environment"
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
await this.initializeAPIs();
|
|
3238
|
+
return new Promise((resolve, reject) => {
|
|
3239
|
+
try {
|
|
3240
|
+
const tokenClient = window.google.accounts.oauth2.initTokenClient({
|
|
3241
|
+
client_id: this.CLIENT_ID,
|
|
3242
|
+
scope: this.SCOPES,
|
|
3243
|
+
callback: (response) => {
|
|
3244
|
+
if (response.error) {
|
|
3245
|
+
console.error("[GoogleDrive] Authentication error:", response.error);
|
|
3246
|
+
reject(new Error(`Google Drive authentication failed: ${response.error}`));
|
|
3247
|
+
return;
|
|
3248
|
+
}
|
|
3249
|
+
this.accessToken = response.access_token;
|
|
3250
|
+
window.gapi.client.setToken({ access_token: this.accessToken });
|
|
3251
|
+
console.log("[GoogleDrive] Successfully authenticated");
|
|
3252
|
+
resolve(true);
|
|
3253
|
+
}
|
|
3254
|
+
});
|
|
3255
|
+
tokenClient.requestAccessToken({ prompt: "consent" });
|
|
3256
|
+
} catch (error) {
|
|
3257
|
+
console.error("[GoogleDrive] Authentication initialization failed:", error);
|
|
3258
|
+
reject(new Error(`Google Drive authentication setup failed: ${error}`));
|
|
3259
|
+
}
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
isAuthenticated() {
|
|
3263
|
+
return !!this.accessToken;
|
|
3264
|
+
}
|
|
3265
|
+
async signOut() {
|
|
3266
|
+
if (this.accessToken) {
|
|
3267
|
+
window.google?.accounts.oauth2.revoke(this.accessToken);
|
|
3268
|
+
this.accessToken = null;
|
|
3269
|
+
window.gapi?.client.setToken(null);
|
|
3270
|
+
console.log("[GoogleDrive] Signed out successfully");
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
async upload(fileName, content, usePrivateStorage = true) {
|
|
3274
|
+
if (!this.isAuthenticated()) {
|
|
3275
|
+
throw new Error("Not authenticated with Google Drive");
|
|
3276
|
+
}
|
|
3277
|
+
await this.initializeAPIs();
|
|
3278
|
+
if (usePrivateStorage) {
|
|
3279
|
+
try {
|
|
3280
|
+
const fileId = await this.uploadToAppDataFolder(fileName, content);
|
|
3281
|
+
console.log("[GoogleDrive] File uploaded to appDataFolder successfully:", fileId);
|
|
3282
|
+
return fileId;
|
|
3283
|
+
} catch (error) {
|
|
3284
|
+
console.warn("[GoogleDrive] AppDataFolder upload failed, trying fallback to regular folder:", error);
|
|
3285
|
+
const fileId = await this.uploadToAppFolder(fileName, content);
|
|
3286
|
+
console.log("[GoogleDrive] File uploaded to app folder successfully:", fileId);
|
|
3287
|
+
return fileId;
|
|
3288
|
+
}
|
|
3289
|
+
} else {
|
|
3290
|
+
try {
|
|
3291
|
+
const fileId = await this.uploadToAppFolder(fileName, content);
|
|
3292
|
+
console.log("[GoogleDrive] File uploaded to app folder successfully:", fileId);
|
|
3293
|
+
return fileId;
|
|
3294
|
+
} catch (error) {
|
|
3295
|
+
console.error("[GoogleDrive] Upload failed:", error);
|
|
3296
|
+
throw new Error(
|
|
3297
|
+
`Failed to upload to Google Drive: ${error instanceof Error ? error.message : String(error)}`
|
|
3298
|
+
);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
async uploadToAppDataFolder(fileName, content) {
|
|
3303
|
+
const metadata = {
|
|
3304
|
+
name: fileName,
|
|
3305
|
+
parents: ["appDataFolder"]
|
|
3306
|
+
// Store in app-specific folder for privacy
|
|
3307
|
+
};
|
|
3308
|
+
return this.performUpload(metadata, content);
|
|
3309
|
+
}
|
|
3310
|
+
async uploadToAppFolder(fileName, content) {
|
|
3311
|
+
const folderName = "Lumia Passport Backups";
|
|
3312
|
+
const folderId = await this.findOrCreateFolder(folderName);
|
|
3313
|
+
const metadata = {
|
|
3314
|
+
name: fileName,
|
|
3315
|
+
parents: [folderId]
|
|
3316
|
+
};
|
|
3317
|
+
return this.performUpload(metadata, content);
|
|
3318
|
+
}
|
|
3319
|
+
async findOrCreateFolder(folderName) {
|
|
3320
|
+
const searchResponse = await fetch(
|
|
3321
|
+
`https://www.googleapis.com/drive/v3/files?q=name='${folderName}' and mimeType='application/vnd.google-apps.folder' and trashed=false`,
|
|
3322
|
+
{
|
|
3323
|
+
headers: { Authorization: `Bearer ${this.accessToken}` }
|
|
3324
|
+
}
|
|
3325
|
+
);
|
|
3326
|
+
if (!searchResponse.ok) {
|
|
3327
|
+
throw new Error(`Failed to search for folder: ${searchResponse.status}`);
|
|
3328
|
+
}
|
|
3329
|
+
const searchResult = await searchResponse.json();
|
|
3330
|
+
if (searchResult.files && searchResult.files.length > 0) {
|
|
3331
|
+
return searchResult.files[0].id;
|
|
3332
|
+
}
|
|
3333
|
+
const createResponse = await fetch("https://www.googleapis.com/drive/v3/files", {
|
|
3334
|
+
method: "POST",
|
|
3335
|
+
headers: {
|
|
3336
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
3337
|
+
"Content-Type": "application/json"
|
|
3338
|
+
},
|
|
3339
|
+
body: JSON.stringify({
|
|
3340
|
+
name: folderName,
|
|
3341
|
+
mimeType: "application/vnd.google-apps.folder"
|
|
3342
|
+
})
|
|
3343
|
+
});
|
|
3344
|
+
if (!createResponse.ok) {
|
|
3345
|
+
throw new Error(`Failed to create folder: ${createResponse.status}`);
|
|
3346
|
+
}
|
|
3347
|
+
const createResult = await createResponse.json();
|
|
3348
|
+
console.log(`[GoogleDrive] Created folder '${folderName}':`, createResult.id);
|
|
3349
|
+
return createResult.id;
|
|
3350
|
+
}
|
|
3351
|
+
async performUpload(metadata, content) {
|
|
3352
|
+
const form = new FormData();
|
|
3353
|
+
form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }));
|
|
3354
|
+
form.append("file", new Blob([content], { type: "application/json" }));
|
|
3355
|
+
const response = await fetch(
|
|
3356
|
+
"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart",
|
|
3357
|
+
{
|
|
3358
|
+
method: "POST",
|
|
3359
|
+
headers: {
|
|
3360
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
3361
|
+
},
|
|
3362
|
+
body: form
|
|
3363
|
+
}
|
|
3364
|
+
);
|
|
3365
|
+
if (!response.ok) {
|
|
3366
|
+
const errorText = await response.text();
|
|
3367
|
+
throw new Error(
|
|
3368
|
+
`Google Drive upload failed: ${response.status} ${response.statusText} - ${errorText}`
|
|
3369
|
+
);
|
|
3370
|
+
}
|
|
3371
|
+
const result = await response.json();
|
|
3372
|
+
return result.id;
|
|
3373
|
+
}
|
|
3374
|
+
};
|
|
3375
|
+
function getAvailableCloudProviders() {
|
|
3376
|
+
const providers = [];
|
|
3377
|
+
const googleDrive = new GoogleDriveProvider();
|
|
3378
|
+
if (googleDrive.isAvailable()) {
|
|
3379
|
+
providers.push(googleDrive);
|
|
3380
|
+
}
|
|
3381
|
+
return providers;
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
// src/iframe/lib/backup/cloud-backup.ts
|
|
3385
|
+
async function backupToCloud(data, providerId, password) {
|
|
3386
|
+
const encryptionMethod = password ? "password" : "passkey";
|
|
3387
|
+
let backupPassword;
|
|
3388
|
+
let credentialId;
|
|
3389
|
+
if (password) {
|
|
3390
|
+
backupPassword = password;
|
|
3391
|
+
} else {
|
|
3392
|
+
const result = await deriveBackupPasswordFromPasskey(data.userId).catch(() => {
|
|
3393
|
+
throw new Error("Backup requires either custom password or passkey authentication");
|
|
3394
|
+
});
|
|
3395
|
+
backupPassword = result.password;
|
|
3396
|
+
credentialId = result.credentialId;
|
|
3397
|
+
console.log("[backupToCloud] Using credential ID from passkey:", credentialId);
|
|
3398
|
+
}
|
|
3399
|
+
const encryptedBackup = await encryptKeyshare(data, backupPassword, encryptionMethod, credentialId);
|
|
3400
|
+
const timestamp = Date.now();
|
|
3401
|
+
const fileName = `lumia-keyshare-backup-${data.userId}-${timestamp}.json`;
|
|
3402
|
+
const fileContent = JSON.stringify(encryptedBackup, null, 2);
|
|
3403
|
+
const providers = getAvailableCloudProviders();
|
|
3404
|
+
if (providers.length === 0) {
|
|
3405
|
+
throw new Error(
|
|
3406
|
+
"No cloud storage providers available in this environment"
|
|
3407
|
+
);
|
|
3408
|
+
}
|
|
3409
|
+
const provider = providerId ? providers.find((p) => p.id === providerId) : providers[0];
|
|
3410
|
+
if (!provider) {
|
|
3411
|
+
throw new Error(
|
|
3412
|
+
providerId ? `Cloud provider '${providerId}' not available` : "No cloud storage provider available"
|
|
3413
|
+
);
|
|
3414
|
+
}
|
|
3415
|
+
console.info(`[BACKUP] Using ${provider.name} for cloud backup`);
|
|
3416
|
+
if (!provider.isAuthenticated()) {
|
|
3417
|
+
console.info(`[BACKUP] Authenticating with ${provider.name}`);
|
|
3418
|
+
const authenticated = await provider.authenticate();
|
|
3419
|
+
if (!authenticated) {
|
|
3420
|
+
throw new Error(`Failed to authenticate with ${provider.name}`);
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
console.info(`[BACKUP] Uploading backup to ${provider.name}`);
|
|
3424
|
+
const fileId = await provider.upload(fileName, fileContent, true);
|
|
3425
|
+
console.info(`[BACKUP] Successfully uploaded backup to ${provider.name}, file ID: ${fileId}`);
|
|
3426
|
+
}
|
|
3427
|
+
function getCloudProviders() {
|
|
3428
|
+
const providers = getAvailableCloudProviders();
|
|
3429
|
+
return providers.map((p) => ({
|
|
3430
|
+
id: p.id,
|
|
3431
|
+
name: p.name,
|
|
3432
|
+
available: p.isAvailable()
|
|
3433
|
+
}));
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
// src/iframe/lib/backup-manager.ts
|
|
3437
|
+
var BACKUP_VERSION = "1.0";
|
|
3438
|
+
var BackupManager = class {
|
|
3439
|
+
constructor() {
|
|
3440
|
+
this.tokenClient = new TokenRefreshApiClient();
|
|
3441
|
+
console.log("[iframe][BackupManager] Initialized");
|
|
3442
|
+
}
|
|
3443
|
+
/**
|
|
3444
|
+
* Get current keyshare data from iframe storage
|
|
3445
|
+
*/
|
|
3446
|
+
getCurrentKeyshareBackupData(userId) {
|
|
3447
|
+
console.log("[iframe][BackupManager] Getting keyshare data for userId:", userId);
|
|
3448
|
+
try {
|
|
3449
|
+
const keyshareData = localStorage.getItem(`tss.${userId}.keyshare`);
|
|
3450
|
+
const sessionId = localStorage.getItem(`tss.${userId}.sessionId`);
|
|
3451
|
+
const ownerAddress = localStorage.getItem(`tss.${userId}.ownerAddress`);
|
|
3452
|
+
console.log("[iframe][BackupManager] localStorage keys check:", {
|
|
3453
|
+
hasKeyshare: !!keyshareData,
|
|
3454
|
+
hasSession: !!sessionId,
|
|
3455
|
+
hasOwner: !!ownerAddress,
|
|
3456
|
+
keyshareLength: keyshareData?.length
|
|
3457
|
+
});
|
|
3458
|
+
if (!keyshareData || !sessionId || !ownerAddress) {
|
|
3459
|
+
console.warn("[iframe][BackupManager] Missing keyshare data - cannot create backup");
|
|
3460
|
+
return null;
|
|
3461
|
+
}
|
|
3462
|
+
console.log("[iframe][BackupManager] Successfully retrieved all keyshare data");
|
|
3463
|
+
return {
|
|
3464
|
+
userId,
|
|
3465
|
+
sessionId,
|
|
3466
|
+
keyshare: keyshareData,
|
|
3467
|
+
ownerAddress,
|
|
3468
|
+
createdAt: Date.now(),
|
|
3469
|
+
version: BACKUP_VERSION
|
|
3470
|
+
};
|
|
3471
|
+
} catch (error) {
|
|
3472
|
+
console.error("[iframe][BackupManager] Error getting keyshare data:", error);
|
|
3473
|
+
return null;
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
/**
|
|
3477
|
+
* Get backup status for all methods
|
|
3478
|
+
*/
|
|
3479
|
+
getBackupStatus(userId) {
|
|
3480
|
+
const statusData = localStorage.getItem(`lumia-passport.backup.status.${userId}`);
|
|
3481
|
+
if (statusData) {
|
|
3482
|
+
try {
|
|
3483
|
+
return JSON.parse(statusData);
|
|
3484
|
+
} catch (error) {
|
|
3485
|
+
console.error("[iframe][BackupManager] Failed to parse backup status:", error);
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
return {
|
|
3489
|
+
server: { lastBackup: void 0, error: void 0 },
|
|
3490
|
+
cloud: { lastBackup: void 0, error: void 0 },
|
|
3491
|
+
local: { lastBackup: void 0, error: void 0 }
|
|
3492
|
+
};
|
|
3493
|
+
}
|
|
3494
|
+
/**
|
|
3495
|
+
* Update backup status for a specific method
|
|
3496
|
+
*/
|
|
3497
|
+
updateBackupStatus(userId, method, status) {
|
|
3498
|
+
const currentStatus = this.getBackupStatus(userId);
|
|
3499
|
+
currentStatus[method] = { ...currentStatus[method], ...status };
|
|
3500
|
+
localStorage.setItem(
|
|
3501
|
+
`lumia-passport.backup.status.${userId}`,
|
|
3502
|
+
JSON.stringify(currentStatus)
|
|
3503
|
+
);
|
|
3504
|
+
console.log(`[iframe][BackupManager] Updated ${method} backup status for ${userId}`);
|
|
3505
|
+
}
|
|
3506
|
+
/**
|
|
3507
|
+
* Create backup using specified method
|
|
3508
|
+
*/
|
|
3509
|
+
async createBackup(userId, request, accessToken) {
|
|
3510
|
+
console.log("[iframe][BackupManager] Create backup request:", request);
|
|
3511
|
+
const data = this.getCurrentKeyshareBackupData(userId);
|
|
3512
|
+
if (!data) {
|
|
3513
|
+
const error = "No keyshare data found for backup";
|
|
3514
|
+
console.error("[iframe][BackupManager]", error);
|
|
3515
|
+
return {
|
|
3516
|
+
success: false,
|
|
3517
|
+
method: request.method,
|
|
3518
|
+
timestamp: Date.now(),
|
|
3519
|
+
error
|
|
3520
|
+
};
|
|
3521
|
+
}
|
|
3522
|
+
try {
|
|
3523
|
+
switch (request.method) {
|
|
3524
|
+
case "server":
|
|
3525
|
+
await backupToServer(data, this.tokenClient, request.password, accessToken);
|
|
3526
|
+
break;
|
|
3527
|
+
case "local":
|
|
3528
|
+
await backupToLocalFile(data, request.password);
|
|
3529
|
+
break;
|
|
3530
|
+
case "cloud":
|
|
3531
|
+
await backupToCloud(data, request.cloudProvider, request.password);
|
|
3532
|
+
break;
|
|
3533
|
+
default:
|
|
3534
|
+
throw new Error(`Unknown backup method: ${request.method}`);
|
|
3535
|
+
}
|
|
3536
|
+
const timestamp = Date.now();
|
|
3537
|
+
this.updateBackupStatus(userId, request.method, {
|
|
3538
|
+
lastBackup: timestamp,
|
|
3539
|
+
error: void 0
|
|
3540
|
+
});
|
|
3541
|
+
console.log(`[iframe][BackupManager] \u2705 ${request.method} backup completed`);
|
|
3542
|
+
return {
|
|
3543
|
+
success: true,
|
|
3544
|
+
method: request.method,
|
|
3545
|
+
timestamp
|
|
3546
|
+
};
|
|
3547
|
+
} catch (error) {
|
|
3548
|
+
const errorMessage = error.message || "Backup failed";
|
|
3549
|
+
console.error("[iframe][BackupManager] Backup failed:", errorMessage);
|
|
3550
|
+
this.updateBackupStatus(userId, request.method, {
|
|
3551
|
+
error: errorMessage
|
|
3552
|
+
});
|
|
3553
|
+
return {
|
|
3554
|
+
success: false,
|
|
3555
|
+
method: request.method,
|
|
3556
|
+
timestamp: Date.now(),
|
|
3557
|
+
error: errorMessage
|
|
3558
|
+
};
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
/**
|
|
3562
|
+
* Restore keyshare from server backup
|
|
3563
|
+
*/
|
|
3564
|
+
async restoreFromServer(userId, password, accessToken) {
|
|
3565
|
+
console.log("[iframe][BackupManager] Restore from server request for userId:", userId);
|
|
3566
|
+
try {
|
|
3567
|
+
const { restoreFromServer: restoreFromServer2 } = await Promise.resolve().then(() => (init_server_backup(), server_backup_exports));
|
|
3568
|
+
const backupData = await restoreFromServer2(userId, this.tokenClient, password, accessToken);
|
|
3569
|
+
console.log("[iframe][BackupManager] \u2705 Server restore completed");
|
|
3570
|
+
localStorage.setItem(`tss.${userId}.keyshare`, backupData.keyshare);
|
|
3571
|
+
localStorage.setItem(`tss.${userId}.sessionId`, backupData.sessionId);
|
|
3572
|
+
localStorage.setItem(`tss.${userId}.ownerAddress`, backupData.ownerAddress);
|
|
3573
|
+
console.log("[iframe][BackupManager] Keyshare saved to localStorage");
|
|
3574
|
+
return backupData;
|
|
3575
|
+
} catch (error) {
|
|
3576
|
+
const errorMessage = error.message || "Restore failed";
|
|
3577
|
+
console.error("[iframe][BackupManager] Restore failed:", errorMessage);
|
|
3578
|
+
throw error;
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
/**
|
|
3582
|
+
* Encrypt backup data without uploading (for cloud/local backups)
|
|
3583
|
+
* Returns encrypted data that parent can upload/download
|
|
3584
|
+
*/
|
|
3585
|
+
async encryptBackupData(userId, password) {
|
|
3586
|
+
console.log("[iframe][BackupManager] Encrypting backup data for userId:", userId);
|
|
3587
|
+
const data = this.getCurrentKeyshareBackupData(userId);
|
|
3588
|
+
if (!data) {
|
|
3589
|
+
throw new Error("No keyshare data found for backup");
|
|
3590
|
+
}
|
|
3591
|
+
const { encryptKeyshare: encryptKeyshare2, deriveBackupPasswordFromPasskey: deriveBackupPasswordFromPasskey2 } = await Promise.resolve().then(() => (init_crypto_utils(), crypto_utils_exports));
|
|
3592
|
+
let encryptionPassword = password;
|
|
3593
|
+
let credentialId;
|
|
3594
|
+
if (!encryptionPassword) {
|
|
3595
|
+
const result = await deriveBackupPasswordFromPasskey2(userId);
|
|
3596
|
+
encryptionPassword = result.password;
|
|
3597
|
+
credentialId = result.credentialId;
|
|
3598
|
+
}
|
|
3599
|
+
const encrypted = await encryptKeyshare2(
|
|
3600
|
+
data,
|
|
3601
|
+
encryptionPassword,
|
|
3602
|
+
password ? "password" : "passkey",
|
|
3603
|
+
credentialId
|
|
3604
|
+
);
|
|
3605
|
+
console.log("[iframe][BackupManager] \u2705 Backup data encrypted");
|
|
3606
|
+
return encrypted;
|
|
3607
|
+
}
|
|
3608
|
+
/**
|
|
3609
|
+
* Restore keyshare from local file backup
|
|
3610
|
+
*/
|
|
3611
|
+
async restoreFromLocalFile(fileContent, userId, password) {
|
|
3612
|
+
console.log("[iframe][BackupManager] Restore from local file request for userId:", userId);
|
|
3613
|
+
try {
|
|
3614
|
+
const { restoreFromLocalFile: restoreFromLocalFile2 } = await Promise.resolve().then(() => (init_local_backup(), local_backup_exports));
|
|
3615
|
+
const backupData = await restoreFromLocalFile2(fileContent, userId, password);
|
|
3616
|
+
console.log("[iframe][BackupManager] \u2705 Local file restore completed");
|
|
3617
|
+
localStorage.setItem(`tss.${userId}.keyshare`, backupData.keyshare);
|
|
3618
|
+
localStorage.setItem(`tss.${userId}.sessionId`, backupData.sessionId);
|
|
3619
|
+
localStorage.setItem(`tss.${userId}.ownerAddress`, backupData.ownerAddress);
|
|
3620
|
+
console.log("[iframe][BackupManager] Keyshare saved to localStorage");
|
|
3621
|
+
return backupData;
|
|
3622
|
+
} catch (error) {
|
|
3623
|
+
const errorMessage = error.message || "Local file restore failed";
|
|
3624
|
+
console.error("[iframe][BackupManager] Local file restore failed:", errorMessage);
|
|
3625
|
+
throw error;
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
/**
|
|
3629
|
+
* Get available cloud providers
|
|
3630
|
+
*/
|
|
3631
|
+
getAvailableCloudProviders() {
|
|
3632
|
+
return getCloudProviders();
|
|
3633
|
+
}
|
|
3634
|
+
};
|
|
3635
|
+
|
|
2625
3636
|
// src/iframe/main.ts
|
|
3637
|
+
var IFRAME_VERSION = "1.2.1";
|
|
2626
3638
|
var IframeWallet = class {
|
|
2627
3639
|
constructor() {
|
|
2628
3640
|
console.log("=".repeat(60));
|
|
2629
|
-
console.log(
|
|
3641
|
+
console.log(` Lumia Passport Secure Wallet - iframe version ${IFRAME_VERSION}`);
|
|
2630
3642
|
console.log("=".repeat(60));
|
|
2631
3643
|
this.messenger = new SecureMessenger();
|
|
2632
3644
|
this.sessionManager = new SessionManager();
|
|
@@ -2635,6 +3647,7 @@ var IframeWallet = class {
|
|
|
2635
3647
|
this.authManager = new AuthorizationManager();
|
|
2636
3648
|
this.storage = new StorageManager();
|
|
2637
3649
|
this.trustedApps = new TrustedAppsManager();
|
|
3650
|
+
this.backupManager = new BackupManager();
|
|
2638
3651
|
}
|
|
2639
3652
|
async initialize() {
|
|
2640
3653
|
console.log("[iframe] Initializing Lumia Passport Secure Wallet...");
|
|
@@ -2666,7 +3679,13 @@ var IframeWallet = class {
|
|
|
2666
3679
|
}
|
|
2667
3680
|
async handleMessage(message, origin) {
|
|
2668
3681
|
const { type, messageId, projectId } = message;
|
|
2669
|
-
console.log(`[iframe] \u{1F4E8} Message
|
|
3682
|
+
console.log(`[iframe] \u{1F4E8} Message received:`, {
|
|
3683
|
+
type,
|
|
3684
|
+
messageId: messageId.substring(0, 20),
|
|
3685
|
+
origin: origin.substring(0, 30),
|
|
3686
|
+
hasData: !!message.data,
|
|
3687
|
+
dataKeys: message.data ? Object.keys(message.data) : []
|
|
3688
|
+
});
|
|
2670
3689
|
try {
|
|
2671
3690
|
switch (type) {
|
|
2672
3691
|
case "SDK_AUTH":
|
|
@@ -2693,6 +3712,24 @@ var IframeWallet = class {
|
|
|
2693
3712
|
case "REMOVE_TRUSTED_APP":
|
|
2694
3713
|
await this.handleRemoveTrustedApp(message, origin);
|
|
2695
3714
|
break;
|
|
3715
|
+
case "CREATE_BACKUP":
|
|
3716
|
+
await this.handleCreateBackup(message, origin);
|
|
3717
|
+
break;
|
|
3718
|
+
case "GET_BACKUP_STATUS":
|
|
3719
|
+
await this.handleGetBackupStatus(message, origin);
|
|
3720
|
+
break;
|
|
3721
|
+
case "GET_CLOUD_PROVIDERS":
|
|
3722
|
+
await this.handleGetCloudProviders(message, origin);
|
|
3723
|
+
break;
|
|
3724
|
+
case "RESTORE_BACKUP":
|
|
3725
|
+
await this.handleRestoreBackup(message, origin);
|
|
3726
|
+
break;
|
|
3727
|
+
case "RESTORE_FROM_FILE":
|
|
3728
|
+
await this.handleRestoreFromFile(message, origin);
|
|
3729
|
+
break;
|
|
3730
|
+
case "ENCRYPT_BACKUP_DATA":
|
|
3731
|
+
await this.handleEncryptBackupData(message, origin);
|
|
3732
|
+
break;
|
|
2696
3733
|
default:
|
|
2697
3734
|
throw new Error(`Unknown message type: ${type}`);
|
|
2698
3735
|
}
|
|
@@ -2903,6 +3940,218 @@ var IframeWallet = class {
|
|
|
2903
3940
|
);
|
|
2904
3941
|
console.log(`[iframe] \u2705 REMOVE_TRUSTED_APP: App removed`);
|
|
2905
3942
|
}
|
|
3943
|
+
async handleCreateBackup(message, origin) {
|
|
3944
|
+
const { sessionToken, userId, backupRequest, accessToken } = message.data;
|
|
3945
|
+
const { messageId } = message;
|
|
3946
|
+
console.log(`[iframe] CREATE_BACKUP received:`, { userId, method: backupRequest?.method, hasSessionToken: !!sessionToken, hasAccessToken: !!accessToken, origin });
|
|
3947
|
+
if (!userId) {
|
|
3948
|
+
console.error("[iframe] CREATE_BACKUP: userId is missing!");
|
|
3949
|
+
this.messenger.sendError(messageId, "userId is required", origin);
|
|
3950
|
+
return;
|
|
3951
|
+
}
|
|
3952
|
+
if (!backupRequest) {
|
|
3953
|
+
console.error("[iframe] CREATE_BACKUP: backupRequest is missing!");
|
|
3954
|
+
this.messenger.sendError(messageId, "backupRequest is required", origin);
|
|
3955
|
+
return;
|
|
3956
|
+
}
|
|
3957
|
+
if (!this.sessionManager.validateSession(sessionToken, origin)) {
|
|
3958
|
+
console.error("[iframe] CREATE_BACKUP: Session validation failed", { sessionToken, origin });
|
|
3959
|
+
this.messenger.sendError(messageId, "Invalid session", origin);
|
|
3960
|
+
return;
|
|
3961
|
+
}
|
|
3962
|
+
console.log("[iframe] CREATE_BACKUP: Session validated, creating backup...");
|
|
3963
|
+
try {
|
|
3964
|
+
const result = await this.backupManager.createBackup(userId, backupRequest, accessToken);
|
|
3965
|
+
console.log("[iframe] CREATE_BACKUP: Backup result:", result);
|
|
3966
|
+
this.messenger.sendResponse(
|
|
3967
|
+
messageId,
|
|
3968
|
+
{
|
|
3969
|
+
type: "LUMIA_PASSPORT_BACKUP_CREATED",
|
|
3970
|
+
result
|
|
3971
|
+
},
|
|
3972
|
+
origin
|
|
3973
|
+
);
|
|
3974
|
+
if (result.success) {
|
|
3975
|
+
console.log(`[iframe] \u2705 CREATE_BACKUP: Backup created successfully`);
|
|
3976
|
+
} else {
|
|
3977
|
+
console.error(`[iframe] \u274C CREATE_BACKUP: Backup failed - ${result.error}`);
|
|
3978
|
+
}
|
|
3979
|
+
} catch (error) {
|
|
3980
|
+
console.error("[iframe] CREATE_BACKUP: Exception:", error);
|
|
3981
|
+
this.messenger.sendError(messageId, error.message || "Backup failed", origin);
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
async handleGetBackupStatus(message, origin) {
|
|
3985
|
+
const { sessionToken, userId } = message.data;
|
|
3986
|
+
const { messageId } = message;
|
|
3987
|
+
console.log(`[iframe] GET_BACKUP_STATUS: userId=${userId}`);
|
|
3988
|
+
if (!this.sessionManager.validateSession(sessionToken, origin)) {
|
|
3989
|
+
console.error("[iframe] GET_BACKUP_STATUS: Session validation failed");
|
|
3990
|
+
throw new Error("Invalid session");
|
|
3991
|
+
}
|
|
3992
|
+
const status = this.backupManager.getBackupStatus(userId);
|
|
3993
|
+
this.messenger.sendResponse(
|
|
3994
|
+
messageId,
|
|
3995
|
+
{
|
|
3996
|
+
type: "LUMIA_PASSPORT_BACKUP_STATUS",
|
|
3997
|
+
status
|
|
3998
|
+
},
|
|
3999
|
+
origin
|
|
4000
|
+
);
|
|
4001
|
+
console.log(`[iframe] \u2705 GET_BACKUP_STATUS: Status retrieved`);
|
|
4002
|
+
}
|
|
4003
|
+
async handleGetCloudProviders(message, origin) {
|
|
4004
|
+
const { sessionToken } = message.data;
|
|
4005
|
+
const { messageId } = message;
|
|
4006
|
+
console.log(`[iframe] GET_CLOUD_PROVIDERS`);
|
|
4007
|
+
if (!this.sessionManager.validateSession(sessionToken, origin)) {
|
|
4008
|
+
console.error("[iframe] GET_CLOUD_PROVIDERS: Session validation failed");
|
|
4009
|
+
throw new Error("Invalid session");
|
|
4010
|
+
}
|
|
4011
|
+
const providers = this.backupManager.getAvailableCloudProviders();
|
|
4012
|
+
this.messenger.sendResponse(
|
|
4013
|
+
messageId,
|
|
4014
|
+
{
|
|
4015
|
+
type: "LUMIA_PASSPORT_CLOUD_PROVIDERS",
|
|
4016
|
+
providers
|
|
4017
|
+
},
|
|
4018
|
+
origin
|
|
4019
|
+
);
|
|
4020
|
+
console.log(`[iframe] \u2705 GET_CLOUD_PROVIDERS: Providers retrieved`);
|
|
4021
|
+
}
|
|
4022
|
+
async handleRestoreBackup(message, origin) {
|
|
4023
|
+
const { sessionToken, userId, password, accessToken } = message.data;
|
|
4024
|
+
const { messageId } = message;
|
|
4025
|
+
console.log(`[iframe] RESTORE_BACKUP received:`, { userId, hasPassword: !!password, hasSessionToken: !!sessionToken, hasAccessToken: !!accessToken, origin });
|
|
4026
|
+
if (!userId) {
|
|
4027
|
+
console.error("[iframe] RESTORE_BACKUP: userId is missing!");
|
|
4028
|
+
this.messenger.sendError(messageId, "userId is required", origin);
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
if (!this.sessionManager.validateSession(sessionToken, origin)) {
|
|
4032
|
+
console.error("[iframe] RESTORE_BACKUP: Session validation failed", { sessionToken, origin });
|
|
4033
|
+
this.messenger.sendError(messageId, "Invalid session", origin);
|
|
4034
|
+
return;
|
|
4035
|
+
}
|
|
4036
|
+
console.log("[iframe] RESTORE_BACKUP: Session validated, restoring backup...");
|
|
4037
|
+
try {
|
|
4038
|
+
const backupData = await this.backupManager.restoreFromServer(userId, password, accessToken);
|
|
4039
|
+
console.log("[iframe] RESTORE_BACKUP: Restore successful");
|
|
4040
|
+
this.messenger.sendResponse(
|
|
4041
|
+
messageId,
|
|
4042
|
+
{
|
|
4043
|
+
type: "LUMIA_PASSPORT_BACKUP_RESTORED",
|
|
4044
|
+
result: {
|
|
4045
|
+
success: true,
|
|
4046
|
+
timestamp: Date.now(),
|
|
4047
|
+
data: backupData
|
|
4048
|
+
}
|
|
4049
|
+
},
|
|
4050
|
+
origin
|
|
4051
|
+
);
|
|
4052
|
+
console.log(`[iframe] \u2705 RESTORE_BACKUP: Backup restored successfully`);
|
|
4053
|
+
} catch (error) {
|
|
4054
|
+
console.error("[iframe] RESTORE_BACKUP: Exception:", error);
|
|
4055
|
+
this.messenger.sendResponse(
|
|
4056
|
+
messageId,
|
|
4057
|
+
{
|
|
4058
|
+
type: "LUMIA_PASSPORT_BACKUP_RESTORED",
|
|
4059
|
+
result: {
|
|
4060
|
+
success: false,
|
|
4061
|
+
timestamp: Date.now(),
|
|
4062
|
+
error: error.message || "Restore failed"
|
|
4063
|
+
}
|
|
4064
|
+
},
|
|
4065
|
+
origin
|
|
4066
|
+
);
|
|
4067
|
+
console.log(`[iframe] \u274C RESTORE_BACKUP: Restore failed - ${error.message}`);
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
async handleRestoreFromFile(message, origin) {
|
|
4071
|
+
const { sessionToken, userId, fileContent, password } = message.data;
|
|
4072
|
+
const { messageId } = message;
|
|
4073
|
+
console.log(`[iframe] RESTORE_FROM_FILE received:`, { userId, hasPassword: !!password, hasFileContent: !!fileContent, hasSessionToken: !!sessionToken, origin });
|
|
4074
|
+
if (!userId) {
|
|
4075
|
+
console.error("[iframe] RESTORE_FROM_FILE: userId is missing!");
|
|
4076
|
+
this.messenger.sendError(messageId, "userId is required", origin);
|
|
4077
|
+
return;
|
|
4078
|
+
}
|
|
4079
|
+
if (!fileContent) {
|
|
4080
|
+
console.error("[iframe] RESTORE_FROM_FILE: fileContent is missing!");
|
|
4081
|
+
this.messenger.sendError(messageId, "fileContent is required", origin);
|
|
4082
|
+
return;
|
|
4083
|
+
}
|
|
4084
|
+
if (!this.sessionManager.validateSession(sessionToken, origin)) {
|
|
4085
|
+
console.error("[iframe] RESTORE_FROM_FILE: Session validation failed", { sessionToken, origin });
|
|
4086
|
+
this.messenger.sendError(messageId, "Invalid session", origin);
|
|
4087
|
+
return;
|
|
4088
|
+
}
|
|
4089
|
+
console.log("[iframe] RESTORE_FROM_FILE: Session validated, restoring from file...");
|
|
4090
|
+
try {
|
|
4091
|
+
const backupData = await this.backupManager.restoreFromLocalFile(fileContent, userId, password);
|
|
4092
|
+
console.log("[iframe] RESTORE_FROM_FILE: Restore successful");
|
|
4093
|
+
this.messenger.sendResponse(
|
|
4094
|
+
messageId,
|
|
4095
|
+
{
|
|
4096
|
+
type: "LUMIA_PASSPORT_FILE_RESTORED",
|
|
4097
|
+
result: {
|
|
4098
|
+
success: true,
|
|
4099
|
+
timestamp: Date.now(),
|
|
4100
|
+
data: backupData
|
|
4101
|
+
}
|
|
4102
|
+
},
|
|
4103
|
+
origin
|
|
4104
|
+
);
|
|
4105
|
+
console.log(`[iframe] \u2705 RESTORE_FROM_FILE: File restored successfully`);
|
|
4106
|
+
} catch (error) {
|
|
4107
|
+
console.error("[iframe] RESTORE_FROM_FILE: Exception:", error);
|
|
4108
|
+
this.messenger.sendResponse(
|
|
4109
|
+
messageId,
|
|
4110
|
+
{
|
|
4111
|
+
type: "LUMIA_PASSPORT_FILE_RESTORED",
|
|
4112
|
+
result: {
|
|
4113
|
+
success: false,
|
|
4114
|
+
timestamp: Date.now(),
|
|
4115
|
+
error: error.message || "File restore failed"
|
|
4116
|
+
}
|
|
4117
|
+
},
|
|
4118
|
+
origin
|
|
4119
|
+
);
|
|
4120
|
+
console.log(`[iframe] \u274C RESTORE_FROM_FILE: Restore failed - ${error.message}`);
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
async handleEncryptBackupData(message, origin) {
|
|
4124
|
+
const { sessionToken, userId, password } = message.data;
|
|
4125
|
+
const { messageId } = message;
|
|
4126
|
+
console.log(`[iframe] ENCRYPT_BACKUP_DATA received:`, { userId, hasPassword: !!password, hasSessionToken: !!sessionToken, origin });
|
|
4127
|
+
if (!userId) {
|
|
4128
|
+
console.error("[iframe] ENCRYPT_BACKUP_DATA: userId is missing!");
|
|
4129
|
+
this.messenger.sendError(messageId, "userId is required", origin);
|
|
4130
|
+
return;
|
|
4131
|
+
}
|
|
4132
|
+
if (!this.sessionManager.validateSession(sessionToken, origin)) {
|
|
4133
|
+
console.error("[iframe] ENCRYPT_BACKUP_DATA: Session validation failed", { sessionToken, origin });
|
|
4134
|
+
this.messenger.sendError(messageId, "Invalid session", origin);
|
|
4135
|
+
return;
|
|
4136
|
+
}
|
|
4137
|
+
console.log("[iframe] ENCRYPT_BACKUP_DATA: Session validated, encrypting backup data...");
|
|
4138
|
+
try {
|
|
4139
|
+
const encryptedData = await this.backupManager.encryptBackupData(userId, password);
|
|
4140
|
+
console.log("[iframe] ENCRYPT_BACKUP_DATA: Encryption successful");
|
|
4141
|
+
this.messenger.sendResponse(
|
|
4142
|
+
messageId,
|
|
4143
|
+
{
|
|
4144
|
+
type: "LUMIA_PASSPORT_BACKUP_ENCRYPTED",
|
|
4145
|
+
encryptedData
|
|
4146
|
+
},
|
|
4147
|
+
origin
|
|
4148
|
+
);
|
|
4149
|
+
console.log(`[iframe] \u2705 ENCRYPT_BACKUP_DATA: Data encrypted successfully`);
|
|
4150
|
+
} catch (error) {
|
|
4151
|
+
console.error("[iframe] ENCRYPT_BACKUP_DATA: Exception:", error);
|
|
4152
|
+
this.messenger.sendError(messageId, error.message || "Encryption failed", origin);
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
2906
4155
|
displayReadyIndicator() {
|
|
2907
4156
|
const app = document.getElementById("app");
|
|
2908
4157
|
if (!app) return;
|