File size: 4,014 Bytes
0bfe2e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import {
  randomBytes,
  createCipheriv,
  createDecipheriv,
  createHash,
} from 'crypto';
import { deflateSync, inflateSync } from 'zlib';
import { Settings } from './settings';
import JSONCrush from 'jsoncrush';
import { createLogger } from './logger';

const logger = createLogger('crypto');

export const loadSecretKey = (log: boolean = false): Buffer | string => {
  const secretKey = Settings.SECRET_KEY;
  if (!secretKey) {
    console.error('No secret key provided');
    throw new Error('No secret key provided');
  }
  // must be 64 characters long and hex

  if (secretKey.length === 32) {
    // backwards compatibility
    if (log)
      logger.warn(
        'Secret key is 32 characters long, consider updating to a 64 character key and reconfiguring for better security'
      );
    return secretKey;
  } else if (secretKey.length !== 64) {
    if (log) logger.error('Secret key must be 64 characters long');
    throw new Error('Secret key must be 64 characters long');
  }

  if (!/^[0-9a-fA-F]+$/.test(secretKey)) {
    if (log) logger.error('Secret key must be a hex string (0-9, a-f)');
    throw new Error('Secret key must be a hex string (0-9, a-f)');
  }

  return Buffer.from(secretKey, 'hex');
};

const pad = (data: Buffer, blockSize: number): Buffer => {
  const padding = blockSize - (data.length % blockSize);
  return Buffer.concat([data, Buffer.alloc(padding, padding)]);
};

const unpad = (data: Buffer): Buffer => {
  const padding = data[data.length - 1];
  return data.subarray(0, data.length - padding);
};

export const crushJson = (data: string): string => {
  return JSONCrush.crush(data);
};

export const uncrushJson = (data: string): string => {
  return JSONCrush.uncrush(data);
};

export const compressData = (data: string): Buffer => {
  return deflateSync(Buffer.from(data, 'utf-8'), {
    level: 9,
  });
};

export const decompressData = (data: Buffer): string => {
  return inflateSync(data).toString('utf-8');
};

export const encryptData = (data: Buffer): { iv: string; data: string } => {
  const secretKey = loadSecretKey();

  // Then encrypt the compressed data
  const iv = randomBytes(16);
  const cipher = createCipheriv('aes-256-cbc', secretKey, iv);

  // Ensure proper padding
  const paddedData = pad(data, 16);
  const encryptedData = Buffer.concat([
    cipher.update(paddedData),
    cipher.final(),
  ]);

  return {
    iv: iv.toString('base64'),
    data: encryptedData.toString('base64'),
  };
};

export const decryptData = (encryptedData: Buffer, iv: Buffer): Buffer => {
  const secretKey = loadSecretKey();
  const decipher = createDecipheriv('aes-256-cbc', secretKey, iv);

  // Decrypt the data
  const decryptedPaddedData = Buffer.concat([
    decipher.update(encryptedData),
    decipher.final(),
  ]);

  // Remove padding
  const decryptedData = unpad(decryptedPaddedData);

  return decryptedData;
};

export function parseAndDecryptString(data: string): string | null {
  try {
    if (data.startsWith('E-') || data.startsWith('E2-')) {
      const eVersion = data.startsWith('E2-') ? 2 : 1;
      const encoding = eVersion === 1 ? 'hex' : 'base64';
      const [ivHex, encryptedHex] = data
        .replace('E-', '')
        .replace('E2-', '')
        .split('-')
        .map(decodeURIComponent);
      const iv = Buffer.from(ivHex, encoding);
      const encrypted = Buffer.from(encryptedHex, encoding);
      const decrypted = decryptData(encrypted, iv);
      const decompressed = decompressData(decrypted);
      return decompressed;
    }
    return data;
  } catch (error: any) {
    logger.error(`Failed to decrypt data: ${error.message}`);
    return null;
  }
}

export function getTextHash(text: string): string {
  const hash = createHash('sha256');
  hash.update(text);
  return hash.digest('hex');
}

export function isValueEncrypted(value?: string): boolean {
  if (!value) return false;
  const tests =
    /^E2-[^-]+-[^-]+$/.test(value) ||
    /^E-[0-9a-fA-F]{32}-[0-9a-fA-F]+$/.test(value);
  return tests;
}