File size: 3,388 Bytes
ff36e71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// AES Encryption wrapper using Web Crypto API
// In a real mobile app, this should be backed by the OS Keychain (via Capacitor Secure Storage).
// For now, we generate a master key and store it in localStorage if it doesn't exist.

function isCryptoAvailable() {
  return window.crypto && window.crypto.subtle;
}

// Fallback for non-secure contexts (HTTP via local IP on mobile)
// In a real app, this should only run via HTTPS or capacitor:// where crypto.subtle is available.
function fallbackEncrypt(text) {
  // Simple Base64 + rot13 obfuscation to prevent plain text in localstorage for development
  const rot13 = str => str.replace(/[a-zA-Z]/g, c => String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26));
  return 'FALLBACK:' + btoa(rot13(text));
}

function fallbackDecrypt(text) {
  if (!text.startsWith('FALLBACK:')) return text; // If it's old plain text or we screwed up
  const rot13 = str => str.replace(/[a-zA-Z]/g, c => String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26));
  return rot13(atob(text.replace('FALLBACK:', '')));
}

async function getMasterKey() {
  if (!isCryptoAvailable()) return null;

  let rawKey = localStorage.getItem('iserv_master_key');
  if (!rawKey) {
    const key = await window.crypto.subtle.generateKey(
      { name: "AES-GCM", length: 256 },
      true,
      ["encrypt", "decrypt"]
    );
    const exported = await window.crypto.subtle.exportKey("raw", key);
    rawKey = btoa(String.fromCharCode(...new Uint8Array(exported)));
    localStorage.setItem('iserv_master_key', rawKey);
  }
  
  const keyBytes = Uint8Array.from(atob(rawKey), c => c.charCodeAt(0));
  return window.crypto.subtle.importKey(
    "raw",
    keyBytes,
    { name: "AES-GCM" },
    false,
    ["encrypt", "decrypt"]
  );
}

export async function encryptPassword(password) {
  if (!password) return "";
  if (!isCryptoAvailable()) {
    console.warn("Crypto Subtle API not available. Using fallback obfuscation.");
    return fallbackEncrypt(password);
  }

  const key = await getMasterKey();
  const iv = window.crypto.getRandomValues(new Uint8Array(12));
  const encoded = new TextEncoder().encode(password);
  
  const encrypted = await window.crypto.subtle.encrypt(
    { name: "AES-GCM", iv: iv },
    key,
    encoded
  );
  
  const ivBase64 = btoa(String.fromCharCode(...iv));
  const encryptedBase64 = btoa(String.fromCharCode(...new Uint8Array(encrypted)));
  return `${ivBase64}:${encryptedBase64}`;
}

export async function decryptPassword(encryptedStr) {
  if (!encryptedStr) return "";
  if (encryptedStr.startsWith('FALLBACK:')) {
    return fallbackDecrypt(encryptedStr);
  }
  if (!isCryptoAvailable()) {
    console.warn("Crypto Subtle API not available and string is not a fallback. Cannot decrypt.");
    return "";
  }

  try {
    const parts = encryptedStr.split(':');
    if (parts.length !== 2) return "";
    
    const key = await getMasterKey();
    const iv = Uint8Array.from(atob(parts[0]), c => c.charCodeAt(0));
    const encryptedData = Uint8Array.from(atob(parts[1]), c => c.charCodeAt(0));
    
    const decrypted = await window.crypto.subtle.decrypt(
      { name: "AES-GCM", iv: iv },
      key,
      encryptedData
    );

    
    return new TextDecoder().decode(decrypted);
  } catch (e) {
    console.error("Decryption failed", e);
    return "";
  }
}