mirror of
https://github.com/RfidResearchGroup/proxmark3.git
synced 2024-12-29 11:52:59 +08:00
renamed gdm getblk to config since that is what it does
This commit is contained in:
parent
932e345c43
commit
97e16ba764
9 changed files with 88 additions and 97 deletions
|
@ -1735,14 +1735,12 @@ static void PacketReceived(PacketCommandNG *packet) {
|
|||
MifareG4WriteBlk(payload->blockno, payload->pwd, payload->data, payload->workFlags);
|
||||
break;
|
||||
}
|
||||
case CMD_HF_MIFARE_G4_GDM_RDBL: {
|
||||
case CMD_HF_MIFARE_G4_GDM_CONFIG: {
|
||||
struct p {
|
||||
uint8_t blockno;
|
||||
uint8_t keytype;
|
||||
uint8_t key[6];
|
||||
} PACKED;
|
||||
struct p *payload = (struct p *) packet->data.asBytes;
|
||||
MifareReadBlockGDM(payload->blockno, payload->keytype, payload->key);
|
||||
MifareReadConfigBlockGDM(payload->key);
|
||||
break;
|
||||
}
|
||||
case CMD_HF_MIFARE_G4_GDM_WRBL: {
|
||||
|
|
|
@ -214,7 +214,7 @@ void MifareUReadBlock(uint8_t arg0, uint8_t arg1, uint8_t *datain) {
|
|||
LEDsoff();
|
||||
}
|
||||
|
||||
void MifareReadBlockGDM(uint8_t blockno, uint8_t keytype, uint8_t *key) {
|
||||
void MifareReadConfigBlockGDM(uint8_t *key) {
|
||||
|
||||
int retval = PM3_SUCCESS;
|
||||
|
||||
|
@ -248,12 +248,12 @@ void MifareReadBlockGDM(uint8_t blockno, uint8_t keytype, uint8_t *key) {
|
|||
goto OUT;
|
||||
}
|
||||
|
||||
if (mifare_classic_authex_2(pcs, cuid, blockno, keytype, ui64key, AUTH_FIRST, NULL, NULL, true)) {
|
||||
if (mifare_classic_authex_2(pcs, cuid, 0, 0, ui64key, AUTH_FIRST, NULL, NULL, true)) {
|
||||
retval = PM3_ESOFT;
|
||||
goto OUT;
|
||||
};
|
||||
|
||||
if (mifare_classic_readblock_ex(pcs, cuid, blockno, outbuf, true)) {
|
||||
if (mifare_classic_readblock_ex(pcs, cuid, 0, outbuf, MIFARE_MAGIC_GDM_READBLOCK)) {
|
||||
retval = PM3_ESOFT;
|
||||
goto OUT;
|
||||
};
|
||||
|
@ -266,7 +266,7 @@ void MifareReadBlockGDM(uint8_t blockno, uint8_t keytype, uint8_t *key) {
|
|||
OUT:
|
||||
crypto1_deinit(pcs);
|
||||
|
||||
reply_ng(CMD_HF_MIFARE_G4_GDM_RDBL, retval, outbuf, sizeof(outbuf));
|
||||
reply_ng(CMD_HF_MIFARE_G4_GDM_CONFIG, retval, outbuf, sizeof(outbuf));
|
||||
FpgaWriteConfWord(FPGA_MAJOR_MODE_OFF);
|
||||
LEDsoff();
|
||||
set_tracing(false);
|
||||
|
|
|
@ -59,7 +59,7 @@ void MifareGen3Blk(uint8_t block_len, uint8_t *block); // Gen 3 magic card overw
|
|||
void MifareGen3Freez(void); // Gen 3 magic card lock further UID changes
|
||||
|
||||
// MFC GEN4 GDM
|
||||
void MifareReadBlockGDM(uint8_t blockno, uint8_t keytype, uint8_t *key);
|
||||
void MifareReadConfigBlockGDM(uint8_t *key);
|
||||
void MifareWriteBlockGDM(uint8_t blockno, uint8_t keytype, uint8_t *key, uint8_t *datain);
|
||||
|
||||
// MFC GEN4 GTU
|
||||
|
|
|
@ -153,7 +153,7 @@ int mifare_classic_authex_2(struct Crypto1State *pcs, uint32_t uid, uint8_t bloc
|
|||
num_to_bytes(prng_successor(GetTickCount(), 32), 4, nr);
|
||||
|
||||
// Transmit MIFARE_CLASSIC_AUTH 0x60, 0x61 or GDM 0x80
|
||||
uint8_t cmdbyte = (is_gdm) ? MIFARE_MAGIC_GDM_AUTH_KEYA : MIFARE_AUTH_KEYA + (keyType & 0x01);
|
||||
uint8_t cmdbyte = (is_gdm) ? MIFARE_MAGIC_GDM_AUTH_KEYA + (keyType & 0x01) : MIFARE_AUTH_KEYA + (keyType & 0x01);
|
||||
len = mifare_sendcmd_short(pcs, isNested, cmdbyte, blockNo, receivedAnswer, receivedAnswerPar, timing);
|
||||
if (len != 4) return 1;
|
||||
|
||||
|
@ -230,20 +230,14 @@ int mifare_classic_authex_2(struct Crypto1State *pcs, uint32_t uid, uint8_t bloc
|
|||
}
|
||||
|
||||
int mifare_classic_readblock(struct Crypto1State *pcs, uint32_t uid, uint8_t blockNo, uint8_t *blockData) {
|
||||
return mifare_classic_readblock_ex(pcs, uid, blockNo, blockData, false);
|
||||
return mifare_classic_readblock_ex(pcs, uid, blockNo, blockData, ISO14443A_CMD_READBLOCK);
|
||||
}
|
||||
int mifare_classic_readblock_ex(struct Crypto1State *pcs, uint32_t uid, uint8_t blockNo, uint8_t *blockData, bool is_gdm) {
|
||||
int mifare_classic_readblock_ex(struct Crypto1State *pcs, uint32_t uid, uint8_t blockNo, uint8_t *blockData, uint8_t iso_byte) {
|
||||
|
||||
uint8_t receivedAnswer[MAX_MIFARE_FRAME_SIZE] = {0x00};
|
||||
uint8_t receivedAnswerPar[MAX_MIFARE_PARITY_SIZE] = {0x00};
|
||||
|
||||
uint16_t len;
|
||||
if (is_gdm) {
|
||||
len = mifare_sendcmd_short(pcs, 1, MIFARE_MAGIC_GDM_READBLOCK, blockNo, receivedAnswer, receivedAnswerPar, NULL);
|
||||
} else {
|
||||
len = mifare_sendcmd_short(pcs, 1, ISO14443A_CMD_READBLOCK, blockNo, receivedAnswer, receivedAnswerPar, NULL);
|
||||
}
|
||||
|
||||
uint16_t len = mifare_sendcmd_short(pcs, 1, iso_byte, blockNo, receivedAnswer, receivedAnswerPar, NULL);
|
||||
if (len == 1) {
|
||||
if (g_dbglevel >= DBG_ERROR) Dbprintf("Cmd Error %02x", receivedAnswer[0]);
|
||||
return 1;
|
||||
|
@ -458,7 +452,6 @@ int mifare_classic_writeblock_ex(struct Crypto1State *pcs, uint32_t uid, uint8_t
|
|||
ReaderTransmitPar(d_block_enc, sizeof(d_block_enc), par, NULL);
|
||||
|
||||
// Receive the response
|
||||
|
||||
len = ReaderReceive(receivedAnswer, receivedAnswerPar);
|
||||
|
||||
uint8_t res = 0;
|
||||
|
|
|
@ -75,7 +75,7 @@ int mifare_classic_authex(struct Crypto1State *pcs, uint32_t uid, uint8_t blockN
|
|||
int mifare_classic_authex_2(struct Crypto1State *pcs, uint32_t uid, uint8_t blockNo, uint8_t keyType, uint64_t ui64Key, uint8_t isNested, uint32_t *ntptr, uint32_t *timing, bool is_gdm);
|
||||
|
||||
int mifare_classic_readblock(struct Crypto1State *pcs, uint32_t uid, uint8_t blockNo, uint8_t *blockData);
|
||||
int mifare_classic_readblock_ex(struct Crypto1State *pcs, uint32_t uid, uint8_t blockNo, uint8_t *blockData, bool is_gdm);
|
||||
int mifare_classic_readblock_ex(struct Crypto1State *pcs, uint32_t uid, uint8_t blockNo, uint8_t *blockData, uint8_t iso_byte);
|
||||
|
||||
int mifare_classic_halt(struct Crypto1State *pcs, uint32_t uid);
|
||||
int mifare_classic_halt_ex(struct Crypto1State *pcs);
|
||||
|
|
|
@ -400,6 +400,47 @@ static void mf_analyse_acl(uint16_t n, uint8_t *d) {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Sector trailer sanity checks.
|
||||
Warn if ACL is strict read-only, or invalid ACL.
|
||||
*/
|
||||
static int mf_analyse_st_block(uint8_t blockno, uint8_t *block, bool force){
|
||||
|
||||
if (mfIsSectorTrailer(blockno) == false) {
|
||||
return PM3_SUCCESS;
|
||||
}
|
||||
|
||||
PrintAndLogEx(INFO, "Sector trailer (ST) write detected");
|
||||
|
||||
// ensure access right isn't messed up.
|
||||
if (mfValidateAccessConditions(&block[6]) == false) {
|
||||
PrintAndLogEx(WARNING, "Invalid Access Conditions detected, replacing with default values");
|
||||
memcpy(block + 6, "\xFF\x07\x80\x69", 4);
|
||||
}
|
||||
|
||||
bool ro_detected = false;
|
||||
uint8_t bar = mfNumBlocksPerSector(mfSectorNum(blockno));
|
||||
for (uint8_t foo = 0; foo < bar; foo++) {
|
||||
if (mfReadOnlyAccessConditions(foo, &block[6])) {
|
||||
PrintAndLogEx(WARNING, "Strict ReadOnly Access Conditions on block " _YELLOW_("%u") " detected", blockno - bar + 1 + foo);
|
||||
ro_detected = true;
|
||||
}
|
||||
}
|
||||
if (ro_detected) {
|
||||
if (force) {
|
||||
PrintAndLogEx(WARNING, " --force override, continuing...");
|
||||
} else {
|
||||
PrintAndLogEx(INFO, "Exiting, please run `" _YELLOW_("hf mf acl -d %s") "` to understand", sprint_hex_inrow(&block[6], 3));
|
||||
PrintAndLogEx(INFO, "Use `" _YELLOW_("--force") "` to override and write this data");
|
||||
return PM3_EINVARG;
|
||||
}
|
||||
} else {
|
||||
PrintAndLogEx(SUCCESS, "ST passed checks, continuing...");
|
||||
}
|
||||
|
||||
return PM3_SUCCESS;
|
||||
}
|
||||
|
||||
static int CmdHF14AMfAcl(const char *Cmd) {
|
||||
CLIParserContext *ctx;
|
||||
CLIParserInit(&ctx, "hf mf acl",
|
||||
|
@ -521,7 +562,6 @@ static int CmdHF14AMfWrBl(const char *Cmd) {
|
|||
arg_lit0(NULL, "force", "override warnings"),
|
||||
arg_str0("k", "key", "<hex>", "key, 6 hex bytes"),
|
||||
arg_str0("d", "data", "<hex>", "bytes to write, 16 hex bytes"),
|
||||
|
||||
arg_param_end
|
||||
};
|
||||
CLIExecWithReturn(ctx, Cmd, argtable, false);
|
||||
|
@ -569,36 +609,8 @@ static int CmdHF14AMfWrBl(const char *Cmd) {
|
|||
|
||||
uint8_t blockno = (uint8_t)b;
|
||||
|
||||
// Sector trailer sanity checks.
|
||||
// Warn if ACL is strict read-only, or invalid ACL.
|
||||
if (mfIsSectorTrailer(blockno)) {
|
||||
PrintAndLogEx(INFO, "Sector trailer (ST) write detected");
|
||||
|
||||
// ensure access right isn't messed up.
|
||||
if (mfValidateAccessConditions(&block[6]) == false) {
|
||||
PrintAndLogEx(WARNING, "Invalid Access Conditions detected, replacing with default values");
|
||||
memcpy(block + 6, "\xFF\x07\x80\x69", 4);
|
||||
}
|
||||
|
||||
bool ro_detected = false;
|
||||
uint8_t bar = mfNumBlocksPerSector(mfSectorNum(blockno));
|
||||
for (uint8_t foo = 0; foo < bar; foo++) {
|
||||
if (mfReadOnlyAccessConditions(foo, &block[6])) {
|
||||
PrintAndLogEx(WARNING, "Strict ReadOnly Access Conditions on block " _YELLOW_("%u") " detected", blockno - bar + 1 + foo);
|
||||
ro_detected = true;
|
||||
}
|
||||
}
|
||||
if (ro_detected) {
|
||||
if (force) {
|
||||
PrintAndLogEx(WARNING, " --force override, continuing...");
|
||||
} else {
|
||||
PrintAndLogEx(INFO, "Exiting, please run `" _YELLOW_("hf mf acl -d %s") "` to understand", sprint_hex_inrow(&block[6], 3));
|
||||
PrintAndLogEx(INFO, "Use `" _YELLOW_("--force") "` to override and write this data");
|
||||
return PM3_EINVARG;
|
||||
}
|
||||
} else {
|
||||
PrintAndLogEx(SUCCESS, "ST passed checks, continuing...");
|
||||
}
|
||||
if (mf_analyse_st_block(blockno, block, force) != PM3_SUCCESS) {
|
||||
return PM3_EINVARG;
|
||||
}
|
||||
|
||||
PrintAndLogEx(INFO, "Writing block no %d, key %c - %s", blockno, (keytype == MF_KEY_B) ? 'B' : 'A', sprint_hex_inrow(key, sizeof(key)));
|
||||
|
@ -7560,73 +7572,47 @@ static int CmdHF14AGen4Save(const char *Cmd) {
|
|||
return PM3_SUCCESS;
|
||||
}
|
||||
|
||||
static int CmdHF14AGen4_GDM_GetBlk(const char *Cmd) {
|
||||
static int CmdHF14AGen4_GDM_ConfigBlk(const char *Cmd) {
|
||||
CLIParserContext *ctx;
|
||||
CLIParserInit(&ctx, "hf mf gdmgetblk",
|
||||
"Get block data from magic gen4 GDM card.",
|
||||
"hf mf gdmgetblk --blk 0 --> get block 0 (manufacturer)\n"
|
||||
"hf mf gdmgetblk --blk 3 -v --> get block 3, decode sector trailer\n"
|
||||
CLIParserInit(&ctx, "hf mf gdmconfig",
|
||||
"Get configuration data from magic gen4 GDM card.",
|
||||
"hf mf gdmconfig\n"
|
||||
);
|
||||
void *argtable[] = {
|
||||
arg_param_begin,
|
||||
arg_int1("b", "blk", "<dec>", "block number"),
|
||||
arg_lit0("v", "verbose", "verbose output"),
|
||||
arg_str0("k", "key", "<hex>", "key 6 bytes"),
|
||||
arg_param_end
|
||||
};
|
||||
CLIExecWithReturn(ctx, Cmd, argtable, false);
|
||||
int b = arg_get_int_def(ctx, 1, 0);
|
||||
bool verbose = arg_get_lit(ctx, 2);
|
||||
CLIExecWithReturn(ctx, Cmd, argtable, true);
|
||||
|
||||
int keylen = 0;
|
||||
uint8_t key[6] = {0};
|
||||
CLIGetHexWithReturn(ctx, 3, key, &keylen);
|
||||
CLIGetHexWithReturn(ctx, 1, key, &keylen);
|
||||
CLIParserFree(ctx);
|
||||
|
||||
// validate args
|
||||
if (b < 0 || b >= MIFARE_4K_MAXBLOCK) {
|
||||
PrintAndLogEx(FAILED, "target block number out-of-range, got %i", b);
|
||||
return PM3_EINVARG;
|
||||
}
|
||||
|
||||
if (keylen != 6 && keylen != 0) {
|
||||
PrintAndLogEx(FAILED, "Must specify 6 bytes, got " _YELLOW_("%u"), keylen);
|
||||
return PM3_EINVARG;
|
||||
}
|
||||
|
||||
uint8_t blockno = (uint8_t)b;
|
||||
PrintAndLogEx(NORMAL, "Block: %x", blockno) ;
|
||||
|
||||
struct p {
|
||||
uint8_t blockno;
|
||||
uint8_t keytype;
|
||||
uint8_t key[6];
|
||||
} PACKED payload;
|
||||
|
||||
payload.blockno = blockno;
|
||||
payload.keytype = 0;
|
||||
memcpy(payload.key, key, sizeof(payload.key));
|
||||
|
||||
clearCommandBuffer();
|
||||
SendCommandNG(CMD_HF_MIFARE_G4_GDM_RDBL, (uint8_t *)&payload, sizeof(payload));
|
||||
SendCommandNG(CMD_HF_MIFARE_G4_GDM_CONFIG, (uint8_t *)&payload, sizeof(payload));
|
||||
PacketResponseNG resp;
|
||||
if (WaitForResponseTimeout(CMD_HF_MIFARE_G4_GDM_RDBL, &resp, 1500) == false) {
|
||||
if (WaitForResponseTimeout(CMD_HF_MIFARE_G4_GDM_CONFIG, &resp, 1500) == false) {
|
||||
PrintAndLogEx(WARNING, "command execute timeout");
|
||||
return PM3_ETIMEOUT;
|
||||
}
|
||||
|
||||
if (resp.status == PM3_SUCCESS) {
|
||||
uint8_t sector = mfSectorNum(blockno);
|
||||
mf_print_sector_hdr(sector);
|
||||
|
||||
uint8_t *d = resp.data.asBytes;
|
||||
mf_print_block_one(blockno, d, verbose);
|
||||
|
||||
if (verbose) {
|
||||
decode_print_st(blockno, d);
|
||||
} else {
|
||||
PrintAndLogEx(NORMAL, "");
|
||||
}
|
||||
PrintAndLogEx(SUCCESS, "config... %s", sprint_hex(d, resp.length));
|
||||
PrintAndLogEx(NORMAL, "");
|
||||
}
|
||||
|
||||
return resp.status;
|
||||
|
@ -7636,7 +7622,9 @@ static int CmdHF14AGen4_GDM_SetBlk(const char *Cmd) {
|
|||
|
||||
CLIParserContext *ctx;
|
||||
CLIParserInit(&ctx, "hf mf gdmsetblk",
|
||||
"Set block data on a magic gen4 GDM card",
|
||||
"Set block data on a magic gen4 GDM card\n"
|
||||
"`--force` param is used to override warnings like bad ACL writes.\n"
|
||||
" if not specified, it will exit if detected",
|
||||
"hf mf gdmsetblk --blk 1 -d 000102030405060708090a0b0c0d0e0f"
|
||||
);
|
||||
void *argtable[] = {
|
||||
|
@ -7644,8 +7632,9 @@ static int CmdHF14AGen4_GDM_SetBlk(const char *Cmd) {
|
|||
arg_int1(NULL, "blk", "<dec>", "block number"),
|
||||
arg_lit0("a", NULL, "input key type is key A (def)"),
|
||||
arg_lit0("b", NULL, "input key type is key B"),
|
||||
arg_str0("k", "key", "<hex>", "key, 6 hex bytes"),
|
||||
arg_str0("d", "data", "<hex>", "bytes to write, 16 hex bytes"),
|
||||
arg_str0("k", "key", "<hex>", "key, 6 hex bytes"),
|
||||
arg_lit0(NULL, "force", "override warnings"),
|
||||
arg_param_end
|
||||
};
|
||||
CLIExecWithReturn(ctx, Cmd, argtable, false);
|
||||
|
@ -7661,13 +7650,15 @@ static int CmdHF14AGen4_GDM_SetBlk(const char *Cmd) {
|
|||
keytype = MF_KEY_B;;
|
||||
}
|
||||
|
||||
int keylen = 0;
|
||||
uint8_t key[6] = {0};
|
||||
CLIGetHexWithReturn(ctx, 4, key, &keylen);
|
||||
|
||||
uint8_t block[MFBLOCK_SIZE] = {0x00};
|
||||
int blen = 0;
|
||||
CLIGetHexWithReturn(ctx, 5, block, &blen);
|
||||
CLIGetHexWithReturn(ctx, 4, block, &blen);
|
||||
|
||||
int keylen = 0;
|
||||
uint8_t key[6] = {0};
|
||||
CLIGetHexWithReturn(ctx, 5, key, &keylen);
|
||||
|
||||
bool force = arg_get_lit(ctx, 6);
|
||||
CLIParserFree(ctx);
|
||||
|
||||
if (blen != MFBLOCK_SIZE) {
|
||||
|
@ -7687,6 +7678,10 @@ static int CmdHF14AGen4_GDM_SetBlk(const char *Cmd) {
|
|||
|
||||
uint8_t blockno = (uint8_t)b;
|
||||
|
||||
if (mf_analyse_st_block(blockno, block, force) != PM3_SUCCESS) {
|
||||
return PM3_EINVARG;
|
||||
}
|
||||
|
||||
PrintAndLogEx(INFO, "Writing block no %d, key %c - %s", blockno, (keytype == MF_KEY_B) ? 'B' : 'A', sprint_hex_inrow(key, sizeof(key)));
|
||||
PrintAndLogEx(INFO, "data: %s", sprint_hex(block, sizeof(block)));
|
||||
|
||||
|
@ -7985,7 +7980,7 @@ static command_t CommandTable[] = {
|
|||
{"gsetblk", CmdHF14AGen4SetBlk, IfPm3Iso14443a, "Write block to card"},
|
||||
{"gview", CmdHF14AGen4View, IfPm3Iso14443a, "View card"},
|
||||
{"-----------", CmdHelp, IfPm3Iso14443a, "-------------------- " _CYAN_("magic gen4 GDM") " --------------------------"},
|
||||
{"gdmgetblk", CmdHF14AGen4_GDM_GetBlk, IfPm3Iso14443a, "Read block from card"},
|
||||
{"gdmconfig", CmdHF14AGen4_GDM_ConfigBlk, IfPm3Iso14443a, "Read config block from card"},
|
||||
{"gdmsetblk", CmdHF14AGen4_GDM_SetBlk, IfPm3Iso14443a, "Write block to card"},
|
||||
{"-----------", CmdHelp, IfPm3Iso14443a, "----------------------- " _CYAN_("ndef") " -----------------------"},
|
||||
// {"ice", CmdHF14AMfice, IfPm3Iso14443a, "collect MIFARE Classic nonces to file"},
|
||||
|
|
|
@ -540,9 +540,11 @@ This tag has simular commands to the [UFUID](#mifare-classic-directwrite-ufuid-v
|
|||
It seems to be developed by the same person.
|
||||
|
||||
**OBS**
|
||||
|
||||
When writing to persistent memory it is possible to write _bad_ ACL and perm-brick the tag.
|
||||
|
||||
**OBS**
|
||||
|
||||
It is possible to write a configuration that perma locks the tag, ie no more magic
|
||||
|
||||
### Identify
|
||||
|
@ -579,6 +581,7 @@ Write config:
|
|||
3. send 16 bytes
|
||||
|
||||
**Warning**
|
||||
|
||||
Example of configuration to Perma lock tag:
|
||||
`85000000000000000000000000000008`
|
||||
|
||||
|
@ -596,7 +599,7 @@ It is unknown what kind of block 0 changes the tag supports
|
|||
hf mf gdmsetblk
|
||||
|
||||
# Read 0xE0 configuration:
|
||||
hf mf gdmgetblk
|
||||
hf mf gdmconfig
|
||||
|
||||
```
|
||||
|
||||
|
|
|
@ -689,6 +689,7 @@ typedef struct {
|
|||
// Gen 4 GDM magic cards
|
||||
#define CMD_HF_MIFARE_G4_GDM_RDBL 0x0870
|
||||
#define CMD_HF_MIFARE_G4_GDM_WRBL 0x0871
|
||||
#define CMD_HF_MIFARE_G4_GDM_CONFIG 0x0872
|
||||
|
||||
#define CMD_UNKNOWN 0xFFFF
|
||||
|
||||
|
|
|
@ -194,6 +194,7 @@ ISO 7816-4 Basic interindustry commands. For command APDU's.
|
|||
#define MIFARE_MAGIC_GDM_AUTH_KEYB 0x81
|
||||
#define MIFARE_MAGIC_GDM_WRITEBLOCK 0xA8
|
||||
#define MIFARE_MAGIC_GDM_READBLOCK 0xE0
|
||||
#define MIFARE_MAGIC_GDM_READBLOCK_1 0xE1
|
||||
|
||||
#define MIFARE_EV1_PERSONAL_UID 0x40
|
||||
#define MIFARE_EV1_SETMODE 0x43
|
||||
|
|
Loading…
Reference in a new issue