mirror of
https://gitlab.com/cryptsetup/cryptsetup.git
synced 2025-12-17 22:00:07 +01:00
Add luksHeaderBackup and luksHeaderRestore commands and API cals.
git-svn-id: https://cryptsetup.googlecode.com/svn/trunk@114 36d66b0a-2a48-0410-832c-cd162a569da5
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
2009-09-28 Milan Broz <mbroz@redhat.com>
|
||||
* Add luksHeaderBackup and luksHeaderRestore commands.
|
||||
|
||||
2009-09-15 Milan Broz <mbroz@redhat.com>
|
||||
* Initialize crypto library before LUKS header load.
|
||||
* Fix manpage to not require --size which expands to device size by default.
|
||||
|
||||
@@ -48,6 +48,7 @@ struct device_infos {
|
||||
};
|
||||
|
||||
struct crypt_device;
|
||||
int crypt_confirm(struct crypt_device *cd, const char *msg);
|
||||
|
||||
void set_error_va(const char *fmt, va_list va);
|
||||
void set_error(const char *fmt, ...);
|
||||
|
||||
@@ -442,6 +442,32 @@ int crypt_get_volume_key_size(struct crypt_device *cd);
|
||||
typedef enum { SLOT_INVALID, SLOT_INACTIVE, SLOT_ACTIVE, SLOT_ACTIVE_LAST } crypt_keyslot_info;
|
||||
crypt_keyslot_info crypt_keyslot_status(struct crypt_device *cd, int keyslot);
|
||||
|
||||
/**
|
||||
* Backup header and keyslots to file
|
||||
*
|
||||
* Returns 0 on success or negative errno value otherwise.
|
||||
*
|
||||
* @cd - crypt device handle
|
||||
* @requested_type - type of header to backup
|
||||
* @backup_file - file to backup header to
|
||||
*/
|
||||
int crypt_header_backup(struct crypt_device *cd,
|
||||
const char *requested_type,
|
||||
const char *backup_file);
|
||||
|
||||
/**
|
||||
* Restore header and keyslots from backup file
|
||||
*
|
||||
* Returns 0 on success or negative errno value otherwise.
|
||||
*
|
||||
* @cd - crypt device handle
|
||||
* @requested_type - type of header to restore
|
||||
* @backup_file - file to restore header from
|
||||
*/
|
||||
int crypt_header_restore(struct crypt_device *cd,
|
||||
const char *requested_type,
|
||||
const char *backup_file);
|
||||
|
||||
/**
|
||||
* Receives last reported error
|
||||
*
|
||||
|
||||
34
lib/setup.c
34
lib/setup.c
@@ -424,6 +424,14 @@ static int yesDialog_wrapper(const char *msg, void *usrptr)
|
||||
return xyesDialog((char*)msg);
|
||||
}
|
||||
|
||||
int crypt_confirm(struct crypt_device *cd, const char *msg)
|
||||
{
|
||||
if (!cd || !cd->confirm)
|
||||
return 1;
|
||||
else
|
||||
return cd->confirm(msg, cd->confirm_usrptr);
|
||||
}
|
||||
|
||||
static void key_from_terminal(struct crypt_device *cd, char *msg, char **key,
|
||||
unsigned int *key_len, int force_verify)
|
||||
{
|
||||
@@ -1126,6 +1134,32 @@ int crypt_load(struct crypt_device *cd,
|
||||
return r;
|
||||
}
|
||||
|
||||
int crypt_header_backup(struct crypt_device *cd,
|
||||
const char *requested_type,
|
||||
const char *backup_file)
|
||||
{
|
||||
if ((requested_type && !isLUKS(requested_type)) || !backup_file)
|
||||
return -EINVAL;
|
||||
|
||||
log_dbg("Requested header backup of device %s (%s) to "
|
||||
"file %s.", cd->device, requested_type, backup_file);
|
||||
|
||||
return LUKS_hdr_backup(backup_file, cd->device, &cd->hdr, cd);
|
||||
}
|
||||
|
||||
int crypt_header_restore(struct crypt_device *cd,
|
||||
const char *requested_type,
|
||||
const char *backup_file)
|
||||
{
|
||||
if (requested_type && !isLUKS(requested_type))
|
||||
return -EINVAL;
|
||||
|
||||
log_dbg("Requested header restore to device %s (%s) from "
|
||||
"file %s.", cd->device, requested_type, backup_file);
|
||||
|
||||
return LUKS_hdr_restore(backup_file, cd->device, &cd->hdr, cd);
|
||||
}
|
||||
|
||||
void crypt_free(struct crypt_device *cd)
|
||||
{
|
||||
if (cd) {
|
||||
|
||||
235
luks/keymanage.c
235
luks/keymanage.c
@@ -23,6 +23,7 @@
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -75,28 +76,179 @@ struct luks_masterkey *LUKS_generate_masterkey(int keylength)
|
||||
return mk;
|
||||
}
|
||||
|
||||
int LUKS_read_phdr(const char *device,
|
||||
int LUKS_hdr_backup(
|
||||
const char *backup_file,
|
||||
const char *device,
|
||||
struct luks_phdr *hdr,
|
||||
struct crypt_device *ctx)
|
||||
{
|
||||
int r = 0, devfd = -1;
|
||||
size_t buffer_size;
|
||||
char *buffer = NULL;
|
||||
struct stat st;
|
||||
|
||||
if(stat(backup_file, &st) == 0) {
|
||||
log_err(ctx, _("Requested file %s already exist.\n"), backup_file);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
r = LUKS_read_phdr(device, hdr, 0, ctx);
|
||||
if (r)
|
||||
return r;
|
||||
|
||||
buffer_size = hdr->payloadOffset << SECTOR_SHIFT;
|
||||
buffer = safe_alloc(buffer_size);
|
||||
if (!buffer || buffer_size < LUKS_ALIGN_KEYSLOTS) {
|
||||
r = -ENOMEM;
|
||||
goto out;
|
||||
}
|
||||
|
||||
log_dbg("Storing backup of header (%u bytes) and keyslot area (%u bytes).",
|
||||
sizeof(*hdr), buffer_size - LUKS_ALIGN_KEYSLOTS);
|
||||
|
||||
devfd = open(device, O_RDONLY | O_DIRECT | O_SYNC);
|
||||
if(devfd == -1) {
|
||||
log_err(ctx, _("Device %s is not LUKS device.\n"), device);
|
||||
r = -EINVAL;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if(read_blockwise(devfd, buffer, buffer_size) < buffer_size) {
|
||||
r = -EIO;
|
||||
goto out;
|
||||
}
|
||||
close(devfd);
|
||||
|
||||
/* Wipe unused area, so backup cannot contain old signatures */
|
||||
memset(buffer + sizeof(*hdr), 0, LUKS_ALIGN_KEYSLOTS - sizeof(*hdr));
|
||||
|
||||
devfd = creat(backup_file, S_IRUSR);
|
||||
if(devfd == -1) {
|
||||
r = -EINVAL;
|
||||
goto out;
|
||||
}
|
||||
if(write(devfd, buffer, buffer_size) < buffer_size) {
|
||||
log_err(ctx, _("Cannot write header backup file %s.\n"), backup_file);
|
||||
r = -EIO;
|
||||
goto out;
|
||||
}
|
||||
close(devfd);
|
||||
|
||||
r = 0;
|
||||
out:
|
||||
if (devfd != -1)
|
||||
close(devfd);
|
||||
safe_free(buffer);
|
||||
return r;
|
||||
}
|
||||
|
||||
int LUKS_hdr_restore(
|
||||
const char *backup_file,
|
||||
const char *device,
|
||||
struct luks_phdr *hdr,
|
||||
struct crypt_device *ctx)
|
||||
{
|
||||
int r = 0, devfd = -1, diff_uuid = 0;
|
||||
size_t buffer_size;
|
||||
char *buffer = NULL, msg[200];
|
||||
struct stat st;
|
||||
struct luks_phdr hdr_file;
|
||||
|
||||
if(stat(backup_file, &st) < 0) {
|
||||
log_err(ctx, _("Backup file %s doesn't exist.\n"), backup_file);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
r = LUKS_read_phdr_backup(backup_file, device, &hdr_file, 0, ctx);
|
||||
buffer_size = hdr_file.payloadOffset << SECTOR_SHIFT;
|
||||
|
||||
if (r || buffer_size < LUKS_ALIGN_KEYSLOTS) {
|
||||
log_err(ctx, _("Backup file do not contain valid LUKS header.\n"));
|
||||
r = -EINVAL;
|
||||
goto out;
|
||||
}
|
||||
|
||||
buffer = safe_alloc(buffer_size);
|
||||
if (!buffer) {
|
||||
r = -ENOMEM;
|
||||
goto out;
|
||||
}
|
||||
|
||||
devfd = open(backup_file, O_RDONLY);
|
||||
if(devfd == -1) {
|
||||
log_err(ctx, _("Cannot open header backup file %s.\n"), backup_file);
|
||||
r = -EINVAL;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if(read(devfd, buffer, buffer_size) < buffer_size) {
|
||||
log_err(ctx, _("Cannot read header backup file %s.\n"), backup_file);
|
||||
r = -EIO;
|
||||
goto out;
|
||||
}
|
||||
close(devfd);
|
||||
|
||||
r = LUKS_read_phdr(device, hdr, 0, ctx);
|
||||
if (r == 0) {
|
||||
log_dbg("Device %s already contains LUKS header, checking UUID and offset.", device);
|
||||
if(hdr->payloadOffset != hdr_file.payloadOffset ||
|
||||
hdr->keyBytes != hdr_file.keyBytes) {
|
||||
log_err(ctx, _("Data offset or key size differs on device and backup, restore failed.\n"));
|
||||
r = -EINVAL;
|
||||
goto out;
|
||||
}
|
||||
if (memcmp(hdr->uuid, hdr_file.uuid, UUID_STRING_L))
|
||||
diff_uuid = 1;
|
||||
}
|
||||
|
||||
if (snprintf(msg, sizeof(msg), _("Device %s %s%s"), device,
|
||||
r ? _("does not contain LUKS header. Replacing header can destroy data on that device.") :
|
||||
_("already contains LUKS header. Replacing header will destroy existing keyslots."),
|
||||
diff_uuid ? _("\nWARNING: real device header has different UUID than backup!") : "") < 0) {
|
||||
r = -ENOMEM;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (!crypt_confirm(ctx, msg)) {
|
||||
r = -EINVAL;
|
||||
goto out;
|
||||
}
|
||||
|
||||
log_dbg("Storing backup of header (%u bytes) and keyslot area (%u bytes) to device %s.",
|
||||
sizeof(*hdr), buffer_size - LUKS_ALIGN_KEYSLOTS, device);
|
||||
|
||||
devfd = open(device, O_WRONLY | O_DIRECT | O_SYNC);
|
||||
if(devfd == -1) {
|
||||
log_err(ctx, _("Cannot open device %s.\n"), device);
|
||||
r = -EINVAL;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if(write_blockwise(devfd, buffer, buffer_size) < buffer_size) {
|
||||
r = -EIO;
|
||||
goto out;
|
||||
}
|
||||
close(devfd);
|
||||
|
||||
/* Be sure to reload new data */
|
||||
r = LUKS_read_phdr(device, hdr, 0, ctx);
|
||||
out:
|
||||
if (devfd != -1)
|
||||
close(devfd);
|
||||
safe_free(buffer);
|
||||
return r;
|
||||
}
|
||||
|
||||
static int _check_and_convert_hdr(const char *device,
|
||||
struct luks_phdr *hdr,
|
||||
int require_luks_device,
|
||||
struct crypt_device *ctx)
|
||||
{
|
||||
int devfd = 0, r = 0;
|
||||
int r = 0;
|
||||
unsigned int i;
|
||||
uint64_t size;
|
||||
char luksMagic[] = LUKS_MAGIC;
|
||||
|
||||
log_dbg("Reading LUKS header of size %d from device %s",
|
||||
sizeof(struct luks_phdr), device);
|
||||
|
||||
devfd = open(device,O_RDONLY | O_DIRECT | O_SYNC);
|
||||
if(-1 == devfd) {
|
||||
log_err(ctx, _("Cannot open device %s.\n"), device);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if(read_blockwise(devfd, hdr, sizeof(struct luks_phdr)) < sizeof(struct luks_phdr)) {
|
||||
r = -EIO;
|
||||
} else if(memcmp(hdr->magic, luksMagic, LUKS_MAGIC_L)) { /* Check magic */
|
||||
if(memcmp(hdr->magic, luksMagic, LUKS_MAGIC_L)) { /* Check magic */
|
||||
log_dbg("LUKS header not detected.");
|
||||
if (require_luks_device)
|
||||
log_err(ctx, _("%s is not a LUKS device.\n"), device);
|
||||
@@ -122,6 +274,57 @@ int LUKS_read_phdr(const char *device,
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
int LUKS_read_phdr_backup(const char *backup_file,
|
||||
const char *device,
|
||||
struct luks_phdr *hdr,
|
||||
int require_luks_device,
|
||||
struct crypt_device *ctx)
|
||||
{
|
||||
int devfd = 0, r = 0;
|
||||
|
||||
log_dbg("Reading LUKS header of size %d from backup file %s",
|
||||
sizeof(struct luks_phdr), backup_file);
|
||||
|
||||
devfd = open(backup_file, O_RDONLY);
|
||||
if(-1 == devfd) {
|
||||
log_err(ctx, _("Cannot open file %s.\n"), device);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if(read(devfd, hdr, sizeof(struct luks_phdr)) < sizeof(struct luks_phdr))
|
||||
r = -EIO;
|
||||
else
|
||||
r = _check_and_convert_hdr(backup_file, hdr, require_luks_device, ctx);
|
||||
|
||||
close(devfd);
|
||||
return r;
|
||||
}
|
||||
|
||||
int LUKS_read_phdr(const char *device,
|
||||
struct luks_phdr *hdr,
|
||||
int require_luks_device,
|
||||
struct crypt_device *ctx)
|
||||
{
|
||||
int devfd = 0, r = 0;
|
||||
uint64_t size;
|
||||
|
||||
log_dbg("Reading LUKS header of size %d from device %s",
|
||||
sizeof(struct luks_phdr), device);
|
||||
|
||||
devfd = open(device,O_RDONLY | O_DIRECT | O_SYNC);
|
||||
if(-1 == devfd) {
|
||||
log_err(ctx, _("Cannot open device %s.\n"), device);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if(read_blockwise(devfd, hdr, sizeof(struct luks_phdr)) < sizeof(struct luks_phdr))
|
||||
r = -EIO;
|
||||
else
|
||||
r = _check_and_convert_hdr(device, hdr, require_luks_device, ctx);
|
||||
|
||||
#ifdef BLKGETSIZE64
|
||||
if (r == 0 && (ioctl(devfd, BLKGETSIZE64, &size) < 0 ||
|
||||
size < (uint64_t)hdr->payloadOffset)) {
|
||||
@@ -195,7 +398,7 @@ int LUKS_generate_phdr(struct luks_phdr *header,
|
||||
char luksMagic[] = LUKS_MAGIC;
|
||||
uuid_t partitionUuid;
|
||||
int currentSector;
|
||||
int alignSectors = 4096/SECTOR_SIZE;
|
||||
int alignSectors = LUKS_ALIGN_KEYSLOTS / SECTOR_SIZE;
|
||||
if (alignPayload == 0)
|
||||
alignPayload = alignSectors;
|
||||
|
||||
|
||||
22
luks/luks.h
22
luks/luks.h
@@ -38,6 +38,9 @@
|
||||
/* Actually we need only 37, but we don't want struct autoaligning to kick in */
|
||||
#define UUID_STRING_L 40
|
||||
|
||||
/* Offset to align kesylot area */
|
||||
#define LUKS_ALIGN_KEYSLOTS 4096
|
||||
|
||||
/* Any integer values are stored in network byte order on disk and must be
|
||||
converted */
|
||||
|
||||
@@ -98,6 +101,25 @@ int LUKS_read_phdr(
|
||||
int require_luks_device,
|
||||
struct crypt_device *ctx);
|
||||
|
||||
int LUKS_read_phdr_backup(
|
||||
const char *backup_file,
|
||||
const char *device,
|
||||
struct luks_phdr *hdr,
|
||||
int require_luks_device,
|
||||
struct crypt_device *ctx);
|
||||
|
||||
int LUKS_hdr_backup(
|
||||
const char *backup_file,
|
||||
const char *device,
|
||||
struct luks_phdr *hdr,
|
||||
struct crypt_device *ctx);
|
||||
|
||||
int LUKS_hdr_restore(
|
||||
const char *backup_file,
|
||||
const char *device,
|
||||
struct luks_phdr *hdr,
|
||||
struct crypt_device *ctx);
|
||||
|
||||
int LUKS_write_phdr(
|
||||
const char *device,
|
||||
struct luks_phdr *hdr,
|
||||
|
||||
@@ -92,6 +92,23 @@ returns true, if <device> is a LUKS partition. Otherwise, false. No options.
|
||||
.IP
|
||||
dumps the header information of a LUKS partition. No options.
|
||||
.PP
|
||||
\fIluksHeaderBackup\fR <device> --header-backup-file <file>
|
||||
.IP
|
||||
Stores binary backup of LUKS header and keyslot areas.
|
||||
|
||||
\fBWARNING:\fR Please note that with this backup file (and old passphrase knowledge) you can decrypt data even if old passphrase was wiped from real device.
|
||||
|
||||
Also note that anti-forensic splitter is not used during manipulation with backup file.
|
||||
.PP
|
||||
\fIluksHeaderRestore\fR <device> --header-backup-file <file>
|
||||
.IP
|
||||
|
||||
Restores binary backup of LUKS header and keyslot areas from specified file.
|
||||
|
||||
\fBWARNING:\fR All the keyslot areas are overwritten, only active keyslots form backup file are available after issuing this command.
|
||||
|
||||
This command allows restoring header if device do not contain LUKS header or if the master key size and data offset in LUKS header on device match the backup file.
|
||||
.PP
|
||||
|
||||
For more information about LUKS, see \fBhttp://code.google.com/p/cryptsetup/wiki/Specification\fR
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ static char *opt_hash = NULL;
|
||||
static int opt_verify_passphrase = 0;
|
||||
static char *opt_key_file = NULL;
|
||||
static char *opt_master_key_file = NULL;
|
||||
static char *opt_header_backup_file = NULL;
|
||||
static unsigned int opt_key_size = 0;
|
||||
static int opt_key_slot = CRYPT_ANY_SLOT;
|
||||
static uint64_t opt_size = 0;
|
||||
@@ -54,6 +55,8 @@ static int action_luksUUID(int arg);
|
||||
static int action_luksDump(int arg);
|
||||
static int action_luksSuspend(int arg);
|
||||
static int action_luksResume(int arg);
|
||||
static int action_luksBackup(int arg);
|
||||
static int action_luksRestore(int arg);
|
||||
|
||||
static struct action_type {
|
||||
const char *type;
|
||||
@@ -81,6 +84,8 @@ static struct action_type {
|
||||
{ "luksDump", action_luksDump, 0, 1, 0, 0, 1, N_("<device>"), N_("dump LUKS partition information") },
|
||||
{ "luksSuspend",action_luksSuspend, 0, 1, 1, 1, 1, N_("<device>"), N_("Suspend LUKS device and wipe key (all IOs are frozen).") },
|
||||
{ "luksResume", action_luksResume, 0, 1, 1, 1, 1, N_("<device>"), N_("Resume suspended LUKS device.") },
|
||||
{ "luksHeaderBackup",action_luksBackup, 0, 1, 1, 1, 1, N_("<device>"), N_("Backup LUKS device header and keyslots") },
|
||||
{ "luksHeaderRestore",action_luksRestore,0,1, 1, 1, 1, N_("<device>"), N_("Restore LUKS device header and keyslots") },
|
||||
{ "luksDelKey", action_luksDelKey, 0, 2, 1, 1, 1, N_("<device> <key slot>"), N_("identical to luksKillSlot - DEPRECATED - see man page") },
|
||||
{ "reload", action_create, 1, 2, 1, 1, 1, N_("<name> <device>"), N_("modify active device - DEPRECATED - see man page") },
|
||||
{ NULL, NULL, 0, 0, 0, 0, 0, NULL, NULL }
|
||||
@@ -129,6 +134,16 @@ static struct interface_callbacks cmd_icb = {
|
||||
.log = cmdLineLog,
|
||||
};
|
||||
|
||||
static void _log(int class, const char *msg, void *usrptr)
|
||||
{
|
||||
cmdLineLog(class, (char *)msg);
|
||||
}
|
||||
|
||||
static int _yesDialog(const char *msg, void *usrptr)
|
||||
{
|
||||
return yesDialog((char*)msg);
|
||||
}
|
||||
|
||||
/* End ICBs */
|
||||
|
||||
static void show_status(int errcode)
|
||||
@@ -377,6 +392,7 @@ static int action_luksOpen(int arg)
|
||||
options.flags |= CRYPT_FLAG_READONLY;
|
||||
if (opt_non_exclusive)
|
||||
log_err(_("Obsolete option --non-exclusive is ignored.\n"));
|
||||
|
||||
return crypt_luksOpen(&options);
|
||||
}
|
||||
|
||||
@@ -529,6 +545,49 @@ out:
|
||||
return r;
|
||||
}
|
||||
|
||||
static int action_luksBackup(int arg)
|
||||
{
|
||||
struct crypt_device *cd = NULL;
|
||||
int r;
|
||||
|
||||
if (!opt_header_backup_file) {
|
||||
log_err(_("Option --header-bakup-file is required.\n"));
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if ((r = crypt_init(&cd, action_argv[0])))
|
||||
goto out;
|
||||
|
||||
crypt_set_log_callback(cd, _log, NULL);
|
||||
crypt_set_confirm_callback(cd, _yesDialog, NULL);
|
||||
|
||||
r = crypt_header_backup(cd, CRYPT_LUKS1, opt_header_backup_file);
|
||||
out:
|
||||
crypt_free(cd);
|
||||
return r;
|
||||
}
|
||||
|
||||
static int action_luksRestore(int arg)
|
||||
{
|
||||
struct crypt_device *cd = NULL;
|
||||
int r = 0;
|
||||
|
||||
if (!opt_header_backup_file) {
|
||||
log_err(_("Option --header-bakup-file is required.\n"));
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if ((r = crypt_init(&cd, action_argv[0])))
|
||||
goto out;
|
||||
|
||||
crypt_set_log_callback(cd, _log, NULL);
|
||||
crypt_set_confirm_callback(cd, _yesDialog, NULL);
|
||||
r = crypt_header_restore(cd, CRYPT_LUKS1, opt_header_backup_file);
|
||||
out:
|
||||
crypt_free(cd);
|
||||
return r;
|
||||
}
|
||||
|
||||
static void usage(poptContext popt_context, int exitcode,
|
||||
const char *error, const char *more)
|
||||
{
|
||||
@@ -638,6 +697,7 @@ int main(int argc, char **argv)
|
||||
{ "tries", 'T', POPT_ARG_INT, &opt_tries, 0, N_("How often the input of the passphrase canbe retried"), NULL },
|
||||
{ "align-payload", '\0', POPT_ARG_INT, &opt_align_payload, 0, N_("Align payload at <n> sector boundaries - for luksFormat"), N_("SECTORS") },
|
||||
{ "non-exclusive", '\0', POPT_ARG_NONE, &opt_non_exclusive, 0, N_("Allows non-exclusive access for luksOpen, WARNING see manpage."), NULL },
|
||||
{ "header-backup-file",'\0', POPT_ARG_STRING, &opt_header_backup_file,0, N_("File with LUKS header and keyslots backup."), NULL },
|
||||
POPT_TABLEEND
|
||||
};
|
||||
poptContext popt_context;
|
||||
|
||||
Reference in New Issue
Block a user