@voxepay/checkout 0.3.2 → 0.3.4
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/index.cjs.js +1 -0
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/voxepay-checkout.min.js +1 -1
- package/dist/voxepay-checkout.min.js.map +1 -1
- package/package.json +2 -1
- package/src/api/client.ts +110 -0
- package/src/components/modal.ts +1245 -0
- package/src/index.ts +73 -0
- package/src/types.ts +141 -0
- package/src/utils/card-validator.ts +181 -0
- package/src/utils/encryption.ts +496 -0
- package/src/utils/formatter.ts +71 -0
- package/src/voxepay.ts +202 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure JavaScript RSA Encryption for card data
|
|
3
|
+
* No Node.js crypto dependency — works directly in the browser
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ============ BigInteger Implementation (minimal, for RSA) ============
|
|
7
|
+
|
|
8
|
+
class BigInt {
|
|
9
|
+
private digits: number[];
|
|
10
|
+
private negative: boolean;
|
|
11
|
+
|
|
12
|
+
constructor(value?: string | number[], radix?: number) {
|
|
13
|
+
this.digits = [];
|
|
14
|
+
this.negative = false;
|
|
15
|
+
|
|
16
|
+
if (typeof value === 'string') {
|
|
17
|
+
this.fromString(value, radix || 10);
|
|
18
|
+
} else if (Array.isArray(value)) {
|
|
19
|
+
this.digits = value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private fromString(s: string, radix: number): void {
|
|
24
|
+
if (s.length === 0) return;
|
|
25
|
+
|
|
26
|
+
let start = 0;
|
|
27
|
+
if (s[0] === '-') {
|
|
28
|
+
this.negative = true;
|
|
29
|
+
start = 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (radix === 16) {
|
|
33
|
+
// Parse hex string
|
|
34
|
+
for (let i = s.length; i > start; i -= 7) {
|
|
35
|
+
const from = Math.max(start, i - 7);
|
|
36
|
+
this.digits.push(parseInt(s.substring(from, i), 16));
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
// Parse decimal
|
|
40
|
+
for (let i = s.length; i > start; i -= 7) {
|
|
41
|
+
const from = Math.max(start, i - 7);
|
|
42
|
+
this.digits.push(parseInt(s.substring(from, i), 10));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
this.trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private trim(): void {
|
|
49
|
+
while (this.digits.length > 0 && this.digits[this.digits.length - 1] === 0) {
|
|
50
|
+
this.digits.pop();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
isZero(): boolean {
|
|
55
|
+
return this.digits.length === 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============ Minimal forge-compatible RSA using raw math ============
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Convert a string to its hex representation
|
|
63
|
+
*/
|
|
64
|
+
const toHex = (str: string): string => {
|
|
65
|
+
let hex = '';
|
|
66
|
+
for (let i = 0; i < str.length; i++) {
|
|
67
|
+
hex += str.charCodeAt(i).toString(16).padStart(2, '0');
|
|
68
|
+
}
|
|
69
|
+
return hex;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Convert hex string to byte array
|
|
74
|
+
*/
|
|
75
|
+
const hexToBytes = (hex: string): number[] => {
|
|
76
|
+
const bytes: number[] = [];
|
|
77
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
78
|
+
bytes.push(parseInt(hex.substring(i, i + 2), 16));
|
|
79
|
+
}
|
|
80
|
+
return bytes;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Convert byte array to base64 string
|
|
85
|
+
*/
|
|
86
|
+
const bytesToBase64 = (bytes: number[]): string => {
|
|
87
|
+
if (typeof btoa !== 'undefined') {
|
|
88
|
+
// Browser
|
|
89
|
+
return btoa(String.fromCharCode(...bytes));
|
|
90
|
+
}
|
|
91
|
+
// Node.js fallback
|
|
92
|
+
return Buffer.from(bytes).toString('base64');
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// ============ BigInteger arithmetic for RSA ============
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Arbitrary precision integer using base-2^26 representation
|
|
99
|
+
*/
|
|
100
|
+
class RSABigInt {
|
|
101
|
+
static BASE = 0x4000000; // 2^26
|
|
102
|
+
static BITS = 26;
|
|
103
|
+
|
|
104
|
+
data: number[];
|
|
105
|
+
sign: number;
|
|
106
|
+
|
|
107
|
+
constructor() {
|
|
108
|
+
this.data = [];
|
|
109
|
+
this.sign = 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static fromHex(hex: string): RSABigInt {
|
|
113
|
+
const n = new RSABigInt();
|
|
114
|
+
if (hex.length === 0) return n;
|
|
115
|
+
|
|
116
|
+
n.sign = 1;
|
|
117
|
+
// Process hex string from right to left, 26 bits at a time
|
|
118
|
+
// 26 bits ≈ 6.5 hex chars, so we'll use 6 hex chars (24 bits) per digit
|
|
119
|
+
// and handle the alignment
|
|
120
|
+
let bitPos = 0;
|
|
121
|
+
let current = 0;
|
|
122
|
+
|
|
123
|
+
for (let i = hex.length - 1; i >= 0; i--) {
|
|
124
|
+
const nibble = parseInt(hex[i], 16);
|
|
125
|
+
current |= nibble << bitPos;
|
|
126
|
+
bitPos += 4;
|
|
127
|
+
if (bitPos >= RSABigInt.BITS) {
|
|
128
|
+
n.data.push(current & (RSABigInt.BASE - 1));
|
|
129
|
+
current = current >>> RSABigInt.BITS;
|
|
130
|
+
bitPos -= RSABigInt.BITS;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (current > 0 || bitPos > 0) {
|
|
134
|
+
n.data.push(current);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
n.trim();
|
|
138
|
+
return n;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
static fromBytes(bytes: number[]): RSABigInt {
|
|
142
|
+
const hex = bytes.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
143
|
+
return RSABigInt.fromHex(hex);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
trim(): void {
|
|
147
|
+
while (this.data.length > 0 && this.data[this.data.length - 1] === 0) {
|
|
148
|
+
this.data.pop();
|
|
149
|
+
}
|
|
150
|
+
if (this.data.length === 0) this.sign = 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
isZero(): boolean {
|
|
154
|
+
return this.data.length === 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
bitLength(): number {
|
|
158
|
+
if (this.data.length === 0) return 0;
|
|
159
|
+
const topWord = this.data[this.data.length - 1];
|
|
160
|
+
let bits = (this.data.length - 1) * RSABigInt.BITS;
|
|
161
|
+
let w = topWord;
|
|
162
|
+
while (w > 0) {
|
|
163
|
+
bits++;
|
|
164
|
+
w >>>= 1;
|
|
165
|
+
}
|
|
166
|
+
return bits;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
toBytes(length?: number): number[] {
|
|
170
|
+
// Convert to hex then to bytes
|
|
171
|
+
let hex = '';
|
|
172
|
+
for (let i = this.data.length - 1; i >= 0; i--) {
|
|
173
|
+
const h = this.data[i].toString(16);
|
|
174
|
+
if (i === this.data.length - 1) {
|
|
175
|
+
hex += h;
|
|
176
|
+
} else {
|
|
177
|
+
// Pad to full digit width (26 bits ≈ 7 hex chars, but only 6 guaranteed)
|
|
178
|
+
const padLen = Math.ceil(RSABigInt.BITS / 4);
|
|
179
|
+
hex += h.padStart(padLen, '0');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Ensure even length
|
|
183
|
+
if (hex.length % 2 !== 0) hex = '0' + hex;
|
|
184
|
+
|
|
185
|
+
const bytes = hexToBytes(hex);
|
|
186
|
+
|
|
187
|
+
if (length && bytes.length < length) {
|
|
188
|
+
// Pad with leading zeros
|
|
189
|
+
const padded = new Array(length - bytes.length).fill(0);
|
|
190
|
+
return padded.concat(bytes);
|
|
191
|
+
}
|
|
192
|
+
return bytes;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
clone(): RSABigInt {
|
|
196
|
+
const r = new RSABigInt();
|
|
197
|
+
r.data = this.data.slice();
|
|
198
|
+
r.sign = this.sign;
|
|
199
|
+
return r;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Compare absolute values: returns -1, 0, or 1
|
|
204
|
+
*/
|
|
205
|
+
compareAbs(other: RSABigInt): number {
|
|
206
|
+
if (this.data.length !== other.data.length) {
|
|
207
|
+
return this.data.length > other.data.length ? 1 : -1;
|
|
208
|
+
}
|
|
209
|
+
for (let i = this.data.length - 1; i >= 0; i--) {
|
|
210
|
+
if (this.data[i] !== other.data[i]) {
|
|
211
|
+
return this.data[i] > other.data[i] ? 1 : -1;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Multiply two RSABigInts
|
|
219
|
+
*/
|
|
220
|
+
multiply(other: RSABigInt): RSABigInt {
|
|
221
|
+
if (this.isZero() || other.isZero()) return new RSABigInt();
|
|
222
|
+
|
|
223
|
+
const result = new RSABigInt();
|
|
224
|
+
result.data = new Array(this.data.length + other.data.length).fill(0);
|
|
225
|
+
result.sign = 1;
|
|
226
|
+
|
|
227
|
+
for (let i = 0; i < this.data.length; i++) {
|
|
228
|
+
let carry = 0;
|
|
229
|
+
for (let j = 0; j < other.data.length; j++) {
|
|
230
|
+
const prod = this.data[i] * other.data[j] + result.data[i + j] + carry;
|
|
231
|
+
result.data[i + j] = prod & (RSABigInt.BASE - 1);
|
|
232
|
+
carry = Math.floor(prod / RSABigInt.BASE);
|
|
233
|
+
}
|
|
234
|
+
if (carry > 0) {
|
|
235
|
+
result.data[i + other.data.length] += carry;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
result.trim();
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Modular exponentiation: this^exp mod m
|
|
245
|
+
* Uses square-and-multiply method
|
|
246
|
+
*/
|
|
247
|
+
modPow(exp: RSABigInt, mod: RSABigInt): RSABigInt {
|
|
248
|
+
if (mod.isZero()) throw new Error('Division by zero');
|
|
249
|
+
|
|
250
|
+
let result = RSABigInt.fromHex('1');
|
|
251
|
+
let base = this.mod(mod);
|
|
252
|
+
const expBits = exp.bitLength();
|
|
253
|
+
|
|
254
|
+
for (let i = 0; i < expBits; i++) {
|
|
255
|
+
const wordIdx = Math.floor(i / RSABigInt.BITS);
|
|
256
|
+
const bitIdx = i % RSABigInt.BITS;
|
|
257
|
+
|
|
258
|
+
if (wordIdx < exp.data.length && (exp.data[wordIdx] & (1 << bitIdx)) !== 0) {
|
|
259
|
+
result = result.multiply(base).mod(mod);
|
|
260
|
+
}
|
|
261
|
+
base = base.multiply(base).mod(mod);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* this mod m (positive remainder)
|
|
269
|
+
*/
|
|
270
|
+
mod(m: RSABigInt): RSABigInt {
|
|
271
|
+
const result = this.divmod(m);
|
|
272
|
+
return result.remainder;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Division with remainder
|
|
277
|
+
*/
|
|
278
|
+
divmod(divisor: RSABigInt): { quotient: RSABigInt; remainder: RSABigInt } {
|
|
279
|
+
if (divisor.isZero()) throw new Error('Division by zero');
|
|
280
|
+
if (this.isZero()) return { quotient: new RSABigInt(), remainder: new RSABigInt() };
|
|
281
|
+
|
|
282
|
+
const cmp = this.compareAbs(divisor);
|
|
283
|
+
if (cmp < 0) return { quotient: new RSABigInt(), remainder: this.clone() };
|
|
284
|
+
if (cmp === 0) {
|
|
285
|
+
const one = RSABigInt.fromHex('1');
|
|
286
|
+
return { quotient: one, remainder: new RSABigInt() };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Binary long division
|
|
290
|
+
const quotient = new RSABigInt();
|
|
291
|
+
quotient.data = new Array(this.data.length).fill(0);
|
|
292
|
+
quotient.sign = 1;
|
|
293
|
+
|
|
294
|
+
let remainder = new RSABigInt();
|
|
295
|
+
|
|
296
|
+
const totalBits = this.bitLength();
|
|
297
|
+
|
|
298
|
+
for (let i = totalBits - 1; i >= 0; i--) {
|
|
299
|
+
// Shift remainder left by 1 bit
|
|
300
|
+
remainder = remainder.shiftLeft(1);
|
|
301
|
+
|
|
302
|
+
// Set lowest bit of remainder to bit i of this
|
|
303
|
+
const wordIdx = Math.floor(i / RSABigInt.BITS);
|
|
304
|
+
const bitIdx = i % RSABigInt.BITS;
|
|
305
|
+
if (wordIdx < this.data.length && (this.data[wordIdx] & (1 << bitIdx)) !== 0) {
|
|
306
|
+
if (remainder.data.length === 0) remainder.data.push(0);
|
|
307
|
+
remainder.data[0] |= 1;
|
|
308
|
+
remainder.sign = 1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// If remainder >= divisor, subtract
|
|
312
|
+
if (remainder.compareAbs(divisor) >= 0) {
|
|
313
|
+
remainder = remainder.subtract(divisor);
|
|
314
|
+
const qWordIdx = Math.floor(i / RSABigInt.BITS);
|
|
315
|
+
const qBitIdx = i % RSABigInt.BITS;
|
|
316
|
+
while (quotient.data.length <= qWordIdx) quotient.data.push(0);
|
|
317
|
+
quotient.data[qWordIdx] |= (1 << qBitIdx);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
quotient.trim();
|
|
322
|
+
remainder.trim();
|
|
323
|
+
return { quotient, remainder };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Shift left by n bits
|
|
328
|
+
*/
|
|
329
|
+
shiftLeft(n: number): RSABigInt {
|
|
330
|
+
if (this.isZero() || n === 0) return this.clone();
|
|
331
|
+
|
|
332
|
+
const wordShift = Math.floor(n / RSABigInt.BITS);
|
|
333
|
+
const bitShift = n % RSABigInt.BITS;
|
|
334
|
+
|
|
335
|
+
const result = new RSABigInt();
|
|
336
|
+
result.sign = this.sign;
|
|
337
|
+
result.data = new Array(this.data.length + wordShift + 1).fill(0);
|
|
338
|
+
|
|
339
|
+
for (let i = 0; i < this.data.length; i++) {
|
|
340
|
+
result.data[i + wordShift] |= (this.data[i] << bitShift) & (RSABigInt.BASE - 1);
|
|
341
|
+
if (bitShift > 0) {
|
|
342
|
+
result.data[i + wordShift + 1] |= this.data[i] >>> (RSABigInt.BITS - bitShift);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
result.trim();
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Subtract (absolute values, assumes this >= other)
|
|
352
|
+
*/
|
|
353
|
+
subtract(other: RSABigInt): RSABigInt {
|
|
354
|
+
const result = new RSABigInt();
|
|
355
|
+
result.sign = 1;
|
|
356
|
+
result.data = new Array(Math.max(this.data.length, other.data.length)).fill(0);
|
|
357
|
+
|
|
358
|
+
let borrow = 0;
|
|
359
|
+
for (let i = 0; i < result.data.length; i++) {
|
|
360
|
+
let diff = (this.data[i] || 0) - (other.data[i] || 0) - borrow;
|
|
361
|
+
if (diff < 0) {
|
|
362
|
+
diff += RSABigInt.BASE;
|
|
363
|
+
borrow = 1;
|
|
364
|
+
} else {
|
|
365
|
+
borrow = 0;
|
|
366
|
+
}
|
|
367
|
+
result.data[i] = diff;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
result.trim();
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ============ PKCS#1 v1.5 Padding ============
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Apply PKCS#1 v1.5 type 2 padding for encryption
|
|
379
|
+
*/
|
|
380
|
+
const pkcs1Pad = (data: number[], keyByteLen: number): number[] => {
|
|
381
|
+
if (data.length > keyByteLen - 11) {
|
|
382
|
+
throw new Error('Message too long for RSA encryption');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const padLen = keyByteLen - data.length - 3;
|
|
386
|
+
const padded = new Array(keyByteLen);
|
|
387
|
+
padded[0] = 0x00;
|
|
388
|
+
padded[1] = 0x02;
|
|
389
|
+
|
|
390
|
+
// Fill with random non-zero bytes
|
|
391
|
+
for (let i = 0; i < padLen; i++) {
|
|
392
|
+
// Use crypto.getRandomValues if available, otherwise Math.random
|
|
393
|
+
let r = 0;
|
|
394
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
395
|
+
const arr = new Uint8Array(1);
|
|
396
|
+
while (r === 0) {
|
|
397
|
+
crypto.getRandomValues(arr);
|
|
398
|
+
r = arr[0];
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
while (r === 0) {
|
|
402
|
+
r = Math.floor(Math.random() * 255) + 1;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
padded[2 + i] = r;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
padded[2 + padLen] = 0x00;
|
|
409
|
+
for (let i = 0; i < data.length; i++) {
|
|
410
|
+
padded[3 + padLen + i] = data[i];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return padded;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// ============ RSA Encryption Config ============
|
|
417
|
+
|
|
418
|
+
const RSA_CONFIG = {
|
|
419
|
+
modulus:
|
|
420
|
+
'009c7b3ba621a26c4b02f48cfc07ef6ee0aed8e12b4bd11c5cc0abf80d5206be69e1891e60fc88e2d565e2fabe4d0cf630e318a6c721c3ded718d0c530cdf050387ad0a30a336899bbda877d0ec7c7c3ffe693988bfae0ffbab71b25468c7814924f022cb5fda36e0d2c30a7161fa1c6fb5fbd7d05adbef7e68d48f8b6c5f511827c4b1c5ed15b6f20555affc4d0857ef7ab2b5c18ba22bea5d3a79bd1834badb5878d8c7a4b19da20c1f62340b1f7fbf01d2f2e97c9714a9df376ac0ea58072b2b77aeb7872b54a89667519de44d0fc73540beeaec4cb778a45eebfbefe2d817a8a8319b2bc6d9fa714f5289ec7c0dbc43496d71cf2a642cb679b0fc4072fd2cf',
|
|
421
|
+
publicExponent: '010001',
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// ============ Public API ============
|
|
425
|
+
|
|
426
|
+
export interface AuthDataParams {
|
|
427
|
+
version: string;
|
|
428
|
+
pan: string;
|
|
429
|
+
pin: string;
|
|
430
|
+
expiryDate: string;
|
|
431
|
+
cvv: string;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Generates encrypted auth data for card payment
|
|
436
|
+
* Uses pure JavaScript RSA — no Node.js crypto dependency
|
|
437
|
+
*
|
|
438
|
+
* @param params - Card details for encryption
|
|
439
|
+
* @returns Base64 encoded encrypted auth data
|
|
440
|
+
*/
|
|
441
|
+
export const generateAuthData = async ({
|
|
442
|
+
version,
|
|
443
|
+
pan,
|
|
444
|
+
pin,
|
|
445
|
+
expiryDate,
|
|
446
|
+
cvv,
|
|
447
|
+
}: AuthDataParams): Promise<string> => {
|
|
448
|
+
try {
|
|
449
|
+
// Build the cipher text
|
|
450
|
+
const authDataCipher = `${version}Z${pan}Z${pin}Z${expiryDate}Z${cvv}`;
|
|
451
|
+
const messageBytes = hexToBytes(toHex(authDataCipher));
|
|
452
|
+
|
|
453
|
+
// Parse RSA public key
|
|
454
|
+
const modulus = RSABigInt.fromHex(RSA_CONFIG.modulus);
|
|
455
|
+
const exponent = RSABigInt.fromHex(RSA_CONFIG.publicExponent);
|
|
456
|
+
|
|
457
|
+
// Determine key size in bytes
|
|
458
|
+
const keyByteLen = Math.ceil(modulus.bitLength() / 8);
|
|
459
|
+
|
|
460
|
+
// Apply PKCS#1 v1.5 padding
|
|
461
|
+
const padded = pkcs1Pad(messageBytes, keyByteLen);
|
|
462
|
+
|
|
463
|
+
// Convert padded message to BigInt
|
|
464
|
+
const m = RSABigInt.fromBytes(padded);
|
|
465
|
+
|
|
466
|
+
// RSA encrypt: c = m^e mod n
|
|
467
|
+
const c = m.modPow(exponent, modulus);
|
|
468
|
+
|
|
469
|
+
// Convert result to bytes (padded to key length)
|
|
470
|
+
const encryptedBytes = c.toBytes(keyByteLen);
|
|
471
|
+
|
|
472
|
+
// Base64 encode
|
|
473
|
+
return bytesToBase64(encryptedBytes);
|
|
474
|
+
} catch (error) {
|
|
475
|
+
console.error('[VoxePay] Encryption error:', error);
|
|
476
|
+
throw new Error('Failed to encrypt card data');
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Converts MM/YY format to YYMM format required by API
|
|
482
|
+
*/
|
|
483
|
+
export const formatExpiryForApi = (expiry: string): string => {
|
|
484
|
+
const cleaned = expiry.replace(/\s|\//g, '');
|
|
485
|
+
if (cleaned.length !== 4) return '';
|
|
486
|
+
const month = cleaned.substring(0, 2);
|
|
487
|
+
const year = cleaned.substring(2, 4);
|
|
488
|
+
return `${year}${month}`;
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Removes spaces and formatting from PAN
|
|
493
|
+
*/
|
|
494
|
+
export const cleanPan = (pan: string): string => {
|
|
495
|
+
return pan.replace(/\s/g, '');
|
|
496
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input formatting utilities for VoxePay Checkout
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format card number with spaces every 4 digits
|
|
7
|
+
*/
|
|
8
|
+
export function formatCardNumber(value: string): string {
|
|
9
|
+
const cleaned = value.replace(/\D/g, '');
|
|
10
|
+
const groups = cleaned.match(/.{1,4}/g) || [];
|
|
11
|
+
return groups.join(' ').slice(0, 23); // Max: 19 digits + 4 spaces
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Format expiry date as MM/YY
|
|
16
|
+
*/
|
|
17
|
+
export function formatExpiry(value: string): string {
|
|
18
|
+
const cleaned = value.replace(/\D/g, '');
|
|
19
|
+
|
|
20
|
+
if (cleaned.length === 0) return '';
|
|
21
|
+
if (cleaned.length === 1) {
|
|
22
|
+
return parseInt(cleaned) > 1 ? `0${cleaned}` : cleaned;
|
|
23
|
+
}
|
|
24
|
+
if (cleaned.length === 2) {
|
|
25
|
+
const month = parseInt(cleaned);
|
|
26
|
+
if (month > 12) return '12';
|
|
27
|
+
if (month === 0) return '01';
|
|
28
|
+
return cleaned;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const month = cleaned.slice(0, 2);
|
|
32
|
+
const year = cleaned.slice(2, 4);
|
|
33
|
+
return `${month}/${year}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format CVV (numbers only, max 4 digits)
|
|
38
|
+
*/
|
|
39
|
+
export function formatCVV(value: string): string {
|
|
40
|
+
return value.replace(/\D/g, '').slice(0, 4);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Format currency amount
|
|
45
|
+
*/
|
|
46
|
+
export function formatAmount(amount: number, currency: string): string {
|
|
47
|
+
const formatter = new Intl.NumberFormat('en-US', {
|
|
48
|
+
style: 'currency',
|
|
49
|
+
currency: currency,
|
|
50
|
+
minimumFractionDigits: 2,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Convert from smallest unit (cents/kobo) to main unit
|
|
54
|
+
return formatter.format(amount / 100);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get currency symbol
|
|
59
|
+
*/
|
|
60
|
+
export function getCurrencySymbol(currency: string): string {
|
|
61
|
+
const symbols: Record<string, string> = {
|
|
62
|
+
NGN: '₦',
|
|
63
|
+
USD: '$',
|
|
64
|
+
EUR: '€',
|
|
65
|
+
GBP: '£',
|
|
66
|
+
GHS: '₵',
|
|
67
|
+
KES: 'KSh',
|
|
68
|
+
ZAR: 'R',
|
|
69
|
+
};
|
|
70
|
+
return symbols[currency.toUpperCase()] || currency;
|
|
71
|
+
}
|