mirror of
https://github.com/moparisthebest/open-keychain
synced 2025-02-12 05:00:22 -05:00
new-edit: add new save keyring stuff
This commit is contained in:
parent
a0f546739d
commit
5c47143d64
@ -171,173 +171,139 @@ public class PgpKeyOperation {
|
|||||||
return new PGPSecretKey(keyPair.getPrivateKey(), keyPair.getPublicKey(),
|
return new PGPSecretKey(keyPair.getPrivateKey(), keyPair.getPublicKey(),
|
||||||
sha1Calc, isMasterKey, keyEncryptor);
|
sha1Calc, isMasterKey, keyEncryptor);
|
||||||
} catch(NoSuchProviderException e) {
|
} catch(NoSuchProviderException e) {
|
||||||
throw new PgpGeneralMsgIdException(R.string.error_encoding, e);
|
throw new RuntimeException(e);
|
||||||
} catch(NoSuchAlgorithmException e) {
|
} catch(NoSuchAlgorithmException e) {
|
||||||
throw new PgpGeneralMsgIdException(R.string.error_encoding, e);
|
throw new RuntimeException(e);
|
||||||
} catch(InvalidAlgorithmParameterException e) {
|
} catch(InvalidAlgorithmParameterException e) {
|
||||||
throw new PgpGeneralMsgIdException(R.string.error_encoding, e);
|
throw new RuntimeException(e);
|
||||||
} catch(PGPException e) {
|
} catch(PGPException e) {
|
||||||
throw new PgpGeneralMsgIdException(R.string.error_encoding, e);
|
throw new PgpGeneralMsgIdException(R.string.msg_mr_error_pgp, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Pair<PGPSecretKeyRing, PGPPublicKeyRing> buildSecretKey(PGPSecretKeyRing sKR,
|
/** This method introduces a list of modifications specified by a SaveKeyringParcel to a
|
||||||
PGPPublicKeyRing pKR,
|
* PGPSecretKeyRing.
|
||||||
SaveKeyringParcel saveParcel,
|
*
|
||||||
String passphrase)
|
* Note that PGPPublicKeyRings can not be directly modified. Instead, the corresponding
|
||||||
|
* PGPSecretKeyRing must be modified and consequently consolidated with its public counterpart.
|
||||||
|
* This is a natural workflow since pgp keyrings are immutable data structures: Old semantics
|
||||||
|
* are changed by adding new certificates, which implicitly override older certificates.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public UncachedKeyRing modifySecretKeyRing(WrappedSecretKeyRing wsKR, SaveKeyringParcel saveParcel,
|
||||||
|
String passphrase)
|
||||||
throws PgpGeneralMsgIdException, PGPException, SignatureException, IOException {
|
throws PgpGeneralMsgIdException, PGPException, SignatureException, IOException {
|
||||||
|
|
||||||
updateProgress(R.string.progress_building_key, 0, 100);
|
|
||||||
|
|
||||||
// sort these, so we can use binarySearch later on
|
|
||||||
Arrays.sort(saveParcel.revokeSubKeys);
|
|
||||||
Arrays.sort(saveParcel.revokeUserIds);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* What's gonna happen here:
|
|
||||||
*
|
|
||||||
* 1. Unlock private key
|
* 1. Unlock private key
|
||||||
*
|
* 2a. Add certificates for new user ids
|
||||||
* 2. Create new secret key ring
|
* 2b. Add revocations for revoked user ids
|
||||||
*
|
* 3. If primary user id changed, generate new certificates for both old and new
|
||||||
* 3. Copy subkeys
|
* 4a. For each subkey change, generate new subkey binding certificate
|
||||||
* - Generate revocation if requested
|
* 4b. For each subkey revocation, generate new subkey revocation certificate
|
||||||
* - Copy old cert, or generate new if change requested
|
* 5. Generate and add new subkeys
|
||||||
*
|
* 6. If requested, change passphrase
|
||||||
* 4. Generate and add new subkeys
|
|
||||||
*
|
|
||||||
* 5. Copy user ids
|
|
||||||
* - Generate revocation if requested
|
|
||||||
* - Copy old cert, or generate new if primary user id status changed
|
|
||||||
*
|
|
||||||
* 6. Add new user ids
|
|
||||||
*
|
|
||||||
* 7. Generate PublicKeyRing from SecretKeyRing
|
|
||||||
*
|
|
||||||
* 8. Return pair (PublicKeyRing,SecretKeyRing)
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 1. Unlock private key
|
|
||||||
updateProgress(R.string.progress_building_key, 0, 100);
|
updateProgress(R.string.progress_building_key, 0, 100);
|
||||||
|
|
||||||
|
// We work on bouncycastle object level here
|
||||||
|
PGPSecretKeyRing sKR = wsKR.getRing();
|
||||||
PGPPublicKey masterPublicKey = sKR.getPublicKey();
|
PGPPublicKey masterPublicKey = sKR.getPublicKey();
|
||||||
|
PGPSecretKey masterSecretKey = sKR.getSecretKey();
|
||||||
|
|
||||||
|
// 1. Unlock private key
|
||||||
PGPPrivateKey masterPrivateKey; {
|
PGPPrivateKey masterPrivateKey; {
|
||||||
PGPSecretKey masterKey = sKR.getSecretKey();
|
|
||||||
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
|
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
|
||||||
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.toCharArray());
|
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.toCharArray());
|
||||||
masterPrivateKey = masterKey.extractPrivateKey(keyDecryptor);
|
masterPrivateKey = masterSecretKey.extractPrivateKey(keyDecryptor);
|
||||||
|
}
|
||||||
|
if (!Arrays.equals(saveParcel.mFingerprint, sKR.getPublicKey().getFingerprint())) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Create new secret key ring
|
|
||||||
updateProgress(R.string.progress_certifying_master_key, 20, 100);
|
updateProgress(R.string.progress_certifying_master_key, 20, 100);
|
||||||
|
|
||||||
// Note we do NOT use PGPKeyRingGeneraor, it's just one level too high and does stuff
|
{ // work on master secret key
|
||||||
// we want to do manually. Instead, we simply use a list of secret keys.
|
|
||||||
ArrayList<PGPSecretKey> secretKeys = new ArrayList<PGPSecretKey>();
|
|
||||||
ArrayList<PGPPublicKey> publicKeys = new ArrayList<PGPPublicKey>();
|
|
||||||
|
|
||||||
// 3. Copy subkeys
|
PGPPublicKey modifiedPublicKey = masterPublicKey;
|
||||||
// - Generate revocation if requested
|
|
||||||
// - Copy old cert, or generate new if change requested
|
// 2a. Add certificates for new user ids
|
||||||
for (PGPSecretKey sKey : new IterableIterator<PGPSecretKey>(sKR.getSecretKeys())) {
|
for (String userId : saveParcel.addUserIds) {
|
||||||
PGPPublicKey pKey = sKey.getPublicKey();
|
PGPSignature cert = generateUserIdSignature(masterPrivateKey,
|
||||||
if (Arrays.binarySearch(saveParcel.revokeSubKeys, sKey.getKeyID()) >= 0) {
|
masterPublicKey, userId, false);
|
||||||
// add revocation signature to key, if there is none yet
|
modifiedPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, cert);
|
||||||
if (!pKey.getSignaturesOfType(PGPSignature.SUBKEY_REVOCATION).hasNext()) {
|
|
||||||
// generate revocation signature
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (saveParcel.changeSubKeys.containsKey(sKey.getKeyID())) {
|
|
||||||
// change subkey flags?
|
|
||||||
SaveKeyringParcel.SubkeyChange change = saveParcel.changeSubKeys.get(sKey.getKeyID());
|
|
||||||
// remove old subkey binding signature(s?)
|
|
||||||
for (PGPSignature sig : new IterableIterator<PGPSignature>(
|
|
||||||
pKey.getSignaturesOfType(PGPSignature.SUBKEY_BINDING))) {
|
|
||||||
pKey = PGPPublicKey.removeCertification(pKey, sig);
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate and add new signature
|
// 2b. Add revocations for revoked user ids
|
||||||
PGPSignature sig = generateSubkeyBindingSignature(masterPublicKey, masterPrivateKey,
|
for (String userId : saveParcel.revokeUserIds) {
|
||||||
sKey, pKey, change.mFlags, change.mExpiry, passphrase);
|
|
||||||
pKey = PGPPublicKey.addCertification(pKey, sig);
|
|
||||||
}
|
|
||||||
secretKeys.add(PGPSecretKey.replacePublicKey(sKey, pKey));
|
|
||||||
publicKeys.add(pKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Generate and add new subkeys
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
// 5. Copy user ids
|
|
||||||
for (String userId : new IterableIterator<String>(masterPublicKey.getUserIDs())) {
|
|
||||||
// - Copy old cert, or generate new if primary user id status changed
|
|
||||||
boolean certified = false, revoked = false;
|
|
||||||
for (PGPSignature sig : new IterableIterator<PGPSignature>(
|
|
||||||
masterPublicKey.getSignaturesForID(userId))) {
|
|
||||||
// We know there are only revocation and certification types in here.
|
|
||||||
switch(sig.getSignatureType()) {
|
|
||||||
case PGPSignature.CERTIFICATION_REVOCATION:
|
|
||||||
revoked = true;
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case PGPSignature.DEFAULT_CERTIFICATION:
|
|
||||||
case PGPSignature.NO_CERTIFICATION:
|
|
||||||
case PGPSignature.CASUAL_CERTIFICATION:
|
|
||||||
case PGPSignature.POSITIVE_CERTIFICATION:
|
|
||||||
// Already got one? Remove this one, then.
|
|
||||||
if (certified) {
|
|
||||||
masterPublicKey = PGPPublicKey.removeCertification(
|
|
||||||
masterPublicKey, userId, sig);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
boolean primary = userId.equals(saveParcel.changePrimaryUserId);
|
|
||||||
// Generate a new one under certain circumstances
|
|
||||||
if (saveParcel.changePrimaryUserId != null &&
|
|
||||||
sig.getHashedSubPackets().isPrimaryUserID() != primary) {
|
|
||||||
PGPSignature cert = generateUserIdSignature(
|
|
||||||
masterPrivateKey, masterPublicKey, userId, primary);
|
|
||||||
PGPPublicKey.addCertification(masterPublicKey, userId, cert);
|
|
||||||
}
|
|
||||||
certified = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// - Generate revocation if requested
|
|
||||||
if (!revoked && Arrays.binarySearch(saveParcel.revokeUserIds, userId) >= 0) {
|
|
||||||
PGPSignature cert = generateRevocationSignature(masterPrivateKey,
|
PGPSignature cert = generateRevocationSignature(masterPrivateKey,
|
||||||
masterPublicKey, userId);
|
masterPublicKey, userId);
|
||||||
masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, cert);
|
modifiedPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, cert);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. If primary user id changed, generate new certificates for both old and new
|
||||||
|
if (saveParcel.changePrimaryUserId != null) {
|
||||||
|
// todo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the secret key ring
|
||||||
|
if (modifiedPublicKey != masterPublicKey) {
|
||||||
|
masterSecretKey = PGPSecretKey.replacePublicKey(masterSecretKey, modifiedPublicKey);
|
||||||
|
masterPublicKey = modifiedPublicKey;
|
||||||
|
sKR = PGPSecretKeyRing.insertSecretKey(sKR, masterSecretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 4a. For each subkey change, generate new subkey binding certificate
|
||||||
|
for (SaveKeyringParcel.SubkeyChange change : saveParcel.changeSubKeys) {
|
||||||
|
PGPSecretKey sKey = sKR.getSecretKey(change.mKeyId);
|
||||||
|
if (sKey == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
PGPPublicKey pKey = sKey.getPublicKey();
|
||||||
|
|
||||||
|
// generate and add new signature
|
||||||
|
PGPSignature sig = generateSubkeyBindingSignature(masterPublicKey, masterPrivateKey,
|
||||||
|
sKey, pKey, change.mFlags, change.mExpiry, passphrase);
|
||||||
|
pKey = PGPPublicKey.addCertification(pKey, sig);
|
||||||
|
sKR = PGPSecretKeyRing.insertSecretKey(sKR, PGPSecretKey.replacePublicKey(sKey, pKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4b. For each subkey change, generate new subkey binding certificate
|
||||||
|
for (long revocation : saveParcel.revokeSubKeys) {
|
||||||
|
PGPSecretKey sKey = sKR.getSecretKey(revocation);
|
||||||
|
if (sKey == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
PGPPublicKey pKey = sKey.getPublicKey();
|
||||||
|
|
||||||
|
// generate and add new signature
|
||||||
|
PGPSignature sig = generateRevocationSignature(masterPublicKey, masterPrivateKey, pKey);
|
||||||
|
|
||||||
|
pKey = PGPPublicKey.addCertification(pKey, sig);
|
||||||
|
sKR = PGPSecretKeyRing.insertSecretKey(sKR, PGPSecretKey.replacePublicKey(sKey, pKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Generate and add new subkeys
|
||||||
|
for (SaveKeyringParcel.SubkeyAdd add : saveParcel.addSubKeys) {
|
||||||
|
try {
|
||||||
|
PGPSecretKey sKey = createKey(add.mAlgorithm, add.mKeysize, passphrase, false);
|
||||||
|
PGPPublicKey pKey = sKey.getPublicKey();
|
||||||
|
PGPSignature cert = generateSubkeyBindingSignature(masterPublicKey, masterPrivateKey,
|
||||||
|
sKey, pKey, add.mFlags, add.mExpiry, passphrase);
|
||||||
|
|
||||||
|
pKey = PGPPublicKey.addCertification(pKey, cert);
|
||||||
|
sKey = PGPSecretKey.replacePublicKey(sKey, pKey);
|
||||||
|
sKR = PGPSecretKeyRing.insertSecretKey(sKR, PGPSecretKey.replacePublicKey(sKey, pKey));
|
||||||
|
} catch (PgpGeneralMsgIdException e) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Add new user ids
|
// 6. If requested, change passphrase
|
||||||
for(String userId : saveParcel.addUserIds) {
|
|
||||||
PGPSignature cert = generateUserIdSignature(masterPrivateKey,
|
|
||||||
masterPublicKey, userId, userId.equals(saveParcel.changePrimaryUserId));
|
|
||||||
masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, cert);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Generate PublicKeyRing from SecretKeyRing
|
|
||||||
updateProgress(R.string.progress_building_master_key, 30, 100);
|
|
||||||
PGPSecretKeyRing ring = new PGPSecretKeyRing(secretKeys);
|
|
||||||
|
|
||||||
// Copy all non-self uid certificates
|
|
||||||
for (String userId : new IterableIterator<String>(masterPublicKey.getUserIDs())) {
|
|
||||||
// - Copy old cert, or generate new if primary user id status changed
|
|
||||||
boolean certified = false, revoked = false;
|
|
||||||
for (PGPSignature sig : new IterableIterator<PGPSignature>(
|
|
||||||
masterPublicKey.getSignaturesForID(userId))) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (PGPPublicKey newKey : publicKeys) {
|
|
||||||
PGPPublicKey oldKey = pKR.getPublicKey(newKey.getKeyID());
|
|
||||||
for (PGPSignature sig : new IterableIterator<PGPSignature>(
|
|
||||||
oldKey.getSignatures())) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If requested, set new passphrase
|
|
||||||
if (saveParcel.newPassphrase != null) {
|
if (saveParcel.newPassphrase != null) {
|
||||||
PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build()
|
PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build()
|
||||||
.get(HashAlgorithmTags.SHA1);
|
.get(HashAlgorithmTags.SHA1);
|
||||||
@ -352,9 +318,7 @@ public class PgpKeyOperation {
|
|||||||
sKR = PGPSecretKeyRing.copyWithNewPassword(sKR, keyDecryptor, keyEncryptorNew);
|
sKR = PGPSecretKeyRing.copyWithNewPassword(sKR, keyDecryptor, keyEncryptorNew);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Return pair (PublicKeyRing,SecretKeyRing)
|
return new UncachedKeyRing(sKR);
|
||||||
|
|
||||||
return new Pair<PGPSecretKeyRing, PGPPublicKeyRing>(sKR, pKR);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -390,10 +354,29 @@ public class PgpKeyOperation {
|
|||||||
return sGen.generateCertification(userId, pKey);
|
return sGen.generateCertification(userId, pKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static PGPSignature generateRevocationSignature(
|
||||||
|
PGPPublicKey masterPublicKey, PGPPrivateKey masterPrivateKey, PGPPublicKey pKey)
|
||||||
|
throws IOException, PGPException, SignatureException {
|
||||||
|
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
|
||||||
|
pKey.getAlgorithm(), PGPUtil.SHA1)
|
||||||
|
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
|
||||||
|
PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
|
||||||
|
PGPSignatureSubpacketGenerator subHashedPacketsGen = new PGPSignatureSubpacketGenerator();
|
||||||
|
subHashedPacketsGen.setSignatureCreationTime(false, new Date());
|
||||||
|
sGen.setHashedSubpackets(subHashedPacketsGen.generate());
|
||||||
|
// Generate key revocation or subkey revocation, depending on master/subkey-ness
|
||||||
|
if (masterPublicKey.getKeyID() == pKey.getKeyID()) {
|
||||||
|
sGen.init(PGPSignature.KEY_REVOCATION, masterPrivateKey);
|
||||||
|
return sGen.generateCertification(masterPublicKey);
|
||||||
|
} else {
|
||||||
|
sGen.init(PGPSignature.SUBKEY_REVOCATION, masterPrivateKey);
|
||||||
|
return sGen.generateCertification(masterPublicKey, pKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static PGPSignature generateSubkeyBindingSignature(
|
private static PGPSignature generateSubkeyBindingSignature(
|
||||||
PGPPublicKey masterPublicKey, PGPPrivateKey masterPrivateKey,
|
PGPPublicKey masterPublicKey, PGPPrivateKey masterPrivateKey,
|
||||||
PGPSecretKey sKey, PGPPublicKey pKey,
|
PGPSecretKey sKey, PGPPublicKey pKey, int flags, Long expiry, String passphrase)
|
||||||
int flags, Long expiry, String passphrase)
|
|
||||||
throws PgpGeneralMsgIdException, IOException, PGPException, SignatureException {
|
throws PgpGeneralMsgIdException, IOException, PGPException, SignatureException {
|
||||||
|
|
||||||
// date for signing
|
// date for signing
|
||||||
@ -510,19 +493,4 @@ public class PgpKeyOperation {
|
|||||||
return publicKey;
|
return publicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple static subclass that stores two values.
|
|
||||||
* <p/>
|
|
||||||
* This is only used to return a pair of values in one function above. We specifically don't use
|
|
||||||
* com.android.Pair to keep this class free from android dependencies.
|
|
||||||
*/
|
|
||||||
public static class Pair<K, V> {
|
|
||||||
public final K first;
|
|
||||||
public final V second;
|
|
||||||
|
|
||||||
public Pair(K first, V second) {
|
|
||||||
this.first = first;
|
|
||||||
this.second = second;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,11 @@ public class PgpGeneralMsgIdException extends Exception {
|
|||||||
mMessageId = messageId;
|
mMessageId = messageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PgpGeneralMsgIdException(int messageId, Throwable cause) {
|
||||||
|
super("msg[" + messageId + "]", cause);
|
||||||
|
mMessageId = messageId;
|
||||||
|
}
|
||||||
|
|
||||||
public PgpGeneralException getContextualized(Context context) {
|
public PgpGeneralException getContextualized(Context context) {
|
||||||
return new PgpGeneralException(context.getString(mMessageId), this);
|
return new PgpGeneralException(context.getString(mMessageId), this);
|
||||||
}
|
}
|
||||||
|
@ -25,14 +25,14 @@ public class SaveKeyringParcel implements Parcelable {
|
|||||||
// the master key id to be edited
|
// the master key id to be edited
|
||||||
public final long mMasterKeyId;
|
public final long mMasterKeyId;
|
||||||
// the key fingerprint, for safety
|
// the key fingerprint, for safety
|
||||||
private final byte[] mFingerprint;
|
public final byte[] mFingerprint;
|
||||||
|
|
||||||
public String newPassphrase;
|
public String newPassphrase;
|
||||||
|
|
||||||
public String[] addUserIds;
|
public String[] addUserIds;
|
||||||
public SubkeyAdd[] addSubKeys;
|
public SubkeyAdd[] addSubKeys;
|
||||||
|
|
||||||
public HashMap<Long, SubkeyChange> changeSubKeys;
|
public SubkeyChange[] changeSubKeys;
|
||||||
public String changePrimaryUserId;
|
public String changePrimaryUserId;
|
||||||
|
|
||||||
public String[] revokeUserIds;
|
public String[] revokeUserIds;
|
||||||
@ -76,7 +76,7 @@ public class SaveKeyringParcel implements Parcelable {
|
|||||||
addUserIds = source.createStringArray();
|
addUserIds = source.createStringArray();
|
||||||
addSubKeys = (SubkeyAdd[]) source.readSerializable();
|
addSubKeys = (SubkeyAdd[]) source.readSerializable();
|
||||||
|
|
||||||
changeSubKeys = (HashMap<Long,SubkeyChange>) source.readSerializable();
|
changeSubKeys = (SubkeyChange[]) source.readSerializable();
|
||||||
changePrimaryUserId = source.readString();
|
changePrimaryUserId = source.readString();
|
||||||
|
|
||||||
revokeUserIds = source.createStringArray();
|
revokeUserIds = source.createStringArray();
|
||||||
|
Loading…
Reference in New Issue
Block a user