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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
|
/*
import {
afterEach,
beforeAll,
beforeEach,
describe,
it,
} from "@std/testing/bdd";
import { type Stub, stub } from "@std/testing/mock";
import { FakeTime } from "@std/testing/time";
import { get } from "../../utils/anonymous.ts";
import { SignatureVerifier } from "./verify.ts";
import { assertEquals } from "@std/assert/equals";
import { assert, assertExists, assertFalse, assertRejects } from "@std/assert";
import {
corruptData,
corruptSignatureFormat,
createDetachedSignature,
createInMemoryFile,
generateKeyPair,
generateKeyPairWithSubkey,
startMockFs,
} from "../../../tests/fixtures/setup.ts";
import { emptyCommandOutput } from "../../../tests/fixtures/test_data.ts";
startMockFs();
describe("SignatureVerifier", () => {
let verifier: SignatureVerifier;
let aliceKeyPair: Awaited<ReturnType<typeof generateKeyPair>>;
let bobKeyPair: Awaited<ReturnType<typeof generateKeyPair>>;
let aliceWithSubkeyKeyPair: Awaited<ReturnType<typeof generateKeyPair>>;
beforeAll(async () => {
aliceKeyPair = await generateKeyPair("Alice");
bobKeyPair = await generateKeyPair("Bob");
aliceWithSubkeyKeyPair = await generateKeyPairWithSubkey("AliceWithSubkey");
});
beforeEach(() => {
verifier = new SignatureVerifier();
Deno.Command.prototype.output = stub(
Deno.Command.prototype,
"output",
() => emptyCommandOutput,
);
});
afterEach(() => {
(Deno.Command.prototype.output as Stub).restore();
});
describe("when verifying a file with a single signature", () => {
const originalData = new TextEncoder().encode(
"This is the original file content for single signature tests.",
) as Uint8Array<ArrayBuffer>;
let originalDataUrl: URL;
beforeEach(() => {
// Create the data file in memory for each single signature test
originalDataUrl = createInMemoryFile(
new URL("file:///test/single_sig_data.txt"),
originalData,
);
});
it("Scenario: No signature found", async () => {
const verification = await verifier.verify([originalDataUrl]);
assertEquals(new Uint8Array(verification.data), originalData);
assertFalse(
await verification.dataCorrupted,
"Data is not corrupted in the absence of a signature to check against",
);
assertEquals(
verification.verifications,
undefined,
"Should not find any signatures to verify",
);
// commit is stubbed, so it will be undefined
});
it("Scenario: Signature cannot be checked (missing key - 'E')", async () => {
// Create a valid signature, but don't add the signing key to the verifier
const signature = await createDetachedSignature(
originalData,
aliceKeyPair.privateKey,
);
const signatureUrl = createInMemoryFile(
new URL("file:///test/single_sig_data.txt.sig"),
signature,
);
const verification = await verifier.verify([
originalDataUrl,
signatureUrl,
]);
assertEquals(new Uint8Array(verification.data), originalData);
assertEquals(await verification.dataCorrupted, [false]);
assertEquals(verification.signatureCorrupted, [false]);
assertExists(verification.verifications, "Should find the signature");
assertEquals(verification.verifications.length, 1); // One signature found
const sigVerification = verification.verifications[0];
assertExists(sigVerification.packet);
assertFalse(await sigVerification.signatureCorrupted.then(get(0)));
assertRejects(
() => sigVerification.verified,
"Verification should fail due to missing key",
);
// assertEquals(await sigVerification.status, "E", "Status should be 'E'");
// The keys promise might resolve with an empty array or throw depending on implementation
// assert(?) sigVerification.keys resolves as expected
});
it("Scenario: Signature cannot be checked (Signature corrupted/malformed - 'E')", async () => {
const signature = await createDetachedSignature(
originalData,
aliceKeyPair.privateKey,
);
const corruptedSignature = corruptSignatureFormat(signature);
const corruptedSignatureUrl = createInMemoryFile(
new URL("file:///test/single_sig_data.txt.sig"),
corruptedSignature,
);
verifier.addKey(aliceKeyPair.publicKey);
const verification = await verifier.verify([
originalDataUrl,
corruptedSignatureUrl,
]);
assertEquals(new Uint8Array(verification.data), originalData);
assertEquals(await verification.dataCorrupted, undefined);
assertEquals(verification.verifications, undefined);
// assertEquals(await sigVerification.status, "E", "Status should be 'E'");
});
it("Scenario: Bad signature ('B')", async () => {
// Create a valid signature for the original data
const signature = await createDetachedSignature(
originalData,
aliceKeyPair.privateKey,
);
const signatureUrl = createInMemoryFile(
new URL("file:///test/single_sig_data.txt.sig"),
signature,
);
// Create corrupted data
const corruptedData = corruptData(originalData);
const corruptedDataUrl = createInMemoryFile(
new URL("file:///test/corrupted_single_sig_data.txt"),
corruptedData,
);
verifier.addKey(aliceKeyPair.publicKey); // Key is available
// Verify the signature (of original data) against the corrupted data
const verification = await verifier.verify([
corruptedDataUrl,
signatureUrl,
]);
assertEquals(new Uint8Array(verification.data), corruptedData); // The verifier processed the corrupted data
assert(
await verification.dataCorrupted,
"Data should be marked as corrupted because signature does not match",
); // Assuming implementation detects this
assertFalse(verification.signatureCorrupted?.[0]);
assertExists(verification.verifications, "Should find the signature");
assertEquals(verification.verifications.length, 1); // One signature found
const sigVerification = verification.verifications[0];
assertExists(sigVerification.key); // Key should be found
assertExists(sigVerification.packet);
assertFalse(await sigVerification.signatureCorrupted.then(get(0))); // Signature data itself is not corrupted
// Expect verification to fail and report 'B'
assertRejects(
() => sigVerification.verified,
"Verification should fail due to data mismatch",
);
// assertEquals(await sigVerification.status, "B", "Status should be 'B'");
});
it("Scenario: Good signature ('G')", async () => {
const signature = await createDetachedSignature(
originalData,
aliceKeyPair.privateKey,
);
const signatureUrl = createInMemoryFile(
new URL("file:///test/single_sig_data.txt.sig"),
signature,
);
// Add the key and assume it's ultimately trusted for this scenario
// In a real test, you might explicitly set trust levels if openpgp.js supports it easily
verifier.addKey(aliceKeyPair.publicKey);
const verification = await verifier.verify([
originalDataUrl,
signatureUrl,
]);
assertEquals(new Uint8Array(verification.data), originalData);
assertFalse(
await verification.dataCorrupted?.then((x) => x[0]),
"Data should not be marked corrupted for a good signature",
);
assertFalse(verification.signatureCorrupted?.[0]);
assertExists(verification.verifications, "Should find the signature");
assertEquals(verification.verifications.length, 1);
const sigVerification = verification.verifications[0];
assertExists(sigVerification.key, "Should find the signing key");
const signingKey = await sigVerification.key; // Assuming one key found
assertExists(signingKey, "Should find the signing key");
assertEquals(signingKey.getKeyID(), aliceKeyPair.publicKey.getKeyID());
assertExists(sigVerification.packet);
assertFalse(await sigVerification.signatureCorrupted.then((x) => x[0]));
// Expect verification to succeed and report 'G'
assert(
await sigVerification.verified,
"Verification should succeed for a good signature",
);
// assertEquals(await sigVerification.status, "G", "Status should be 'G'");
});
it("Scenario: Good signature, unknown validity ('U')", async () => {
const signature = await createDetachedSignature(
originalData,
aliceKeyPair.privateKey,
);
const signatureUrl = createInMemoryFile(
new URL("file:///test/single_sig_data.txt.sig"),
signature,
);
// Add the key but do *not* establish ultimate trust for this key in the verifier's context
// This scenario relies on your verifier or OpenPGP.js handling the 'unknown trust' case.
verifier.addKey(aliceKeyPair.publicKey); // Key is available, but trust level is not set
const verification = await verifier.verify([
originalDataUrl,
signatureUrl,
]);
assertEquals(new Uint8Array(verification.data), originalData);
assertFalse(await verification.dataCorrupted?.then((x) => x[0]));
assertFalse(verification.signatureCorrupted?.[0]);
assertExists(verification.verifications, "Should find the signature");
assertEquals(verification.verifications.length, 1);
const sigVerification = verification.verifications[0];
assertExists(sigVerification.key);
assertExists(sigVerification.packet);
assertFalse(await sigVerification.signatureCorrupted.then((x) => x[0]));
// Expect cryptographic verification to succeed, but status to be 'U'
assert(
await sigVerification.verified,
"Cryptographic verification should succeed",
);
// assertEquals(
// await sigVerification.status,
// "U",
// "Status should be 'U' due to unknown validity",
// );
});
// TODO(#): Add tests for Scenarios involving Key Expiration ('X', 'Y')
// This requires creating keys with specific expiration dates and mocking the system clock
it("Scenario: Good signature, key expired *after* signature time ('X')", async () => {
// Use fake time to control the 'now'
const time = new FakeTime();
const keyExpirationTime = time.now + 30 * 1000;
const keyPairWithExpiry = await generateKeyPair("AliceWithExpiry", {
keyExpirationTime,
});
const signature = await createDetachedSignature(
originalData,
keyPairWithExpiry.privateKey,
);
const signatureUrl = createInMemoryFile(
new URL("file:///test/sig_expired_after.sig"),
signature,
);
time.tick(60 * 1000);
verifier.addKey(keyPairWithExpiry.publicKey);
const verification = await verifier.verify([
originalDataUrl,
signatureUrl,
]);
time.restore();
assertFalse(await verification.dataCorrupted?.then((x) => x[0]));
assertFalse(verification.signatureCorrupted?.[0]);
assertExists(verification.verifications);
// const expirationDate = await verification.verifications[0].keys[0].then((
// x,
// ) => x.getExpirationTime());
// assertEquals(
// expirationDate?.valueOf(),
// new Date(keyExpirationTime).valueOf(),
// );
assertExists(await verification.verifications[0].packet);
assertFalse(
await verification.verifications[0].signatureCorrupted.then((x) =>
x[0]
),
);
assert(await verification.verifications[0].verified);
// assertEquals(
// await verification.verifications![0].status,
// "X",
// "Status should be 'X' due to key expired after signature",
// );
});
it("Scenario: Good signature, key expired *before* signature time ('Y')", async () => {
// Use fake time to control the 'now' when creating the key (for expiration)
const time = new FakeTime();
const keyExpirationTime = time.now + 30 * 1000;
const keyPairExpiredBefore = await generateKeyPair("AliceExpiredBefore", {
keyExpirationTime,
});
time.tick(60 * 1000);
const signature = await createDetachedSignature(
originalData,
keyPairExpiredBefore.privateKey,
);
const signatureUrl = createInMemoryFile(
new URL("file:///test/sig_expired_before.sig"),
signature,
);
verifier.addKey(keyPairExpiredBefore.publicKey);
time.tick(60 * 1000);
const verification = await verifier.verify([
originalDataUrl,
signatureUrl,
]);
time.restore();
assertFalse(await verification.dataCorrupted?.then((x) => x[0]));
assertFalse(verification.signatureCorrupted?.[0]);
assertExists(verification.verifications);
// const expirationDate = await verification.verifications[0].keys[0].then((
// x,
// ) => x.getExpirationTime());
// assertEquals(
// expirationDate?.valueOf(),
// new Date(keyExpirationTime).valueOf(),
// );
assertExists(await verification.verifications[0].packet);
assertFalse(
await verification.verifications[0].signatureCorrupted.then((x) =>
x[0]
),
);
assert(await verification.verifications[0].verified);
//assertEquals(
// await verification.verifications![0].status,
// "Y",
// "Status should be 'Y' due to key expired before signature",
//);
});
// // TODO: Add tests for Scenarios involving Key Revocation ('R', 'Y')
// // This requires creating and distributing key revocation certificates. Simulating this is complex and might need mocking OpenPGP.js internal behavior or relying on its revocation handling.
// it("Scenario: Good signature, key revoked *after* signature time ('R')", async () => {
// // This requires creating a revocation certificate for the key *after* signing.
// assert(
// false,
// "Test not implemented: Simulating key revocation requires revocation certs.",
// );
// });
// it("Scenario: Good signature, key revoked *before* signature time ('Y')", async () => {
// // This requires creating a revocation certificate for the key *before* signing.
// assert(
// false,
// "Test not implemented: Simulating key revocation requires revocation certs.",
// );
// });
// it("Scenario: Signature cannot be checked (Public key available but not signing)", async () => {
// // Generate a key with only encryption or certification usage flags
// const nonSigningKeyPair = await generateKeyPair("AliceNonSigning", {
// usage: ["encrypt"],
// }); // Or ["certify"]
// const signature = await createDetachedSignature(
// originalData,
// aliceKeyPair.privateKey,
// ); // Signed with a signing key
// const signatureUrl = createInMemoryFile(
// new URL("file:///test/sig_non_signing_key.sig"),
// signature,
// );
// // Add the non-signing key to the verifier instead of the actual signing key
// await verifier.addKey(nonSigningKeyPair.publicKey);
// const verification: Verification = await verifier.verify([
// originalDataUrl,
// signatureUrl,
// ]);
// assertExists(verification.verifications, "Should find the signature");
// assertEquals(verification.verifications.length, 1);
// const sigVerification = verification.verifications[0];
// // Key is found, but it's the wrong type of key for verification
// assertExists(sigVerification.keys, "Should find a key");
// // Expect verification to fail and report 'E' or potentially 'B' depending on how openpgp.js handles this
// // OpenPGP.js often reports 'E' if the key's capabilities don't match the packet type.
// assertEquals(
// await sigVerification.verified,
// false,
// "Verification should fail with a non-signing key",
// );
// // We expect 'E' as the most likely status
// assertEquals(
// await sigVerification.status,
// "E",
// "Status should be 'E' with a non-signing key",
// );
// });
// TODO: Add scenarios involving signing subkeys if your verifier needs to distinguish them
// These would require more complex key generation and potentially inspecting the packet details.
});
// // --- Scenarios for multiple signatures ---
// describe("when verifying a file with multiple signatures", () => {
// const originalData = new TextEncoder().encode("This file has multiple signatures.");
// let originalDataUrl: URL;
//
// beforeEach(() => {
// originalDataUrl = createInMemoryFile(new URL("file:///test/multi_sig_data.txt"), originalData);
// });
//
//
// it("Scenario: All signatures are Good ('G')", async () => {
// // Create signatures by Alice and Bob
// const aliceSignature = await createDetachedSignature(originalData, aliceKeyPair.privateKey);
// const bobSignature = await createDetachedSignature(originalData, bobKeyPair.privateKey);
//
// const aliceSignatureUrl = createInMemoryFile(new URL("file:///test/multi_sig_data.txt.alice.sig"), aliceSignature);
// const bobSignatureUrl = createInMemoryFile(new URL("file:///test/multi_sig_data.txt.bob.sig"), bobSignature);
//
// // Add both signing keys (assume trusted for this scenario)
// await verifier.addKey(aliceKeyPair.publicKey);
// await verifier.addKey(bobKeyPair.publicKey);
//
// // Verify with multiple signature files
// const verification: Verification = await verifier.verify([originalDataUrl, aliceSignatureUrl, bobSignatureUrl]);
//
// assertEquals(new Uint8Array(verification.data), originalData);
// assertEquals(verification.dataCorrupted, false);
// assertExists(verification.verifications);
// assertEquals(verification.verifications.length, 2); // Two signatures found
//
// // Check the status of each verification result
// const statuses = await Promise.all(verification.verifications.map(v => v.status));
// assertArrayIncludes(statuses, ['G', 'G'], "Both signatures should have 'G' status");
//
// // Check the key IDs found for each verification
// const keyIDs = await Promise.all(verification.verifications.map(async v => (await v.keys)[0]?.getKeyID()));
// assertArrayIncludes(keyIDs.filter(defined), [aliceKeyPair.publicKey.getKeyID(), bobKeyPair.publicKey.getKeyID()]);
// });
//
// it("Scenario: Some signatures are Good ('G'), others are Bad ('B')", async () => {
// // Create a good signature by Alice
// const aliceSignature = await createDetachedSignature(originalData, aliceKeyPair.privateKey);
// const aliceSignatureUrl = createInMemoryFile(new URL("file:///test/multi_sig_data.txt.alice.sig"), aliceSignature);
//
// // Create a bad signature by attempting to sign corrupted data with Bob's key
// const corruptedDataForBadSig = corruptData(originalData);
// const bobBadSignature = await createDetachedSignature(corruptedDataForBadSig, bobKeyPair.privateKey);
// const bobBadSignatureUrl = createInMemoryFile(new URL("file:///test/multi_sig_data.txt.bob.sig"), bobBadSignature);
//
//
// // Add both signing keys
// await verifier.addKey(aliceKeyPair.publicKey);
// await verifier.addKey(bobKeyPair.publicKey);
//
// // Verify against the original data, but provide one good and one bad signature file
// const verification: Verification = await verifier.verify([originalDataUrl, aliceSignatureUrl, bobBadSignatureUrl]);
//
// assertEquals(new Uint8Array(verification.data), originalData); // Verifier should use the original data if found and matching a good sig
// assertEquals(verification.dataCorrupted, false, "Data should not be marked corrupted if at least one good signature matches");
//
// assertExists(verification.verifications);
// assertEquals(verification.verifications.length, 2);
//
// // Check the status of each verification result
// const statuses = await Promise.all(verification.verifications.map(v => v.status));
// // Expect one 'G' and one 'B' status
// assertEquals(statuses.filter(s => s === 'G').length, 1);
// assertEquals(statuses.filter(s => s === 'B').length, 1);
//
// // You would also need to check which key corresponded to the 'G' and 'B' status
// // This requires correlating the verification result with the key ID/fingerprint.
// const verifications = await Promise.all(verification.verifications.map(async v => ({ status: await v.status, keyID: (await Promise.all(v.keys))[0]?.getKeyID() })));
//
// assert(verifications.some(v => v.status === 'G' && v.keyID === aliceKeyPair.publicKey.getKeyID()), "Alice's signature should be Good");
// assert(verifications.some(v => v.status === 'B' && v.keyID === bobKeyPair.publicKey.getKeyID()), "Bob's signature should be Bad");
// });
//
// it("Scenario: Some signatures cannot be checked ('E'), others are Good ('G')", async () => {
// // Create a good signature by Alice
// const aliceSignature = await createDetachedSignature(originalData, aliceKeyPair.privateKey);
// const aliceSignatureUrl = createInMemoryFile(new URL("file:///test/multi_sig_data.txt.alice.sig"), aliceSignature);
//
// // Create a signature by Bob but don't add Bob's key to the verifier (will result in 'E')
// const bobSignature = await createDetachedSignature(originalData, bobKeyPair.privateKey);
// const bobSignatureUrl = createInMemoryFile(new URL("file:///test/multi_sig_data.txt.bob.sig"), bobSignature);
//
//
// // Add only Alice's key
// await verifier.addKey(aliceKeyPair.publicKey);
//
// const verification: Verification = await verifier.verify([originalDataUrl, aliceSignatureUrl, bobSignatureUrl]);
//
// assertEquals(new Uint8Array(verification.data), originalData);
// assertEquals(verification.dataCorrupted, false);
// assertExists(verification.verifications);
// assertEquals(verification.verifications.length, 2);
//
// const statuses = await Promise.all(verification.verifications.map(v => v.status));
//
// assertEquals(statuses.filter(s => s === 'G').length, 1, "One signature should be Good (Alice)");
// assertEquals(statuses.filter(s => s === 'E').length, 1, "One signature should be 'E' (Bob - missing key)");
//
// const verifications = await Promise.all(verification.verifications.map(async v => ({ status: await v.status, keyID: (await Promise.all(v.keys))[0]?.getKeyID() })));
//
// assert(verifications.some(v => v.status === 'G' && v.keyID === aliceKeyPair.publicKey.getKeyID()), "Alice's signature should be Good");
// // For the 'E' status (missing key), the keyID might be undefined or the partial KeyID from the packet.
// // We'll just check that one status is 'E'.
// assert(verifications.some(v => v.status === 'E'), "One signature should be 'E'");
// });
//
//
// // TODO: Continue adding tests for all combinations from the multiple signatures table
// // This requires combining different key states (expired, revoked, untrusted) for different signers
// // within the same verification process. This is the most complex part.
//
// it("Scenario: All signatures Unknown Validity ('U')", async () => {
// // Requires generating signatures with keys that are valid but not ultimately trusted for all signers.
// // Then verifying without establishing a trust path for any key.
// assert(false, "Test not implemented: Simulating unknown trust for all signatures.");
// });
//
// it("Scenario: At least one Good signature, with others having Key Status issues (e.g., 'X', 'Y', 'R')", async () => {
// // Requires creating signatures with a mix of good keys and expired/revoked keys for different signers.
// assert(false, "Test not implemented: Combining different key states for multiple signers.");
// });
//
// it("Scenario: All signatures have Key Status issues ('X', 'Y', 'R')", async () => {
// // Requires creating signatures with only expired or revoked keys for all signers.
// assert(false, "Test not implemented: Simulating all signatures with key status issues.");
// });
//
// it("Scenario: Combination of Bad, Unknown, and Key Status issues", async () => {
// // This is a very complex scenario combining multiple failure types across different signatures.
// assert(false, "Test not implemented: Simulating a complex mix of failure types.");
// });
//
// it("Scenario: At least one signature is valid, but some Public Keys not available", async () => {
// // Requires providing multiple signature files, but only providing some of the signing keys to the verifier.
// assert(false, "Test not implemented: Simulating missing keys for some signatures in a multi-signature scenario.");
// });
//
// it("Scenario: At least one signature is valid, but some Public Keys available but not Signing Keys", async () => {
// // Requires providing multiple signature files, and providing a key that is NOT a signing key for one of them.
// assert(false, "Test not implemented: Simulating non-signing keys for some signatures in a multi-signature scenario.");
// });
// });
});
*/
|