mirror of
https://github.com/topjohnwu/Magisk.git
synced 2024-12-25 22:07:38 +00:00
Generate keys for signing hidden Magisk Manager
This commit is contained in:
parent
a02493fbaa
commit
325d9a0b86
@ -32,8 +32,9 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
const val ROOT_ACCESS = "root_access"
|
const val ROOT_ACCESS = "root_access"
|
||||||
const val SU_MULTIUSER_MODE = "multiuser_mode"
|
const val SU_MULTIUSER_MODE = "multiuser_mode"
|
||||||
const val SU_MNT_NS = "mnt_ns"
|
const val SU_MNT_NS = "mnt_ns"
|
||||||
const val SU_MANAGER = "requester"
|
|
||||||
const val SU_FINGERPRINT = "su_fingerprint"
|
const val SU_FINGERPRINT = "su_fingerprint"
|
||||||
|
const val SU_MANAGER = "requester"
|
||||||
|
const val KEYSTORE = "keystore"
|
||||||
|
|
||||||
// prefs
|
// prefs
|
||||||
const val SU_REQUEST_TIMEOUT = "su_request_timeout"
|
const val SU_REQUEST_TIMEOUT = "su_request_timeout"
|
||||||
@ -123,6 +124,7 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||||
var suFingerprint by dbSettings(Key.SU_FINGERPRINT, false)
|
var suFingerprint by dbSettings(Key.SU_FINGERPRINT, false)
|
||||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||||
|
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||||
|
|
||||||
// Always return a path in external storage where we can write
|
// Always return a path in external storage where we can write
|
||||||
val downloadDirectory get() =
|
val downloadDirectory get() =
|
||||||
@ -205,4 +207,4 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
|
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
133
app/src/main/java/com/topjohnwu/magisk/utils/Keygen.kt
Normal file
133
app/src/main/java/com/topjohnwu/magisk/utils/Keygen.kt
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package com.topjohnwu.magisk.utils
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Base64OutputStream
|
||||||
|
import com.topjohnwu.magisk.Config
|
||||||
|
import com.topjohnwu.magisk.di.koinModules
|
||||||
|
import com.topjohnwu.signing.CryptoUtils.readCertificate
|
||||||
|
import com.topjohnwu.signing.CryptoUtils.readPrivateKey
|
||||||
|
import com.topjohnwu.superuser.internal.InternalUtils
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||||
|
import org.koin.core.context.GlobalContext
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import java.util.*
|
||||||
|
import java.util.zip.GZIPInputStream
|
||||||
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
|
private interface CertKeyProvider {
|
||||||
|
val cert: X509Certificate
|
||||||
|
val key: PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
object Keygen: CertKeyProvider {
|
||||||
|
private const val ALIAS = "magisk"
|
||||||
|
private val PASSWORD = "magisk".toCharArray()
|
||||||
|
private const val TESTKEY_CERT = "61ed377e85d386a8dfee6b864bd85b0bfaa5af81"
|
||||||
|
|
||||||
|
private val start get() = Calendar.getInstance()
|
||||||
|
private val end get() = Calendar.getInstance().apply {
|
||||||
|
add(Calendar.YEAR, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val cert get() = provider.cert
|
||||||
|
override val key get() = provider.key
|
||||||
|
|
||||||
|
private val provider: CertKeyProvider
|
||||||
|
|
||||||
|
class KeyStoreProvider : CertKeyProvider {
|
||||||
|
private val ks by lazy { init() }
|
||||||
|
override val cert by lazy { ks.getCertificate(ALIAS) as X509Certificate }
|
||||||
|
override val key by lazy { ks.getKey(ALIAS, PASSWORD) as PrivateKey }
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestProvider : CertKeyProvider {
|
||||||
|
override val cert by lazy {
|
||||||
|
readCertificate(javaClass.getResourceAsStream("/keys/testkey.x509.pem"))
|
||||||
|
}
|
||||||
|
override val key by lazy {
|
||||||
|
readPrivateKey(javaClass.getResourceAsStream("/keys/testkey.pk8"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
// This object could possibly be accessed from an external app
|
||||||
|
// Get context from reflection into Android's framework
|
||||||
|
val context = InternalUtils.getContext()
|
||||||
|
val pm = context.packageManager
|
||||||
|
val info = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
|
||||||
|
val sig = info.signatures[0]
|
||||||
|
val digest = MessageDigest.getInstance("SHA1")
|
||||||
|
val chksum = digest.digest(sig.toByteArray())
|
||||||
|
|
||||||
|
val sb = StringBuilder()
|
||||||
|
for (b in chksum) {
|
||||||
|
sb.append("%02x".format(0xFF and b.toInt()))
|
||||||
|
}
|
||||||
|
|
||||||
|
provider = if (sb.toString() == TESTKEY_CERT) {
|
||||||
|
// The app was signed by the test key, continue to use it (legacy mode)
|
||||||
|
TestProvider()
|
||||||
|
} else {
|
||||||
|
KeyStoreProvider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun init(): KeyStore {
|
||||||
|
GlobalContext.getOrNull() ?: {
|
||||||
|
// Invoked externally, do some basic initialization
|
||||||
|
startKoin {
|
||||||
|
modules(koinModules)
|
||||||
|
}
|
||||||
|
Timber.plant(Timber.DebugTree())
|
||||||
|
}()
|
||||||
|
|
||||||
|
val raw = Config.keyStoreRaw
|
||||||
|
val ks = KeyStore.getInstance("PKCS12")
|
||||||
|
if (raw.isEmpty()) {
|
||||||
|
ks.load(null)
|
||||||
|
} else {
|
||||||
|
GZIPInputStream(ByteArrayInputStream(
|
||||||
|
Base64.decode(raw, Base64.NO_PADDING or Base64.NO_WRAP)
|
||||||
|
)).use {
|
||||||
|
ks.load(it, PASSWORD)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys already exist
|
||||||
|
if (ks.containsAlias(ALIAS))
|
||||||
|
return ks
|
||||||
|
|
||||||
|
// Generate new private key and certificate
|
||||||
|
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(2048) }.genKeyPair()
|
||||||
|
val dn = X500Name("CN=Magisk")
|
||||||
|
val builder = JcaX509v3CertificateBuilder(dn,
|
||||||
|
BigInteger.valueOf(start.timeInMillis), start.time, end.time, dn, kp.public)
|
||||||
|
val signer = JcaContentSignerBuilder("SHA256WithRSA").build(kp.private)
|
||||||
|
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||||
|
|
||||||
|
// Store them into keystore
|
||||||
|
ks.setKeyEntry(ALIAS, kp.private, PASSWORD, arrayOf(cert))
|
||||||
|
val bytes = ByteArrayOutputStream()
|
||||||
|
GZIPOutputStream(Base64OutputStream(bytes, Base64.NO_PADDING or Base64.NO_WRAP)).use {
|
||||||
|
ks.store(it, PASSWORD)
|
||||||
|
}
|
||||||
|
Config.keyStoreRaw = bytes.toString()
|
||||||
|
|
||||||
|
return ks
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -107,7 +107,7 @@ object PatchAPK {
|
|||||||
|
|
||||||
// Write apk changes
|
// Write apk changes
|
||||||
jar.getOutputStream(je).write(xml)
|
jar.getOutputStream(je).write(xml)
|
||||||
SignAPK.sign(jar, FileOutputStream(out).buffered())
|
SignAPK.sign(Keygen.cert, Keygen.key, jar, FileOutputStream(out).buffered())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
|
@ -24,7 +24,7 @@ import java.security.spec.PKCS8EncodedKeySpec;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
class CryptoUtils {
|
public class CryptoUtils {
|
||||||
|
|
||||||
static final Map<String, String> ID_TO_ALG;
|
static final Map<String, String> ID_TO_ALG;
|
||||||
static final Map<String, String> ALG_TO_ID;
|
static final Map<String, String> ALG_TO_ID;
|
||||||
@ -81,7 +81,7 @@ class CryptoUtils {
|
|||||||
return new AlgorithmIdentifier(new ASN1ObjectIdentifier(id));
|
return new AlgorithmIdentifier(new ASN1ObjectIdentifier(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
static X509Certificate readCertificate(InputStream input)
|
public static X509Certificate readCertificate(InputStream input)
|
||||||
throws IOException, GeneralSecurityException {
|
throws IOException, GeneralSecurityException {
|
||||||
try {
|
try {
|
||||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
@ -92,7 +92,7 @@ class CryptoUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Read a PKCS#8 format private key. */
|
/** Read a PKCS#8 format private key. */
|
||||||
static PrivateKey readPrivateKey(InputStream input)
|
public static PrivateKey readPrivateKey(InputStream input)
|
||||||
throws IOException, GeneralSecurityException {
|
throws IOException, GeneralSecurityException {
|
||||||
try {
|
try {
|
||||||
ByteArrayStream buf = new ByteArrayStream();
|
ByteArrayStream buf = new ByteArrayStream();
|
||||||
|
@ -30,7 +30,6 @@ import java.io.PrintStream;
|
|||||||
import java.io.RandomAccessFile;
|
import java.io.RandomAccessFile;
|
||||||
import java.security.DigestOutputStream;
|
import java.security.DigestOutputStream;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
@ -61,29 +60,8 @@ public class SignAPK {
|
|||||||
private static final int USE_SHA1 = 1;
|
private static final int USE_SHA1 = 1;
|
||||||
private static final int USE_SHA256 = 2;
|
private static final int USE_SHA256 = 2;
|
||||||
|
|
||||||
public static void sign(JarMap input, OutputStream output) throws Exception {
|
public static void signAndAdjust(X509Certificate cert, PrivateKey key,
|
||||||
sign(SignAPK.class.getResourceAsStream("/keys/testkey.x509.pem"),
|
JarMap input, OutputStream output) throws Exception {
|
||||||
SignAPK.class.getResourceAsStream("/keys/testkey.pk8"), input, output);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void sign(InputStream certIs, InputStream keyIs,
|
|
||||||
JarMap input, OutputStream output) throws Exception {
|
|
||||||
X509Certificate cert = CryptoUtils.readCertificate(certIs);
|
|
||||||
PrivateKey key = CryptoUtils.readPrivateKey(keyIs);
|
|
||||||
sign(cert, key, input, output);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void sign(InputStream jks, String keyStorePass, String alias, String keyPass,
|
|
||||||
JarMap input, OutputStream output) throws Exception {
|
|
||||||
KeyStore ks = KeyStore.getInstance("JKS");
|
|
||||||
ks.load(jks, keyStorePass.toCharArray());
|
|
||||||
X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
|
|
||||||
PrivateKey key = (PrivateKey) ks.getKey(alias, keyPass.toCharArray());
|
|
||||||
sign(cert, key, input, output);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sign(X509Certificate cert, PrivateKey key,
|
|
||||||
JarMap input, OutputStream output) throws Exception {
|
|
||||||
File temp1 = File.createTempFile("signAPK", null);
|
File temp1 = File.createTempFile("signAPK", null);
|
||||||
File temp2 = File.createTempFile("signAPK", null);
|
File temp2 = File.createTempFile("signAPK", null);
|
||||||
|
|
||||||
@ -103,6 +81,11 @@ public class SignAPK {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void sign(X509Certificate cert, PrivateKey key,
|
||||||
|
JarMap input, OutputStream output) throws Exception {
|
||||||
|
sign(cert, key, input, output, false);
|
||||||
|
}
|
||||||
|
|
||||||
private static void sign(X509Certificate cert, PrivateKey key,
|
private static void sign(X509Certificate cert, PrivateKey key,
|
||||||
JarMap input, OutputStream output, boolean minSign) throws Exception {
|
JarMap input, OutputStream output, boolean minSign) throws Exception {
|
||||||
int hashes = 0;
|
int hashes = 0;
|
||||||
@ -498,11 +481,11 @@ public class SignAPK {
|
|||||||
outputStream.close();
|
outputStream.close();
|
||||||
}
|
}
|
||||||
private static void signFile(Manifest manifest, JarMap inputJar,
|
private static void signFile(Manifest manifest, JarMap inputJar,
|
||||||
X509Certificate publicKey, PrivateKey privateKey,
|
X509Certificate cert, PrivateKey privateKey,
|
||||||
JarOutputStream outputJar)
|
JarOutputStream outputJar)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
// Assume the certificate is valid for at least an hour.
|
// Assume the certificate is valid for at least an hour.
|
||||||
long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
|
long timestamp = cert.getNotBefore().getTime() + 3600L * 1000;
|
||||||
// MANIFEST.MF
|
// MANIFEST.MF
|
||||||
JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
|
JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
|
||||||
je.setTime(timestamp);
|
je.setTime(timestamp);
|
||||||
@ -512,15 +495,15 @@ public class SignAPK {
|
|||||||
je.setTime(timestamp);
|
je.setTime(timestamp);
|
||||||
outputJar.putNextEntry(je);
|
outputJar.putNextEntry(je);
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey));
|
writeSignatureFile(manifest, baos, getDigestAlgorithm(cert));
|
||||||
byte[] signedData = baos.toByteArray();
|
byte[] signedData = baos.toByteArray();
|
||||||
outputJar.write(signedData);
|
outputJar.write(signedData);
|
||||||
// CERT.{EC,RSA} / CERT#.{EC,RSA}
|
// CERT.{EC,RSA} / CERT#.{EC,RSA}
|
||||||
final String keyType = publicKey.getPublicKey().getAlgorithm();
|
final String keyType = cert.getPublicKey().getAlgorithm();
|
||||||
je = new JarEntry(String.format(CERT_SIG_NAME, keyType));
|
je = new JarEntry(String.format(CERT_SIG_NAME, keyType));
|
||||||
je.setTime(timestamp);
|
je.setTime(timestamp);
|
||||||
outputJar.putNextEntry(je);
|
outputJar.putNextEntry(je);
|
||||||
writeSignatureBlock(new CMSProcessableByteArray(signedData),
|
writeSignatureBlock(new CMSProcessableByteArray(signedData),
|
||||||
publicKey, privateKey, outputJar);
|
cert, privateKey, outputJar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,23 +4,61 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|||||||
|
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
public class ZipSigner {
|
public class ZipSigner {
|
||||||
|
|
||||||
public static void usage() {
|
private static void usage() {
|
||||||
System.err.println("ZipSigner usage:");
|
System.err.println("ZipSigner usage:");
|
||||||
System.err.println(" zipsigner.jar input.jar output.jar");
|
System.err.println(" zipsigner.jar input.jar output.jar");
|
||||||
System.err.println(" sign jar with AOSP test keys");
|
System.err.println(" sign jar with AOSP test keys");
|
||||||
System.err.println(" zipsigner.jar x509.pem pk8 input.jar output.jar");
|
System.err.println(" zipsigner.jar x509.pem pk8 input.jar output.jar");
|
||||||
System.err.println(" sign jar with certificate / private key pair");
|
System.err.println(" sign jar with certificate / private key pair");
|
||||||
System.err.println(" zipsigner.jar jks keyStorePass keyAlias keyPass input.jar output.jar");
|
System.err.println(" zipsigner.jar keyStore keyStorePass alias keyPass input.jar output.jar");
|
||||||
System.err.println(" sign jar with Java KeyStore");
|
System.err.println(" sign jar with Java KeyStore");
|
||||||
System.exit(2);
|
System.exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void sign(JarMap input, OutputStream output) throws Exception {
|
||||||
|
sign(SignAPK.class.getResourceAsStream("/keys/testkey.x509.pem"),
|
||||||
|
SignAPK.class.getResourceAsStream("/keys/testkey.pk8"), input, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sign(InputStream certIs, InputStream keyIs,
|
||||||
|
JarMap input, OutputStream output) throws Exception {
|
||||||
|
X509Certificate cert = CryptoUtils.readCertificate(certIs);
|
||||||
|
PrivateKey key = CryptoUtils.readPrivateKey(keyIs);
|
||||||
|
SignAPK.signAndAdjust(cert, key, input, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sign(String keyStore, String keyStorePass, String alias, String keyPass,
|
||||||
|
JarMap in, OutputStream out) throws Exception {
|
||||||
|
KeyStore ks;
|
||||||
|
try {
|
||||||
|
ks = KeyStore.getInstance("JKS");
|
||||||
|
try (InputStream is = new FileInputStream(keyStore)) {
|
||||||
|
ks.load(is, keyStorePass.toCharArray());
|
||||||
|
}
|
||||||
|
} catch (KeyStoreException|IOException|CertificateException|NoSuchAlgorithmException e) {
|
||||||
|
ks = KeyStore.getInstance("PKCS12");
|
||||||
|
try (InputStream is = new FileInputStream(keyStore)) {
|
||||||
|
ks.load(is, keyStorePass.toCharArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
|
||||||
|
PrivateKey key = (PrivateKey) ks.getKey(alias, keyPass.toCharArray());
|
||||||
|
SignAPK.signAndAdjust(cert, key, in, out);
|
||||||
|
}
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
if (args.length != 2 && args.length != 4 && args.length != 6)
|
if (args.length != 2 && args.length != 4 && args.length != 6)
|
||||||
usage();
|
usage();
|
||||||
@ -30,16 +68,14 @@ public class ZipSigner {
|
|||||||
try (JarMap in = new JarMap(args[args.length - 2], false);
|
try (JarMap in = new JarMap(args[args.length - 2], false);
|
||||||
OutputStream out = new FileOutputStream(args[args.length - 1])) {
|
OutputStream out = new FileOutputStream(args[args.length - 1])) {
|
||||||
if (args.length == 2) {
|
if (args.length == 2) {
|
||||||
SignAPK.sign(in, out);
|
sign(in, out);
|
||||||
} else if (args.length == 4) {
|
} else if (args.length == 4) {
|
||||||
try (InputStream cert = new FileInputStream(args[0]);
|
try (InputStream cert = new FileInputStream(args[0]);
|
||||||
InputStream key = new FileInputStream(args[1])) {
|
InputStream key = new FileInputStream(args[1])) {
|
||||||
SignAPK.sign(cert, key, in, out);
|
sign(cert, key, in, out);
|
||||||
}
|
}
|
||||||
} else if (args.length == 6) {
|
} else if (args.length == 6) {
|
||||||
try (InputStream jks = new FileInputStream(args[0])) {
|
sign(args[0], args[1], args[2], args[3], in, out);
|
||||||
SignAPK.sign(jks, args[1], args[2], args[3], in, out);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user