@protontech/openpgp 6.0.0-alpha.1.patch.1 → 6.0.0-beta.0.patch.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/dist/lightweight/argon2id.min.mjs +1 -1
- package/dist/lightweight/argon2id.mjs +1 -1
- package/dist/lightweight/bn.interface.min.mjs +2 -2
- package/dist/lightweight/bn.interface.min.mjs.map +1 -1
- package/dist/lightweight/bn.interface.mjs +1 -1
- package/dist/lightweight/interface.min.mjs +1 -1
- package/dist/lightweight/interface.mjs +1 -1
- package/dist/lightweight/legacy_ciphers.min.mjs +1 -1
- package/dist/lightweight/legacy_ciphers.mjs +1 -1
- package/dist/lightweight/native.interface.min.mjs +1 -1
- package/dist/lightweight/native.interface.mjs +1 -1
- package/dist/lightweight/noble_curves.min.mjs +3 -3
- package/dist/lightweight/noble_curves.min.mjs.map +1 -1
- package/dist/lightweight/noble_curves.mjs +1 -1
- package/dist/lightweight/noble_hashes.min.mjs +1 -1
- package/dist/lightweight/noble_hashes.mjs +1 -1
- package/dist/lightweight/openpgp.min.mjs +2 -2
- package/dist/lightweight/openpgp.min.mjs.map +1 -1
- package/dist/lightweight/openpgp.mjs +211 -83
- package/dist/lightweight/sha3.min.mjs +2 -2
- package/dist/lightweight/sha3.min.mjs.map +1 -1
- package/dist/lightweight/sha3.mjs +1 -1
- package/dist/node/openpgp.cjs +211 -83
- package/dist/node/openpgp.min.cjs +11 -11
- package/dist/node/openpgp.min.cjs.map +1 -1
- package/dist/node/openpgp.min.mjs +11 -11
- package/dist/node/openpgp.min.mjs.map +1 -1
- package/dist/node/openpgp.mjs +211 -83
- package/dist/openpgp.js +211 -83
- package/dist/openpgp.min.js +11 -11
- package/dist/openpgp.min.js.map +1 -1
- package/dist/openpgp.min.mjs +11 -11
- package/dist/openpgp.min.mjs.map +1 -1
- package/dist/openpgp.mjs +211 -83
- package/openpgp.d.ts +5 -3
- package/package.json +9 -9
package/dist/openpgp.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! OpenPGP.js v6.0.0-
|
|
1
|
+
/*! OpenPGP.js v6.0.0-beta.0.patch.0 - 2024-04-19 - this is LGPL licensed code, see LICENSE/our website https://openpgpjs.org/ for more information. */
|
|
2
2
|
const globalThis = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
3
3
|
|
|
4
4
|
const doneWritingPromise = Symbol('doneWritingPromise');
|
|
@@ -1465,6 +1465,14 @@ var config = {
|
|
|
1465
1465
|
* @property {Boolean} aeadProtect
|
|
1466
1466
|
*/
|
|
1467
1467
|
aeadProtect: false,
|
|
1468
|
+
/**
|
|
1469
|
+
* Whether to disable encrypton using SEIPDv2 even if the encryption keys include the SEIPDv2 feature flag.
|
|
1470
|
+
* If true, SEIPDv1 (i.e. no AEAD) packets are always used instead.
|
|
1471
|
+
* SEIPDv2 is a more secure and faster choice, but it is not necessarily compatible with other libs and our mobile apps.
|
|
1472
|
+
* @memberof module:config
|
|
1473
|
+
* @property {Boolean} ignoreSEIPDv2FeatureFlag
|
|
1474
|
+
*/
|
|
1475
|
+
ignoreSEIPDv2FeatureFlag: false,
|
|
1468
1476
|
/**
|
|
1469
1477
|
* When reading OpenPGP v4 private keys (e.g. those generated in OpenPGP.js when not setting `config.v5Keys = true`)
|
|
1470
1478
|
* which were encrypted by OpenPGP.js v5 (or older) using `config.aeadProtect = true`,
|
|
@@ -1577,11 +1585,6 @@ var config = {
|
|
|
1577
1585
|
* @property {Boolean} passwordCollisionCheck
|
|
1578
1586
|
*/
|
|
1579
1587
|
passwordCollisionCheck: false,
|
|
1580
|
-
/**
|
|
1581
|
-
* @memberof module:config
|
|
1582
|
-
* @property {Boolean} revocationsExpire If true, expired revocation signatures are ignored
|
|
1583
|
-
*/
|
|
1584
|
-
revocationsExpire: false,
|
|
1585
1588
|
/**
|
|
1586
1589
|
* Allow decryption using RSA keys without `encrypt` flag.
|
|
1587
1590
|
* This setting is potentially insecure, but it is needed to get around an old openpgpjs bug
|
|
@@ -1657,7 +1660,7 @@ var config = {
|
|
|
1657
1660
|
* @memberof module:config
|
|
1658
1661
|
* @property {String} versionString A version string to be included in armored messages
|
|
1659
1662
|
*/
|
|
1660
|
-
versionString: 'OpenPGP.js 6.0.0-
|
|
1663
|
+
versionString: 'OpenPGP.js 6.0.0-beta.0.patch.0',
|
|
1661
1664
|
/**
|
|
1662
1665
|
* @memberof module:config
|
|
1663
1666
|
* @property {String} commentString A comment string to be included in armored messages
|
|
@@ -1677,6 +1680,14 @@ var config = {
|
|
|
1677
1680
|
* @property {Array} knownNotations
|
|
1678
1681
|
*/
|
|
1679
1682
|
knownNotations: [],
|
|
1683
|
+
/**
|
|
1684
|
+
* If true, a salt notation is used to randomize signatures generated by v4 and v5 keys (v6 signatures are always non-deterministic, by design).
|
|
1685
|
+
* This protects EdDSA signatures from potentially leaking the secret key in case of faults (i.e. bitflips) which, in principle, could occur
|
|
1686
|
+
* during the signing computation. It is added to signatures of any algo for simplicity, and as it may also serve as protection in case of
|
|
1687
|
+
* weaknesses in the hash algo, potentially hindering e.g. some chosen-prefix attacks.
|
|
1688
|
+
* NOTE: the notation is interoperable, but will reveal that the signature has been generated using OpenPGP.js, which may not be desirable in some cases.
|
|
1689
|
+
*/
|
|
1690
|
+
nonDeterministicSignaturesViaNotation: true,
|
|
1680
1691
|
/**
|
|
1681
1692
|
* Whether to use the the noble-curves library for curves (other than Curve25519) that are not supported by the available native crypto API.
|
|
1682
1693
|
* When false, certain standard curves will not be supported (depending on the platform).
|
|
@@ -1707,14 +1718,7 @@ var config = {
|
|
|
1707
1718
|
* @memberof module:config
|
|
1708
1719
|
* @property {Set<String>} rejectCurves {@link module:enums.curve}
|
|
1709
1720
|
*/
|
|
1710
|
-
rejectCurves: new Set([enums.curve.secp256k1])
|
|
1711
|
-
/**
|
|
1712
|
-
* Whether to validate generated EdDSA signatures before returning them, to ensure they are not faulty signatures.
|
|
1713
|
-
* This check will make signing 2-3 times slower.
|
|
1714
|
-
* Faulty signatures may be generated (in principle) if random bitflips occur at specific points in the signature
|
|
1715
|
-
* computation, and could be used to recover the signer's secret key given a second signature over the same data.
|
|
1716
|
-
*/
|
|
1717
|
-
checkEdDSAFaultySignatures: true
|
|
1721
|
+
rejectCurves: new Set([enums.curve.secp256k1])
|
|
1718
1722
|
};
|
|
1719
1723
|
|
|
1720
1724
|
/**
|
|
@@ -2200,16 +2204,19 @@ const util = {
|
|
|
2200
2204
|
},
|
|
2201
2205
|
|
|
2202
2206
|
/**
|
|
2203
|
-
* Test email format
|
|
2204
|
-
*
|
|
2205
|
-
*
|
|
2206
|
-
*
|
|
2207
|
+
* Test email format to ensure basic compliance:
|
|
2208
|
+
* - must include a single @
|
|
2209
|
+
* - no control or space unicode chars allowed
|
|
2210
|
+
* - no backslash and square brackets (as the latter can mess with the userID parsing)
|
|
2211
|
+
* - cannot end with a punctuation char
|
|
2212
|
+
* These checks are not meant to be exhaustive; applications are strongly encouraged to implement stricter validation,
|
|
2213
|
+
* e.g. based on the W3C HTML spec (https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email)).
|
|
2207
2214
|
*/
|
|
2208
2215
|
isEmailAddress: function(data) {
|
|
2209
2216
|
if (!util.isString(data)) {
|
|
2210
2217
|
return false;
|
|
2211
2218
|
}
|
|
2212
|
-
const re = /^[
|
|
2219
|
+
const re = /^[^\p{C}\p{Z}@<>\\]+@[^\p{C}\p{Z}@<>\\]+[^\p{C}\p{Z}\p{P}]$/u;
|
|
2213
2220
|
return re.test(data);
|
|
2214
2221
|
},
|
|
2215
2222
|
|
|
@@ -2615,6 +2622,78 @@ function addheader(customComment, config) {
|
|
|
2615
2622
|
return result;
|
|
2616
2623
|
}
|
|
2617
2624
|
|
|
2625
|
+
/**
|
|
2626
|
+
* Calculates a checksum over the given data and returns it base64 encoded
|
|
2627
|
+
* @param {String | ReadableStream<String>} data - Data to create a CRC-24 checksum for
|
|
2628
|
+
* @returns {String | ReadableStream<String>} Base64 encoded checksum.
|
|
2629
|
+
* @private
|
|
2630
|
+
*/
|
|
2631
|
+
function getCheckSum(data) {
|
|
2632
|
+
const crc = createcrc24(data);
|
|
2633
|
+
return encode$1(crc);
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
// https://create.stephan-brumme.com/crc32/#slicing-by-8-overview
|
|
2637
|
+
|
|
2638
|
+
const crc_table = [
|
|
2639
|
+
new Array(0xFF),
|
|
2640
|
+
new Array(0xFF),
|
|
2641
|
+
new Array(0xFF),
|
|
2642
|
+
new Array(0xFF)
|
|
2643
|
+
];
|
|
2644
|
+
|
|
2645
|
+
for (let i = 0; i <= 0xFF; i++) {
|
|
2646
|
+
let crc = i << 16;
|
|
2647
|
+
for (let j = 0; j < 8; j++) {
|
|
2648
|
+
crc = (crc << 1) ^ ((crc & 0x800000) !== 0 ? 0x864CFB : 0);
|
|
2649
|
+
}
|
|
2650
|
+
crc_table[0][i] =
|
|
2651
|
+
((crc & 0xFF0000) >> 16) |
|
|
2652
|
+
(crc & 0x00FF00) |
|
|
2653
|
+
((crc & 0x0000FF) << 16);
|
|
2654
|
+
}
|
|
2655
|
+
for (let i = 0; i <= 0xFF; i++) {
|
|
2656
|
+
crc_table[1][i] = (crc_table[0][i] >> 8) ^ crc_table[0][crc_table[0][i] & 0xFF];
|
|
2657
|
+
}
|
|
2658
|
+
for (let i = 0; i <= 0xFF; i++) {
|
|
2659
|
+
crc_table[2][i] = (crc_table[1][i] >> 8) ^ crc_table[0][crc_table[1][i] & 0xFF];
|
|
2660
|
+
}
|
|
2661
|
+
for (let i = 0; i <= 0xFF; i++) {
|
|
2662
|
+
crc_table[3][i] = (crc_table[2][i] >> 8) ^ crc_table[0][crc_table[2][i] & 0xFF];
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView#Endianness
|
|
2666
|
+
const isLittleEndian$1 = (function() {
|
|
2667
|
+
const buffer = new ArrayBuffer(2);
|
|
2668
|
+
new DataView(buffer).setInt16(0, 0xFF, true /* littleEndian */);
|
|
2669
|
+
// Int16Array uses the platform's endianness.
|
|
2670
|
+
return new Int16Array(buffer)[0] === 0xFF;
|
|
2671
|
+
}());
|
|
2672
|
+
|
|
2673
|
+
/**
|
|
2674
|
+
* Internal function to calculate a CRC-24 checksum over a given string (data)
|
|
2675
|
+
* @param {String | ReadableStream<String>} input - Data to create a CRC-24 checksum for
|
|
2676
|
+
* @returns {Uint8Array | ReadableStream<Uint8Array>} The CRC-24 checksum.
|
|
2677
|
+
* @private
|
|
2678
|
+
*/
|
|
2679
|
+
function createcrc24(input) {
|
|
2680
|
+
let crc = 0xCE04B7;
|
|
2681
|
+
return transform(input, value => {
|
|
2682
|
+
const len32 = isLittleEndian$1 ? Math.floor(value.length / 4) : 0;
|
|
2683
|
+
const arr32 = new Uint32Array(value.buffer, value.byteOffset, len32);
|
|
2684
|
+
for (let i = 0; i < len32; i++) {
|
|
2685
|
+
crc ^= arr32[i];
|
|
2686
|
+
crc =
|
|
2687
|
+
crc_table[0][(crc >> 24) & 0xFF] ^
|
|
2688
|
+
crc_table[1][(crc >> 16) & 0xFF] ^
|
|
2689
|
+
crc_table[2][(crc >> 8) & 0xFF] ^
|
|
2690
|
+
crc_table[3][(crc >> 0) & 0xFF];
|
|
2691
|
+
}
|
|
2692
|
+
for (let i = len32 * 4; i < value.length; i++) {
|
|
2693
|
+
crc = (crc >> 8) ^ crc_table[0][(crc & 0xFF) ^ value[i]];
|
|
2694
|
+
}
|
|
2695
|
+
}, () => new Uint8Array([crc, crc >> 8, crc >> 16]));
|
|
2696
|
+
}
|
|
2618
2697
|
|
|
2619
2698
|
/**
|
|
2620
2699
|
* Verify armored headers. crypto-refresh-06, section 6.2:
|
|
@@ -2770,10 +2849,13 @@ function unarmor(input) {
|
|
|
2770
2849
|
* @param {Integer} [partIndex]
|
|
2771
2850
|
* @param {Integer} [partTotal]
|
|
2772
2851
|
* @param {String} [customComment] - Additional comment to add to the armored string
|
|
2852
|
+
* @param {Boolean} [emitChecksum] - Whether to compute and include the CRC checksum
|
|
2853
|
+
* (NB: some types of data must not include it, but compliance is left as responsibility of the caller: this function does not carry out any checks)
|
|
2854
|
+
* @param {Object} [config] - Full configuration, defaults to openpgp.config
|
|
2773
2855
|
* @returns {String | ReadableStream<String>} Armored text.
|
|
2774
2856
|
* @static
|
|
2775
2857
|
*/
|
|
2776
|
-
function armor(messageType, body, partIndex, partTotal, customComment, config$1 = config) {
|
|
2858
|
+
function armor(messageType, body, partIndex, partTotal, customComment, emitChecksum = false, config$1 = config) {
|
|
2777
2859
|
let text;
|
|
2778
2860
|
let hash;
|
|
2779
2861
|
if (messageType === enums.armor.signed) {
|
|
@@ -2781,18 +2863,24 @@ function armor(messageType, body, partIndex, partTotal, customComment, config$1
|
|
|
2781
2863
|
hash = body.hash;
|
|
2782
2864
|
body = body.data;
|
|
2783
2865
|
}
|
|
2866
|
+
// unless explicitly forbidden by the spec, we need to include the checksum to work around a GnuPG bug
|
|
2867
|
+
// where data fails to be decoded if the base64 ends with no padding chars (=) (see https://dev.gnupg.org/T7071)
|
|
2868
|
+
const maybeBodyClone = emitChecksum && passiveClone(body);
|
|
2869
|
+
|
|
2784
2870
|
const result = [];
|
|
2785
2871
|
switch (messageType) {
|
|
2786
2872
|
case enums.armor.multipartSection:
|
|
2787
2873
|
result.push('-----BEGIN PGP MESSAGE, PART ' + partIndex + '/' + partTotal + '-----\n');
|
|
2788
2874
|
result.push(addheader(customComment, config$1));
|
|
2789
2875
|
result.push(encode$1(body));
|
|
2876
|
+
maybeBodyClone && result.push('=', getCheckSum(maybeBodyClone));
|
|
2790
2877
|
result.push('-----END PGP MESSAGE, PART ' + partIndex + '/' + partTotal + '-----\n');
|
|
2791
2878
|
break;
|
|
2792
2879
|
case enums.armor.multipartLast:
|
|
2793
2880
|
result.push('-----BEGIN PGP MESSAGE, PART ' + partIndex + '-----\n');
|
|
2794
2881
|
result.push(addheader(customComment, config$1));
|
|
2795
2882
|
result.push(encode$1(body));
|
|
2883
|
+
maybeBodyClone && result.push('=', getCheckSum(maybeBodyClone));
|
|
2796
2884
|
result.push('-----END PGP MESSAGE, PART ' + partIndex + '-----\n');
|
|
2797
2885
|
break;
|
|
2798
2886
|
case enums.armor.signed:
|
|
@@ -2802,30 +2890,35 @@ function armor(messageType, body, partIndex, partTotal, customComment, config$1
|
|
|
2802
2890
|
result.push('\n-----BEGIN PGP SIGNATURE-----\n');
|
|
2803
2891
|
result.push(addheader(customComment, config$1));
|
|
2804
2892
|
result.push(encode$1(body));
|
|
2893
|
+
maybeBodyClone && result.push('=', getCheckSum(maybeBodyClone));
|
|
2805
2894
|
result.push('-----END PGP SIGNATURE-----\n');
|
|
2806
2895
|
break;
|
|
2807
2896
|
case enums.armor.message:
|
|
2808
2897
|
result.push('-----BEGIN PGP MESSAGE-----\n');
|
|
2809
2898
|
result.push(addheader(customComment, config$1));
|
|
2810
2899
|
result.push(encode$1(body));
|
|
2900
|
+
maybeBodyClone && result.push('=', getCheckSum(maybeBodyClone));
|
|
2811
2901
|
result.push('-----END PGP MESSAGE-----\n');
|
|
2812
2902
|
break;
|
|
2813
2903
|
case enums.armor.publicKey:
|
|
2814
2904
|
result.push('-----BEGIN PGP PUBLIC KEY BLOCK-----\n');
|
|
2815
2905
|
result.push(addheader(customComment, config$1));
|
|
2816
2906
|
result.push(encode$1(body));
|
|
2907
|
+
maybeBodyClone && result.push('=', getCheckSum(maybeBodyClone));
|
|
2817
2908
|
result.push('-----END PGP PUBLIC KEY BLOCK-----\n');
|
|
2818
2909
|
break;
|
|
2819
2910
|
case enums.armor.privateKey:
|
|
2820
2911
|
result.push('-----BEGIN PGP PRIVATE KEY BLOCK-----\n');
|
|
2821
2912
|
result.push(addheader(customComment, config$1));
|
|
2822
2913
|
result.push(encode$1(body));
|
|
2914
|
+
maybeBodyClone && result.push('=', getCheckSum(maybeBodyClone));
|
|
2823
2915
|
result.push('-----END PGP PRIVATE KEY BLOCK-----\n');
|
|
2824
2916
|
break;
|
|
2825
2917
|
case enums.armor.signature:
|
|
2826
2918
|
result.push('-----BEGIN PGP SIGNATURE-----\n');
|
|
2827
2919
|
result.push(addheader(customComment, config$1));
|
|
2828
2920
|
result.push(encode$1(body));
|
|
2921
|
+
maybeBodyClone && result.push('=', getCheckSum(maybeBodyClone));
|
|
2829
2922
|
result.push('-----END PGP SIGNATURE-----\n');
|
|
2830
2923
|
break;
|
|
2831
2924
|
}
|
|
@@ -9352,20 +9445,6 @@ async function sign$5(oid, hashAlgo, message, publicKey, privateKey, hashed) {
|
|
|
9352
9445
|
}
|
|
9353
9446
|
const secretKey = util.concatUint8Array([privateKey, publicKey.subarray(1)]);
|
|
9354
9447
|
const signature = nacl.sign.detached(hashed, secretKey);
|
|
9355
|
-
if (config.checkEdDSAFaultySignatures && !nacl.sign.detached.verify(hashed, signature, publicKey.subarray(1))) {
|
|
9356
|
-
/**
|
|
9357
|
-
* Detect faulty signatures caused by random bitflips during `crypto_sign` which could lead to private key extraction
|
|
9358
|
-
* if two signatures over the same message are obtained.
|
|
9359
|
-
* See https://github.com/jedisct1/libsodium/issues/170.
|
|
9360
|
-
* If the input data is not deterministic, e.g. thanks to the random salt in v6 OpenPGP signatures (not yet implemented),
|
|
9361
|
-
* then the generated signature is always safe, and the verification step is skipped.
|
|
9362
|
-
* Otherwise, we need to verify the generated to ensure that no bitflip occured:
|
|
9363
|
-
* - in M between the computation of `r` and `h`.
|
|
9364
|
-
* - in the public key before computing `h`
|
|
9365
|
-
* The verification step is almost 2-3 times as slow as signing, but it's faster than re-signing + re-deriving the public key for separate checks.
|
|
9366
|
-
*/
|
|
9367
|
-
throw new Error('Transient signing failure');
|
|
9368
|
-
}
|
|
9369
9448
|
// EdDSA signature params are returned in little-endian format
|
|
9370
9449
|
return {
|
|
9371
9450
|
r: signature.subarray(0, 32),
|
|
@@ -9486,20 +9565,6 @@ async function sign$4(algo, hashAlgo, message, publicKey, privateKey, hashed) {
|
|
|
9486
9565
|
case enums.publicKey.ed25519: {
|
|
9487
9566
|
const secretKey = util.concatUint8Array([privateKey, publicKey]);
|
|
9488
9567
|
const signature = nacl.sign.detached(hashed, secretKey);
|
|
9489
|
-
if (config.checkEdDSAFaultySignatures && !nacl.sign.detached.verify(hashed, signature, publicKey)) {
|
|
9490
|
-
/**
|
|
9491
|
-
* Detect faulty signatures caused by random bitflips during `crypto_sign` which could lead to private key extraction
|
|
9492
|
-
* if two signatures over the same message are obtained.
|
|
9493
|
-
* See https://github.com/jedisct1/libsodium/issues/170.
|
|
9494
|
-
* If the input data is not deterministic, e.g. thanks to the random salt in v6 OpenPGP signatures (not yet implemented),
|
|
9495
|
-
* then the generated signature is always safe, and the verification step is skipped.
|
|
9496
|
-
* Otherwise, we need to verify the generated to ensure that no bitflip occured:
|
|
9497
|
-
* - in M between the computation of `r` and `h`.
|
|
9498
|
-
* - in the public key before computing `h`
|
|
9499
|
-
* The verification step is almost 2-3 times as slow as signing, but it's faster than re-signing + re-deriving the public key for separate checks.
|
|
9500
|
-
*/
|
|
9501
|
-
throw new Error('Transient signing failure');
|
|
9502
|
-
}
|
|
9503
9568
|
return { RS: signature };
|
|
9504
9569
|
}
|
|
9505
9570
|
case enums.publicKey.ed448: {
|
|
@@ -11232,7 +11297,7 @@ class ECDHXSymmetricKey {
|
|
|
11232
11297
|
* Encrypts data using specified algorithm and public key parameters.
|
|
11233
11298
|
* See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} for public key algorithms.
|
|
11234
11299
|
* @param {module:enums.publicKey} keyAlgo - Public key algorithm
|
|
11235
|
-
* @param {module:enums.symmetric} symmetricAlgo - Cipher algorithm
|
|
11300
|
+
* @param {module:enums.symmetric|null} symmetricAlgo - Cipher algorithm (v3 only)
|
|
11236
11301
|
* @param {Object} publicParams - Algorithm-specific public key parameters
|
|
11237
11302
|
* @param {Object} privateParams - Algorithm-specific private key parameters
|
|
11238
11303
|
* @param {Uint8Array} data - Data to be encrypted
|
|
@@ -11260,7 +11325,7 @@ async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, privatePar
|
|
|
11260
11325
|
}
|
|
11261
11326
|
case enums.publicKey.x25519:
|
|
11262
11327
|
case enums.publicKey.x448: {
|
|
11263
|
-
if (!util.isAES(symmetricAlgo)) {
|
|
11328
|
+
if (symmetricAlgo && !util.isAES(symmetricAlgo)) {
|
|
11264
11329
|
// see https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/276
|
|
11265
11330
|
throw new Error('X25519 and X448 keys can only encrypt AES session keys');
|
|
11266
11331
|
}
|
|
@@ -11892,9 +11957,26 @@ class Argon2OutOfMemoryError extends Error {
|
|
|
11892
11957
|
let loadArgonWasmModule;
|
|
11893
11958
|
let argon2Promise;
|
|
11894
11959
|
// reload wasm module above this treshold, to deallocated used memory
|
|
11895
|
-
|
|
11960
|
+
// (cannot be declared as a simple `static` field as its not supported by Safari 14)
|
|
11961
|
+
let ARGON2_WASM_MEMORY_THRESHOLD_RELOAD = 2 << 19;
|
|
11896
11962
|
|
|
11897
11963
|
class Argon2S2K {
|
|
11964
|
+
static get ARGON2_WASM_MEMORY_THRESHOLD_RELOAD() {
|
|
11965
|
+
return ARGON2_WASM_MEMORY_THRESHOLD_RELOAD;
|
|
11966
|
+
}
|
|
11967
|
+
|
|
11968
|
+
static set ARGON2_WASM_MEMORY_THRESHOLD_RELOAD(memoryThreshold) {
|
|
11969
|
+
ARGON2_WASM_MEMORY_THRESHOLD_RELOAD = memoryThreshold;
|
|
11970
|
+
}
|
|
11971
|
+
|
|
11972
|
+
static reloadWasmModule() {
|
|
11973
|
+
if (!loadArgonWasmModule) return;
|
|
11974
|
+
|
|
11975
|
+
// it will be awaited if needed at the next `produceKey` invocation
|
|
11976
|
+
argon2Promise = loadArgonWasmModule();
|
|
11977
|
+
argon2Promise.catch(() => {});
|
|
11978
|
+
}
|
|
11979
|
+
|
|
11898
11980
|
/**
|
|
11899
11981
|
* @param {Object} [config] - Full configuration, defaults to openpgp.config
|
|
11900
11982
|
*/
|
|
@@ -11982,10 +12064,8 @@ class Argon2S2K {
|
|
|
11982
12064
|
});
|
|
11983
12065
|
|
|
11984
12066
|
// a lot of memory was used, reload to deallocate
|
|
11985
|
-
if (decodedM > ARGON2_WASM_MEMORY_THRESHOLD_RELOAD) {
|
|
11986
|
-
|
|
11987
|
-
argon2Promise = loadArgonWasmModule();
|
|
11988
|
-
argon2Promise.catch(() => {});
|
|
12067
|
+
if (decodedM > Argon2S2K.ARGON2_WASM_MEMORY_THRESHOLD_RELOAD) {
|
|
12068
|
+
Argon2S2K.reloadWasmModule();
|
|
11989
12069
|
}
|
|
11990
12070
|
return hash;
|
|
11991
12071
|
} catch (e) {
|
|
@@ -14234,6 +14314,14 @@ class KeyID {
|
|
|
14234
14314
|
// Symbol to store cryptographic validity of the signature, to avoid recomputing multiple times on verification.
|
|
14235
14315
|
const verified = Symbol('verified');
|
|
14236
14316
|
|
|
14317
|
+
// A salt notation is used to randomize signatures.
|
|
14318
|
+
// This is to protect EdDSA signatures in particular, which are known to be vulnerable to fault attacks
|
|
14319
|
+
// leading to secret key extraction if two signatures over the same data can be collected (see https://github.com/jedisct1/libsodium/issues/170).
|
|
14320
|
+
// For simplicity, we add the salt to all algos, as it may also serve as protection in case of weaknesses in the hash algo, potentially hindering e.g.
|
|
14321
|
+
// some chosen-prefix attacks.
|
|
14322
|
+
// v6 signatures do not need to rely on this notation, as they already include a separate, built-in salt.
|
|
14323
|
+
const SALT_NOTATION_NAME = 'salt@notations.openpgpjs.org';
|
|
14324
|
+
|
|
14237
14325
|
// GPG puts the Issuer and Signature subpackets in the unhashed area.
|
|
14238
14326
|
// Tampering with those invalidates the signature, so we still trust them and parse them.
|
|
14239
14327
|
// All other unhashed subpackets are ignored.
|
|
@@ -14403,7 +14491,7 @@ class SignaturePacket {
|
|
|
14403
14491
|
* @throws {Error} if signing failed
|
|
14404
14492
|
* @async
|
|
14405
14493
|
*/
|
|
14406
|
-
async sign(key, data, date = new Date(), detached = false) {
|
|
14494
|
+
async sign(key, data, date = new Date(), detached = false, config) {
|
|
14407
14495
|
this.version = key.version;
|
|
14408
14496
|
|
|
14409
14497
|
this.created = util.normalizeDate(date);
|
|
@@ -14413,6 +14501,31 @@ class SignaturePacket {
|
|
|
14413
14501
|
|
|
14414
14502
|
const arr = [new Uint8Array([this.version, this.signatureType, this.publicKeyAlgorithm, this.hashAlgorithm])];
|
|
14415
14503
|
|
|
14504
|
+
// add randomness to the signature
|
|
14505
|
+
if (this.version === 6) {
|
|
14506
|
+
const saltLength = saltLengthForHash(this.hashAlgorithm);
|
|
14507
|
+
if (this.salt === null) {
|
|
14508
|
+
this.salt = mod$1.random.getRandomBytes(saltLength);
|
|
14509
|
+
} else if (saltLength !== this.salt.length) {
|
|
14510
|
+
throw new Error('Provided salt does not have the required length');
|
|
14511
|
+
}
|
|
14512
|
+
} else if (config.nonDeterministicSignaturesViaNotation) {
|
|
14513
|
+
const saltNotations = this.rawNotations.filter(({ name }) => (name === SALT_NOTATION_NAME));
|
|
14514
|
+
// since re-signing the same object is not supported, it's not expected to have multiple salt notations,
|
|
14515
|
+
// but we guard against it as a sanity check
|
|
14516
|
+
if (saltNotations.length === 0) {
|
|
14517
|
+
const saltValue = mod$1.random.getRandomBytes(saltLengthForHash(this.hashAlgorithm));
|
|
14518
|
+
this.rawNotations.push({
|
|
14519
|
+
name: SALT_NOTATION_NAME,
|
|
14520
|
+
value: saltValue,
|
|
14521
|
+
humanReadable: false,
|
|
14522
|
+
critical: false
|
|
14523
|
+
});
|
|
14524
|
+
} else {
|
|
14525
|
+
throw new Error('Unexpected existing salt notation');
|
|
14526
|
+
}
|
|
14527
|
+
}
|
|
14528
|
+
|
|
14416
14529
|
// Add hashed subpackets
|
|
14417
14530
|
arr.push(this.writeHashedSubPackets());
|
|
14418
14531
|
|
|
@@ -14423,14 +14536,6 @@ class SignaturePacket {
|
|
|
14423
14536
|
|
|
14424
14537
|
this.signatureData = util.concat(arr);
|
|
14425
14538
|
|
|
14426
|
-
if (this.version === 6) {
|
|
14427
|
-
const saltLength = saltLengthForHash(this.hashAlgorithm);
|
|
14428
|
-
if (this.salt === null) {
|
|
14429
|
-
this.salt = mod$1.random.getRandomBytes(saltLength);
|
|
14430
|
-
} else if (saltLength !== this.salt.length) {
|
|
14431
|
-
throw new Error('Provided salt does not have the required length');
|
|
14432
|
-
}
|
|
14433
|
-
}
|
|
14434
14539
|
const toHash = this.toHash(this.signatureType, data, detached);
|
|
14435
14540
|
const hash = await this.hash(this.signatureType, data, toHash, detached);
|
|
14436
14541
|
|
|
@@ -16232,9 +16337,12 @@ class PublicKeyEncryptedSessionKeyPacket {
|
|
|
16232
16337
|
}
|
|
16233
16338
|
this.publicKeyAlgorithm = bytes[offset++];
|
|
16234
16339
|
this.encrypted = mod$1.parseEncSessionKeyParams(this.publicKeyAlgorithm, bytes.subarray(offset));
|
|
16235
|
-
if (this.
|
|
16236
|
-
|
|
16237
|
-
|
|
16340
|
+
if (this.publicKeyAlgorithm === enums.publicKey.x25519 || this.publicKeyAlgorithm === enums.publicKey.x448) {
|
|
16341
|
+
if (this.version === 3) {
|
|
16342
|
+
this.sessionKeyAlgorithm = enums.write(enums.symmetric, this.encrypted.C.algorithm);
|
|
16343
|
+
} else if (this.encrypted.C.algorithm !== null) {
|
|
16344
|
+
throw new Error('Unexpected cleartext symmetric algorithm');
|
|
16345
|
+
}
|
|
16238
16346
|
}
|
|
16239
16347
|
}
|
|
16240
16348
|
|
|
@@ -16278,10 +16386,13 @@ class PublicKeyEncryptedSessionKeyPacket {
|
|
|
16278
16386
|
*/
|
|
16279
16387
|
async encrypt(key) {
|
|
16280
16388
|
const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm);
|
|
16281
|
-
|
|
16389
|
+
// No symmetric encryption algorithm identifier is passed to the public-key algorithm for a
|
|
16390
|
+
// v6 PKESK packet, as it is included in the v2 SEIPD packet.
|
|
16391
|
+
const sessionKeyAlgorithm = this.version === 3 ? this.sessionKeyAlgorithm : null;
|
|
16392
|
+
const encoded = encodeSessionKey(this.version, algo, sessionKeyAlgorithm, this.sessionKey);
|
|
16282
16393
|
const privateParams = algo === enums.publicKey.aead ? key.privateParams : null;
|
|
16283
16394
|
this.encrypted = await mod$1.publicKeyEncrypt(
|
|
16284
|
-
algo,
|
|
16395
|
+
algo, sessionKeyAlgorithm, key.publicParams, privateParams, encoded, key.getFingerprintBytes());
|
|
16285
16396
|
}
|
|
16286
16397
|
|
|
16287
16398
|
/**
|
|
@@ -16380,6 +16491,7 @@ function decodeSessionKey(version, keyAlgo, decryptedData, randomSessionKey) {
|
|
|
16380
16491
|
case enums.publicKey.x25519:
|
|
16381
16492
|
case enums.publicKey.x448:
|
|
16382
16493
|
return {
|
|
16494
|
+
sessionKeyAlgorithm: null,
|
|
16383
16495
|
sessionKey: decryptedData
|
|
16384
16496
|
};
|
|
16385
16497
|
default:
|
|
@@ -18067,7 +18179,9 @@ class Signature {
|
|
|
18067
18179
|
* @returns {ReadableStream<String>} ASCII armor.
|
|
18068
18180
|
*/
|
|
18069
18181
|
armor(config$1 = config) {
|
|
18070
|
-
|
|
18182
|
+
// An ASCII-armored sequence of Signature packets that only includes v6 Signature packets MUST NOT contain a CRC24 footer.
|
|
18183
|
+
const emitChecksum = this.packets.some(packet => packet.constructor.tag === SignaturePacket.tag && packet.version !== 6);
|
|
18184
|
+
return armor(enums.armor.signature, this.write(), undefined, undefined, undefined, emitChecksum, config$1);
|
|
18071
18185
|
}
|
|
18072
18186
|
|
|
18073
18187
|
/**
|
|
@@ -18280,7 +18394,7 @@ async function getPreferredCompressionAlgo(keys = [], date = new Date(), userIDs
|
|
|
18280
18394
|
async function getPreferredCipherSuite(keys = [], date = new Date(), userIDs = [], config$1 = config) {
|
|
18281
18395
|
const selfSigs = await Promise.all(keys.map((key, i) => key.getPrimarySelfSignature(date, userIDs[i], config$1)));
|
|
18282
18396
|
const withAEAD = keys.length ?
|
|
18283
|
-
selfSigs.every(selfSig => selfSig.features[0] & enums.features.seipdv2) :
|
|
18397
|
+
!config$1.ignoreSEIPDv2FeatureFlag && selfSigs.every(selfSig => selfSig.features && (selfSig.features[0] & enums.features.seipdv2)) :
|
|
18284
18398
|
config$1.aeadProtect;
|
|
18285
18399
|
|
|
18286
18400
|
if (withAEAD) {
|
|
@@ -18327,8 +18441,8 @@ async function createSignaturePacket(dataToSign, privateKey, signingKeyPacket, s
|
|
|
18327
18441
|
Object.assign(signaturePacket, signatureProperties);
|
|
18328
18442
|
signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm;
|
|
18329
18443
|
signaturePacket.hashAlgorithm = await getPreferredHashAlgo(privateKey, signingKeyPacket, date, userID, config);
|
|
18330
|
-
signaturePacket.rawNotations = notations;
|
|
18331
|
-
await signaturePacket.sign(signingKeyPacket, dataToSign, date, detached);
|
|
18444
|
+
signaturePacket.rawNotations = [...notations];
|
|
18445
|
+
await signaturePacket.sign(signingKeyPacket, dataToSign, date, detached, config);
|
|
18332
18446
|
return signaturePacket;
|
|
18333
18447
|
}
|
|
18334
18448
|
|
|
@@ -18391,7 +18505,7 @@ async function isDataRevoked(primaryKey, signatureType, dataToVerify, revocation
|
|
|
18391
18505
|
!signature || revocationSignature.issuerKeyID.equals(signature.issuerKeyID)
|
|
18392
18506
|
) {
|
|
18393
18507
|
await revocationSignature.verify(
|
|
18394
|
-
key, signatureType, dataToVerify,
|
|
18508
|
+
key, signatureType, dataToVerify, date, false, config
|
|
18395
18509
|
);
|
|
18396
18510
|
|
|
18397
18511
|
// TODO get an identifier of the revoked object instead
|
|
@@ -19661,7 +19775,9 @@ class Key {
|
|
|
19661
19775
|
const revocationSignature = await getLatestValidSignature(this.revocationSignatures, this.keyPacket, enums.signature.keyRevocation, dataToVerify, date, config$1);
|
|
19662
19776
|
const packetlist = new PacketList();
|
|
19663
19777
|
packetlist.push(revocationSignature);
|
|
19664
|
-
|
|
19778
|
+
// An ASCII-armored Transferable Public Key packet sequence of a v6 key MUST NOT contain a CRC24 footer.
|
|
19779
|
+
const emitChecksum = this.keyPacket.version !== 6;
|
|
19780
|
+
return armor(enums.armor.publicKey, packetlist.write(), null, null, 'This is a revocation certificate', emitChecksum, config$1);
|
|
19665
19781
|
}
|
|
19666
19782
|
|
|
19667
19783
|
/**
|
|
@@ -19851,7 +19967,9 @@ class PublicKey extends Key {
|
|
|
19851
19967
|
* @returns {ReadableStream<String>} ASCII armor.
|
|
19852
19968
|
*/
|
|
19853
19969
|
armor(config$1 = config) {
|
|
19854
|
-
|
|
19970
|
+
// An ASCII-armored Transferable Public Key packet sequence of a v6 key MUST NOT contain a CRC24 footer.
|
|
19971
|
+
const emitChecksum = this.keyPacket.version !== 6;
|
|
19972
|
+
return armor(enums.armor.publicKey, this.toPacketList().write(), undefined, undefined, undefined, emitChecksum, config$1);
|
|
19855
19973
|
}
|
|
19856
19974
|
}
|
|
19857
19975
|
|
|
@@ -19924,7 +20042,9 @@ class PrivateKey extends PublicKey {
|
|
|
19924
20042
|
* @returns {ReadableStream<String>} ASCII armor.
|
|
19925
20043
|
*/
|
|
19926
20044
|
armor(config$1 = config) {
|
|
19927
|
-
|
|
20045
|
+
// An ASCII-armored Transferable Public Key packet sequence of a v6 key MUST NOT contain a CRC24 footer.
|
|
20046
|
+
const emitChecksum = this.keyPacket.version !== 6;
|
|
20047
|
+
return armor(enums.armor.privateKey, this.toPacketList().write(), undefined, undefined, undefined, emitChecksum, config$1);
|
|
19928
20048
|
}
|
|
19929
20049
|
|
|
19930
20050
|
/**
|
|
@@ -21243,7 +21363,13 @@ class Message {
|
|
|
21243
21363
|
* @returns {ReadableStream<String>} ASCII armor.
|
|
21244
21364
|
*/
|
|
21245
21365
|
armor(config$1 = config) {
|
|
21246
|
-
|
|
21366
|
+
const trailingPacket = this.packets[this.packets.length - 1];
|
|
21367
|
+
// An ASCII-armored Encrypted Message packet sequence that ends in an v2 SEIPD packet MUST NOT contain a CRC24 footer.
|
|
21368
|
+
// An ASCII-armored sequence of Signature packets that only includes v6 Signature packets MUST NOT contain a CRC24 footer.
|
|
21369
|
+
const emitChecksum = trailingPacket.constructor.tag === SymEncryptedIntegrityProtectedDataPacket.tag ?
|
|
21370
|
+
trailingPacket.version !== 2 :
|
|
21371
|
+
this.packets.some(packet => packet.constructor.tag === SignaturePacket.tag && packet.version !== 6);
|
|
21372
|
+
return armor(enums.armor.message, this.write(), null, null, null, emitChecksum, config$1);
|
|
21247
21373
|
}
|
|
21248
21374
|
}
|
|
21249
21375
|
|
|
@@ -21578,9 +21704,9 @@ class CleartextMessage {
|
|
|
21578
21704
|
* @returns {String | ReadableStream<String>} ASCII armor.
|
|
21579
21705
|
*/
|
|
21580
21706
|
armor(config$1 = config) {
|
|
21581
|
-
// emit header if one of the signatures has a version not 6
|
|
21582
|
-
const
|
|
21583
|
-
const hash =
|
|
21707
|
+
// emit header and checksum if one of the signatures has a version not 6
|
|
21708
|
+
const emitHeaderAndChecksum = this.signature.packets.some(packet => packet.version !== 6);
|
|
21709
|
+
const hash = emitHeaderAndChecksum ?
|
|
21584
21710
|
Array.from(new Set(this.signature.packets.map(
|
|
21585
21711
|
packet => enums.read(enums.hash, packet.hashAlgorithm).toUpperCase()
|
|
21586
21712
|
))).join() :
|
|
@@ -21591,7 +21717,9 @@ class CleartextMessage {
|
|
|
21591
21717
|
text: this.text,
|
|
21592
21718
|
data: this.signature.packets.write()
|
|
21593
21719
|
};
|
|
21594
|
-
|
|
21720
|
+
|
|
21721
|
+
// An ASCII-armored sequence of Signature packets that only includes v6 Signature packets MUST NOT contain a CRC24 footer.
|
|
21722
|
+
return armor(enums.armor.signed, body, undefined, undefined, undefined, emitHeaderAndChecksum, config$1);
|
|
21595
21723
|
}
|
|
21596
21724
|
}
|
|
21597
21725
|
|
package/openpgp.d.ts
CHANGED
|
@@ -324,12 +324,12 @@ interface Config {
|
|
|
324
324
|
showVersion: boolean;
|
|
325
325
|
showComment: boolean;
|
|
326
326
|
aeadProtect: boolean;
|
|
327
|
+
ignoreSEIPDv2FeatureFlag: boolean;
|
|
327
328
|
allowUnauthenticatedMessages: boolean;
|
|
328
329
|
allowUnauthenticatedStream: boolean;
|
|
329
330
|
allowForwardedMessages: boolean;
|
|
330
331
|
minRSABits: number;
|
|
331
332
|
passwordCollisionCheck: boolean;
|
|
332
|
-
revocationsExpire: boolean;
|
|
333
333
|
ignoreUnsupportedPackets: boolean;
|
|
334
334
|
ignoreMalformedPackets: boolean;
|
|
335
335
|
versionString: string;
|
|
@@ -353,7 +353,7 @@ interface Config {
|
|
|
353
353
|
rejectPublicKeyAlgorithms: Set<enums.publicKey>;
|
|
354
354
|
rejectCurves: Set<enums.curve>;
|
|
355
355
|
}
|
|
356
|
-
export var config: Config
|
|
356
|
+
export var config: Config;
|
|
357
357
|
|
|
358
358
|
// PartialConfig has the same properties as Config, but declared as optional.
|
|
359
359
|
// This interface is relevant for top-level functions, which accept a subset of configuration options
|
|
@@ -738,7 +738,7 @@ export interface VerifyMessageResult<T extends MaybeStream<Data> = MaybeStream<D
|
|
|
738
738
|
/**
|
|
739
739
|
* Armor an OpenPGP binary packet block
|
|
740
740
|
*/
|
|
741
|
-
export function armor(messagetype: enums.armor, body: object, partindex?: number, parttotal?: number, customComment?: string, config?: Config): string;
|
|
741
|
+
export function armor(messagetype: enums.armor, body: object, partindex?: number, parttotal?: number, customComment?: string, emitChecksum?: boolean, config?: Config): string;
|
|
742
742
|
|
|
743
743
|
/**
|
|
744
744
|
* DeArmor an OpenPGP armored message; verify the checksum and return the encoded bytes
|
|
@@ -938,6 +938,8 @@ export namespace enums {
|
|
|
938
938
|
}
|
|
939
939
|
|
|
940
940
|
export declare class Argon2S2K {
|
|
941
|
+
static reloadWasmModule(): void;
|
|
942
|
+
static ARGON2_WASM_MEMORY_THRESHOLD_RELOAD: number;
|
|
941
943
|
constructor(config: Config);
|
|
942
944
|
salt: Uint8Array;
|
|
943
945
|
/** @throws Argon2OutOfMemoryError */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protontech/openpgp",
|
|
3
3
|
"description": "OpenPGP.js is a Javascript implementation of the OpenPGP protocol. This is defined in RFC 4880.",
|
|
4
|
-
"version": "6.0.0-
|
|
4
|
+
"version": "6.0.0-beta.0.patch.0",
|
|
5
5
|
"license": "LGPL-3.0+",
|
|
6
6
|
"homepage": "https://openpgpjs.org/",
|
|
7
7
|
"engines": {
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"@rollup/plugin-replace": "^5.0.5",
|
|
76
76
|
"@rollup/plugin-terser": "^0.4.4",
|
|
77
77
|
"@rollup/plugin-wasm": "^6.2.2",
|
|
78
|
-
"@types/chai": "^4.3.
|
|
78
|
+
"@types/chai": "^4.3.14",
|
|
79
79
|
"argon2id": "^1.0.1",
|
|
80
80
|
"benchmark": "^2.1.4",
|
|
81
81
|
"c8": "^8.0.1",
|
|
@@ -93,17 +93,17 @@
|
|
|
93
93
|
"karma": "^6.4.3",
|
|
94
94
|
"karma-browserstack-launcher": "^1.6.0",
|
|
95
95
|
"karma-chrome-launcher": "^3.2.0",
|
|
96
|
-
"karma-firefox-launcher": "^2.1.
|
|
96
|
+
"karma-firefox-launcher": "^2.1.3",
|
|
97
97
|
"karma-mocha": "^2.0.1",
|
|
98
98
|
"karma-mocha-reporter": "^2.2.5",
|
|
99
99
|
"karma-webkit-launcher": "^2.4.0",
|
|
100
|
-
"mocha": "^10.
|
|
101
|
-
"playwright": "^1.
|
|
102
|
-
"rollup": "^4.
|
|
103
|
-
"sinon": "^
|
|
100
|
+
"mocha": "^10.4.0",
|
|
101
|
+
"playwright": "^1.43.0",
|
|
102
|
+
"rollup": "^4.14.1",
|
|
103
|
+
"sinon": "^17.0.1",
|
|
104
104
|
"ts-node": "^10.9.2",
|
|
105
|
-
"tsx": "^4.7.
|
|
106
|
-
"typescript": "^5.
|
|
105
|
+
"tsx": "^4.7.2",
|
|
106
|
+
"typescript": "^5.4.4",
|
|
107
107
|
"web-streams-polyfill": "^3.3.3"
|
|
108
108
|
},
|
|
109
109
|
"repository": {
|