Implement Key.prototype.addSubkey
(#963)
This commit is contained in:
parent
9b5124d5cd
commit
7f40ab0940
96
src/key.js
96
src/key.js
|
@ -826,6 +826,37 @@ Key.prototype.verifyAllUsers = async function(keys) {
|
||||||
return results;
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new OpenPGP subkey, and returns a clone of the Key object with the new subkey added.
|
||||||
|
* Supports RSA and ECC keys. Defaults to the algorithm and bit size/curve of the primary key.
|
||||||
|
* @param {Integer} options.numBits number of bits for the key creation.
|
||||||
|
* @param {Number} [options.keyExpirationTime=0]
|
||||||
|
* The number of seconds after the key creation time that the key expires
|
||||||
|
* @param {String} curve (optional) Elliptic curve for ECC keys
|
||||||
|
* @param {Date} date (optional) Override the creation date of the key and the key signatures
|
||||||
|
* @param {Boolean} subkeys (optional) Indicates whether the subkey should sign rather than encrypt. Defaults to false
|
||||||
|
* @returns {Promise<module:key.Key>}
|
||||||
|
* @async
|
||||||
|
*/
|
||||||
|
Key.prototype.addSubkey = async function(options = {}) {
|
||||||
|
if (!this.isPrivate()) {
|
||||||
|
throw new Error("Cannot add a subkey to a public key");
|
||||||
|
}
|
||||||
|
const defaultOptions = this.primaryKey.getAlgorithmInfo();
|
||||||
|
defaultOptions.numBits = defaultOptions.bits;
|
||||||
|
const secretKeyPacket = this.primaryKey;
|
||||||
|
if (!secretKeyPacket.isDecrypted()) {
|
||||||
|
throw new Error("Key is not decrypted");
|
||||||
|
}
|
||||||
|
options = sanitizeKeyOptions(options, defaultOptions);
|
||||||
|
const keyPacket = await generateSecretSubkey(options);
|
||||||
|
const bindingSignature = await createBindingSignature(keyPacket, secretKeyPacket, options);
|
||||||
|
const packetList = this.toPacketlist();
|
||||||
|
packetList.push(keyPacket);
|
||||||
|
packetList.push(bindingSignature);
|
||||||
|
return new Key(packetList);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class
|
* @class
|
||||||
* @classdesc Class that represents an user ID or attribute packet and the relevant signatures.
|
* @classdesc Class that represents an user ID or attribute packet and the relevant signatures.
|
||||||
|
@ -1328,8 +1359,9 @@ export async function generate(options) {
|
||||||
let promises = [generateSecretKey(options)];
|
let promises = [generateSecretKey(options)];
|
||||||
promises = promises.concat(options.subkeys.map(generateSecretSubkey));
|
promises = promises.concat(options.subkeys.map(generateSecretSubkey));
|
||||||
return Promise.all(promises).then(packets => wrapKeyObject(packets[0], packets.slice(1), options));
|
return Promise.all(promises).then(packets => wrapKeyObject(packets[0], packets.slice(1), options));
|
||||||
|
}
|
||||||
|
|
||||||
function sanitizeKeyOptions(options, subkeyDefaults = {}) {
|
function sanitizeKeyOptions(options, subkeyDefaults = {}) {
|
||||||
options.curve = options.curve || subkeyDefaults.curve;
|
options.curve = options.curve || subkeyDefaults.curve;
|
||||||
options.numBits = options.numBits || subkeyDefaults.numBits;
|
options.numBits = options.numBits || subkeyDefaults.numBits;
|
||||||
options.keyExpirationTime = options.keyExpirationTime !== undefined ? options.keyExpirationTime : subkeyDefaults.keyExpirationTime;
|
options.keyExpirationTime = options.keyExpirationTime !== undefined ? options.keyExpirationTime : subkeyDefaults.keyExpirationTime;
|
||||||
|
@ -1358,23 +1390,22 @@ export async function generate(options) {
|
||||||
throw new Error('Unrecognized key type');
|
throw new Error('Unrecognized key type');
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateSecretKey(options) {
|
async function generateSecretKey(options) {
|
||||||
const secretKeyPacket = new packet.SecretKey(options.date);
|
const secretKeyPacket = new packet.SecretKey(options.date);
|
||||||
secretKeyPacket.packets = null;
|
secretKeyPacket.packets = null;
|
||||||
secretKeyPacket.algorithm = enums.read(enums.publicKey, options.algorithm);
|
secretKeyPacket.algorithm = enums.read(enums.publicKey, options.algorithm);
|
||||||
await secretKeyPacket.generate(options.numBits, options.curve);
|
await secretKeyPacket.generate(options.numBits, options.curve);
|
||||||
return secretKeyPacket;
|
return secretKeyPacket;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateSecretSubkey(options) {
|
async function generateSecretSubkey(options) {
|
||||||
const secretSubkeyPacket = new packet.SecretSubkey(options.date);
|
const secretSubkeyPacket = new packet.SecretSubkey(options.date);
|
||||||
secretSubkeyPacket.packets = null;
|
secretSubkeyPacket.packets = null;
|
||||||
secretSubkeyPacket.algorithm = enums.read(enums.publicKey, options.algorithm);
|
secretSubkeyPacket.algorithm = enums.read(enums.publicKey, options.algorithm);
|
||||||
await secretSubkeyPacket.generate(options.numBits, options.curve);
|
await secretSubkeyPacket.generate(options.numBits, options.curve);
|
||||||
return secretSubkeyPacket;
|
return secretSubkeyPacket;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1541,27 +1572,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) {
|
||||||
|
|
||||||
await Promise.all(secretSubkeyPackets.map(async function(secretSubkeyPacket, index) {
|
await Promise.all(secretSubkeyPackets.map(async function(secretSubkeyPacket, index) {
|
||||||
const subkeyOptions = options.subkeys[index];
|
const subkeyOptions = options.subkeys[index];
|
||||||
const dataToSign = {};
|
const subkeySignaturePacket = await createBindingSignature(secretSubkeyPacket, secretKeyPacket, subkeyOptions);
|
||||||
dataToSign.key = secretKeyPacket;
|
|
||||||
dataToSign.bind = secretSubkeyPacket;
|
|
||||||
const subkeySignaturePacket = new packet.Signature(subkeyOptions.date);
|
|
||||||
subkeySignaturePacket.signatureType = enums.signature.subkey_binding;
|
|
||||||
subkeySignaturePacket.publicKeyAlgorithm = secretKeyPacket.algorithm;
|
|
||||||
subkeySignaturePacket.hashAlgorithm = await getPreferredHashAlgo(null, secretSubkeyPacket);
|
|
||||||
if (subkeyOptions.sign) {
|
|
||||||
subkeySignaturePacket.keyFlags = [enums.keyFlags.sign_data];
|
|
||||||
subkeySignaturePacket.embeddedSignature = await createSignaturePacket(dataToSign, null, secretSubkeyPacket, {
|
|
||||||
signatureType: enums.signature.key_binding
|
|
||||||
}, subkeyOptions.date);
|
|
||||||
} else {
|
|
||||||
subkeySignaturePacket.keyFlags = [enums.keyFlags.encrypt_communication | enums.keyFlags.encrypt_storage];
|
|
||||||
}
|
|
||||||
if (subkeyOptions.keyExpirationTime > 0) {
|
|
||||||
subkeySignaturePacket.keyExpirationTime = subkeyOptions.keyExpirationTime;
|
|
||||||
subkeySignaturePacket.keyNeverExpires = false;
|
|
||||||
}
|
|
||||||
await subkeySignaturePacket.sign(secretKeyPacket, dataToSign);
|
|
||||||
|
|
||||||
return { secretSubkeyPacket, subkeySignaturePacket };
|
return { secretSubkeyPacket, subkeySignaturePacket };
|
||||||
})).then(packets => {
|
})).then(packets => {
|
||||||
packets.forEach(({ secretSubkeyPacket, subkeySignaturePacket }) => {
|
packets.forEach(({ secretSubkeyPacket, subkeySignaturePacket }) => {
|
||||||
|
@ -1594,6 +1605,37 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) {
|
||||||
return new Key(packetlist);
|
return new Key(packetlist);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create subkey binding signature
|
||||||
|
* @see {@link https://tools.ietf.org/html/rfc4880#section-5.2.1|RFC4880 Section 5.2.1}
|
||||||
|
* @param {module:packet.SecretSubkey} subkey Subkey key packet
|
||||||
|
* @param {module:packet.SecretKey} primaryKey Primary key packet
|
||||||
|
* @param {Object} options
|
||||||
|
*/
|
||||||
|
async function createBindingSignature(subkey, primaryKey, options) {
|
||||||
|
const dataToSign = {};
|
||||||
|
dataToSign.key = primaryKey;
|
||||||
|
dataToSign.bind = subkey;
|
||||||
|
const subkeySignaturePacket = new packet.Signature(options.date);
|
||||||
|
subkeySignaturePacket.signatureType = enums.signature.subkey_binding;
|
||||||
|
subkeySignaturePacket.publicKeyAlgorithm = primaryKey.algorithm;
|
||||||
|
subkeySignaturePacket.hashAlgorithm = await getPreferredHashAlgo(null, subkey);
|
||||||
|
if (options.sign) {
|
||||||
|
subkeySignaturePacket.keyFlags = [enums.keyFlags.sign_data];
|
||||||
|
subkeySignaturePacket.embeddedSignature = await createSignaturePacket(dataToSign, null, subkey, {
|
||||||
|
signatureType: enums.signature.key_binding
|
||||||
|
}, options.date);
|
||||||
|
} else {
|
||||||
|
subkeySignaturePacket.keyFlags = [enums.keyFlags.encrypt_communication | enums.keyFlags.encrypt_storage];
|
||||||
|
}
|
||||||
|
if (options.keyExpirationTime > 0) {
|
||||||
|
subkeySignaturePacket.keyExpirationTime = options.keyExpirationTime;
|
||||||
|
subkeySignaturePacket.keyNeverExpires = false;
|
||||||
|
}
|
||||||
|
await subkeySignaturePacket.sign(primaryKey, dataToSign);
|
||||||
|
return subkeySignaturePacket;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a given certificate or binding signature is revoked
|
* Checks if a given certificate or binding signature is revoked
|
||||||
* @param {module:packet.SecretKey|
|
* @param {module:packet.SecretKey|
|
||||||
|
|
|
@ -2865,3 +2865,173 @@ VYGdb3eNlV8CfoEC
|
||||||
})()).to.be.rejectedWith('Key packet is already encrypted');
|
})()).to.be.rejectedWith('Key packet is already encrypted');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('addSubkey functionality testing', function(){
|
||||||
|
it('create and add a new rsa subkey to a rsa key', async function() {
|
||||||
|
const privateKey = (await openpgp.key.readArmored(priv_key_rsa)).keys[0];
|
||||||
|
await privateKey.decrypt('hello world');
|
||||||
|
const total = privateKey.subKeys.length;
|
||||||
|
let newPrivateKey = await privateKey.addSubkey();
|
||||||
|
const armoredKey = newPrivateKey.armor();
|
||||||
|
newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
|
||||||
|
const subKey = newPrivateKey.subKeys[total];
|
||||||
|
expect(subKey).to.exist;
|
||||||
|
expect(newPrivateKey.subKeys.length).to.be.equal(total+1);
|
||||||
|
const subkeyN = subKey.keyPacket.params[0];
|
||||||
|
const pkN = privateKey.primaryKey.params[0];
|
||||||
|
expect(subkeyN.byteLength()).to.be.equal(pkN.byteLength());
|
||||||
|
expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('rsa_encrypt_sign');
|
||||||
|
expect(await subKey.verify(newPrivateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('encrypt and decrypt key with added subkey', async function() {
|
||||||
|
const privateKey = (await openpgp.key.readArmored(priv_key_rsa)).keys[0];
|
||||||
|
await privateKey.decrypt('hello world');
|
||||||
|
const total = privateKey.subKeys.length;
|
||||||
|
let newPrivateKey = await privateKey.addSubkey();
|
||||||
|
newPrivateKey = (await openpgp.key.readArmored(newPrivateKey.armor())).keys[0];
|
||||||
|
await newPrivateKey.encrypt('12345678');
|
||||||
|
const armoredKey = newPrivateKey.armor();
|
||||||
|
let importedPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
|
||||||
|
await importedPrivateKey.decrypt('12345678');
|
||||||
|
const subKey = importedPrivateKey.subKeys[total];
|
||||||
|
expect(subKey).to.exist;
|
||||||
|
expect(importedPrivateKey.subKeys.length).to.be.equal(total+1);
|
||||||
|
const subkeyN = subKey.keyPacket.params[0];
|
||||||
|
const pkN = privateKey.primaryKey.params[0];
|
||||||
|
expect(subkeyN.byteLength()).to.be.equal(pkN.byteLength());
|
||||||
|
expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('rsa_encrypt_sign');
|
||||||
|
expect(await subKey.verify(importedPrivateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create and add a new ec subkey to a ec key', async function() {
|
||||||
|
const userId = 'test <a@b.com>';
|
||||||
|
const opt = {curve: 'curve25519', userIds: [userId], subkeys:[]};
|
||||||
|
const privateKey = (await openpgp.generateKey(opt)).key;
|
||||||
|
const total = privateKey.subKeys.length;
|
||||||
|
const opt2 = {curve: 'curve25519', userIds: [userId], sign: true};
|
||||||
|
let newPrivateKey = await privateKey.addSubkey(opt2);
|
||||||
|
const subKey1 = newPrivateKey.subKeys[total];
|
||||||
|
await newPrivateKey.encrypt('12345678');
|
||||||
|
const armoredKey = newPrivateKey.armor();
|
||||||
|
newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
|
||||||
|
await newPrivateKey.decrypt('12345678');
|
||||||
|
const subKey = newPrivateKey.subKeys[total];
|
||||||
|
expect(subKey.isDecrypted()).to.be.true;
|
||||||
|
expect(subKey1.getKeyId().toHex()).to.be.equal(subKey.getKeyId().toHex());
|
||||||
|
expect(subKey).to.exist;
|
||||||
|
expect(newPrivateKey.subKeys.length).to.be.equal(total+1);
|
||||||
|
const subkeyOid = subKey.keyPacket.params[0];
|
||||||
|
const pkOid = privateKey.primaryKey.params[0];
|
||||||
|
expect(subkeyOid.getName()).to.be.equal(pkOid.getName());
|
||||||
|
expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('eddsa');
|
||||||
|
expect(await subKey.verify(privateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create and add a new ec subkey to a rsa key', async function() {
|
||||||
|
const privateKey = (await openpgp.key.readArmored(priv_key_rsa)).keys[0];
|
||||||
|
privateKey.subKeys = [];
|
||||||
|
await privateKey.decrypt('hello world');
|
||||||
|
const total = privateKey.subKeys.length;
|
||||||
|
const opt2 = {curve: 'curve25519'};
|
||||||
|
let newPrivateKey = await privateKey.addSubkey(opt2);
|
||||||
|
const armoredKey = newPrivateKey.armor();
|
||||||
|
newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
|
||||||
|
const subKey = newPrivateKey.subKeys[total];
|
||||||
|
expect(subKey).to.exist;
|
||||||
|
expect(newPrivateKey.subKeys.length).to.be.equal(total+1);
|
||||||
|
expect(subKey.keyPacket.params[0].getName()).to.be.equal(openpgp.enums.curve.curve25519);
|
||||||
|
expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('ecdh');
|
||||||
|
expect(await subKey.verify(privateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sign/verify data with the new subkey correctly using curve25519', async function() {
|
||||||
|
const userId = 'test <a@b.com>';
|
||||||
|
const opt = {curve: 'curve25519', userIds: [userId], subkeys:[]};
|
||||||
|
const privateKey = (await openpgp.generateKey(opt)).key;
|
||||||
|
const total = privateKey.subKeys.length;
|
||||||
|
const opt2 = {sign: true};
|
||||||
|
let newPrivateKey = await privateKey.addSubkey(opt2);
|
||||||
|
const armoredKey = newPrivateKey.armor();
|
||||||
|
newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
|
||||||
|
const subKey = newPrivateKey.subKeys[total];
|
||||||
|
const subkeyOid = subKey.keyPacket.params[0];
|
||||||
|
const pkOid = newPrivateKey.primaryKey.params[0];
|
||||||
|
expect(subkeyOid.getName()).to.be.equal(pkOid.getName());
|
||||||
|
expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('eddsa');
|
||||||
|
expect(await subKey.verify(newPrivateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid);
|
||||||
|
expect(await newPrivateKey.getSigningKey()).to.be.equal(subKey);
|
||||||
|
const signed = await openpgp.sign({message: openpgp.cleartext.fromText('the data to signed'), privateKeys: newPrivateKey, armor:false});
|
||||||
|
const verified = await signed.message.verify([newPrivateKey.toPublic()]);
|
||||||
|
expect(verified).to.exist;
|
||||||
|
expect(verified.length).to.be.equal(1);
|
||||||
|
expect(await verified[0].keyid).to.be.equal(subKey.getKeyId());
|
||||||
|
expect(await verified[0].verified).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('encrypt/decrypt data with the new subkey correctly using curve25519', async function() {
|
||||||
|
const userId = 'test <a@b.com>';
|
||||||
|
const vData = 'the data to encrypted!';
|
||||||
|
const opt = {curve: 'curve25519', userIds: [userId], subkeys:[]};
|
||||||
|
const privateKey = (await openpgp.generateKey(opt)).key;
|
||||||
|
const total = privateKey.subKeys.length;
|
||||||
|
let newPrivateKey = await privateKey.addSubkey();
|
||||||
|
const armoredKey = newPrivateKey.armor();
|
||||||
|
newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
|
||||||
|
const subKey = newPrivateKey.subKeys[total];
|
||||||
|
const publicKey = newPrivateKey.toPublic();
|
||||||
|
expect(await subKey.verify(newPrivateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid);
|
||||||
|
expect(await newPrivateKey.getEncryptionKey()).to.be.equal(subKey);
|
||||||
|
const encrypted = await openpgp.encrypt({message: openpgp.message.fromText(vData), publicKeys: publicKey, armor:false});
|
||||||
|
expect(encrypted.message).to.be.exist;
|
||||||
|
const pkSessionKeys = encrypted.message.packets.filterByTag(openpgp.enums.packet.publicKeyEncryptedSessionKey);
|
||||||
|
expect(pkSessionKeys).to.exist;
|
||||||
|
expect(pkSessionKeys.length).to.be.equal(1);
|
||||||
|
expect(pkSessionKeys[0].publicKeyId.toHex()).to.be.equals(subKey.keyPacket.getKeyId().toHex());
|
||||||
|
const decrypted = await openpgp.decrypt({message: encrypted.message, privateKeys: newPrivateKey})
|
||||||
|
expect(decrypted).to.exist;
|
||||||
|
expect(decrypted.data).to.be.equal(vData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sign/verify data with the new subkey correctly using rsa', async function() {
|
||||||
|
const privateKey = (await openpgp.key.readArmored(priv_key_rsa)).keys[0];
|
||||||
|
await privateKey.decrypt('hello world');
|
||||||
|
const total = privateKey.subKeys.length;
|
||||||
|
const opt2 = {sign: true};
|
||||||
|
let newPrivateKey = await privateKey.addSubkey(opt2);
|
||||||
|
const armoredKey = newPrivateKey.armor();
|
||||||
|
newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
|
||||||
|
const subKey = newPrivateKey.subKeys[total];
|
||||||
|
expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('rsa_encrypt_sign');
|
||||||
|
expect(await subKey.verify(newPrivateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid);
|
||||||
|
expect(await newPrivateKey.getSigningKey()).to.be.equal(subKey);
|
||||||
|
const signed = await openpgp.sign({message: openpgp.cleartext.fromText('the data to signed'), privateKeys: newPrivateKey, armor:false});
|
||||||
|
const verified = await signed.message.verify([newPrivateKey.toPublic()]);
|
||||||
|
expect(verified).to.exist;
|
||||||
|
expect(verified.length).to.be.equal(1);
|
||||||
|
expect(await verified[0].keyid).to.be.equal(subKey.getKeyId());
|
||||||
|
expect(await verified[0].verified).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('encrypt/decrypt data with the new subkey correctly using rsa', async function() {
|
||||||
|
const privateKey = (await openpgp.key.readArmored(priv_key_rsa)).keys[0];
|
||||||
|
await privateKey.decrypt('hello world');
|
||||||
|
const total = privateKey.subKeys.length;
|
||||||
|
let newPrivateKey = await privateKey.addSubkey();
|
||||||
|
const armoredKey = newPrivateKey.armor();
|
||||||
|
newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
|
||||||
|
const subKey = newPrivateKey.subKeys[total];
|
||||||
|
const publicKey = newPrivateKey.toPublic();
|
||||||
|
const vData = 'the data to encrypted!';
|
||||||
|
expect(await newPrivateKey.getEncryptionKey()).to.be.equal(subKey);
|
||||||
|
const encrypted = await openpgp.encrypt({message: openpgp.message.fromText(vData), publicKeys: publicKey, armor:false});
|
||||||
|
expect(encrypted.message).to.be.exist;
|
||||||
|
const pkSessionKeys = encrypted.message.packets.filterByTag(openpgp.enums.packet.publicKeyEncryptedSessionKey);
|
||||||
|
expect(pkSessionKeys).to.exist;
|
||||||
|
expect(pkSessionKeys.length).to.be.equal(1);
|
||||||
|
expect(pkSessionKeys[0].publicKeyId.toHex()).to.be.equals(subKey.keyPacket.getKeyId().toHex());
|
||||||
|
const decrypted = await openpgp.decrypt({message: encrypted.message, privateKeys: newPrivateKey})
|
||||||
|
expect(decrypted).to.exist;
|
||||||
|
expect(decrypted.data).to.be.equal(vData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user