BD Drive Reverse Engineering

From PS3 Developer wiki
Jump to navigation Jump to search

Introduction

  • The following information was reverse engineered from Storage Manager which runs in LPAR1 and from sv_iso_spu_module.self.

Information about EID4

  • EID4 contains 2 128bit keys which are necessary to establish a secure communication channel to BD drive for sending vendor specific security commands.
  • EID4 is encrypted with AES-CBC-256 algorithm.
  • EID4 is of size 0x30 bytes: 0x0-0xf bytes = 1st key, 0x10-0x1f - 2nd key, 0x20-0x2f - CMAC-OMAC1 of EID4
  • The first key is used for encrypting data sent from host to BD drive.
  • The second key is used for decrypting data sent from BD drive to host.

Dumping EID4 IV and Key

  • I modified sv_iso_spu_module.self to dump EID4 IV and key.
  • I used my spuisofs Linux kernel module and the below SPU program to dump EID4 IV key on PS3 Linux.
  • After dumping EID4 key use CMAC-OMAC1 algorithm to check the CMAC of EID4. If the EID4 key you got is correct then the CMAC should match.

SPU Program

My program to dump EID4 AES-CBC-256 IV and key to PPU memory:

/*
 * Dump EID4 IV and key to EA with MFC
 *
 * Copyright (C) 2012 glevand <[email protected]>
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published
 * by the Free Software Foundation; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

.text

start:

	ila		$2, 0x3dfa0
	lr		$sp, $2

	ila		$80, 0x3e000
	lr		$81, $3

	stqd		$7, 0($80)
	stqd		$8, 0x10($80)	# store EID4 IV
	stqd		$9, 0x20($80)	# store upper 16bytes of EID4 key
	stqd		$10, 0x30($80)	# store lower 16bytes of EID4 key
	stqd		$11, 0x40($80)
	stqd		$12, 0x50($80)

	lr		$3, $80
	lr		$4, $81
	il		$5, 0x60
	il		$6, 0x7
	il		$7, 0x20
	brsl		$lr, 0x10	# mfc_dma_xfer

	il		$3, 0x7
	brsl		$lr, 0x28	# mfc_dma_wait

	stop		0x666		# our evil stop code :)

/*
 * r3 - LSA
 * r4 - EA
 * r5 - size
 * r6 - tag
 * r7 - cmd
 */
mfc_dma_xfer:

	wrch		$ch16, $3
	wrch		$ch17, $4
	shlqbyi		$4, $4, 4
	wrch		$ch18, $4
	wrch		$ch19, $5
	wrch		$ch20, $6
	wrch		$ch21, $7

	bi		$lr

/*
 * r3 - tag
 */
mfc_dma_wait:

	il		$2, 0
	nop		$127
	hbra		2f, 1f
	wrch		$ch23, $2

1:

	rchcnt		$2, $ch23
	ceqi		$2, $2, 1
	nop		$127
	nop		$127
	nop		$127
	nop		$127
	nop		$127

2:

	brz		$2, 1b
	hbr		3f, $lr
	rdch		$2, $ch24
	il		$2, 1
	shl		$2, $2, $3
	wrch		$ch22, $2
	il		$2, 2
	wrch		$ch23, $2
	rdch		$2, $ch24
	nop		$127

3:

	bi		$lr

Result

[glevand@arch dump_eid4_key]$ ./dump_eid4_key ../dump_eid4_key.self ../eid4
spuisofs found at /mnt
arg1 kernel virtual address d000000000722000
shadow: spe_execution_status 7
priv2: puint_mb_R 2
shadow: spe_execution_status b
problem: spu_status_R 6660082
[glevand@arch dump_eid4_key]$ hexdump -C /mnt/arg1
...
Here should be you EID4 IV and key
IV is at offset 0x10 (16 bytes)
Key is at offset 0x20 (32 bytes)
...

Establish Secure Communication Channel

  • With both keys from EID4 we are now able to establish a secure communication channel with BD drive and send vendor-specific ATAPI commands to it.
  • Keys from EID4 are used only to derive a session key. After that they are not used anymore. A similar procedure is used e.g. to establish a secure communication channel with ENCDEC device.
  • ATAPI commands SEND_KEY and REPORT_KEY are used to exchange random number between host and BD drive.
  • Exchanged random numbers are used to derive the session key which is used later to send vendor-specific ATAPI commands (0xE0 and 0xE1) to BD drive.
  • The same procedue is follwed e.g. by Storage Manager which runs in LPAR1.
  • 3DES-CBC with 2 keys is used to encrypt commands sent to BD drive.

Session Key

TODO

Get Version

  • Here is an example of a simple program which establishes a secure communication channel with BD drive and reads BD FW version by using vendor-specific ATAPI commands 0xE0 and 0xE1.
  • It was tested on PS3 OtherOS++ 3.55 and Linux kernel 3.5.1.
  • The program uses Linux SCSI Generic driver to send ATAPI commands to BD drive.
/*-
 * Copyright (C) 2012 glevand <[email protected]>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer,
 *    without modification, immediately at the beginning of the file.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <getopt.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>

#include <scsi/sg.h>
#include <scsi/scsi_ioctl.h>

#include <openssl/aes.h>
#include <openssl/des.h>

static const unsigned char iv1[AES_BLOCK_SIZE] = {
	0x22, 0x26, 0x92, 0x8d, 0x44, 0x03, 0x2f, 0x43, 0x6a, 0xfd, 0x26, 0x7e, 0x74, 0x8b, 0x23, 0x93,
};

static const unsigned char iv2[DES_KEY_SZ] = {
	0xe8, 0x0b, 0x3f, 0x0c, 0xd6, 0x56, 0x6d, 0xd0,
};

static const unsigned char iv3[AES_BLOCK_SIZE] = {
	0x3b, 0xd6, 0x24, 0x02, 0x0b, 0xd3, 0xf8, 0x65, 0xe8, 0x0b, 0x3f, 0x0c, 0xd6, 0x56, 0x6d, 0xd0,
};

static const unsigned char key3[AES_BLOCK_SIZE] = {
	0x12, 0x6c, 0x6b, 0x59, 0x45, 0x37, 0x0e, 0xee, 0xca, 0x68, 0x26, 0x2d, 0x02, 0xdd, 0x12, 0xd2,
};

static const unsigned char key4[AES_BLOCK_SIZE] = {
	0xd9, 0xa2, 0x0a, 0x79, 0x66, 0x6c, 0x27, 0xd1, 0x10, 0x32, 0xac, 0xcf, 0x0d, 0x7f, 0xb5, 0x01,
};

static const unsigned char key5[AES_BLOCK_SIZE] = {
	0x19, 0x76, 0x6f, 0xbc, 0x77, 0xe4, 0xe7, 0x5c, 0xf4, 0x41, 0xe4, 0x8b, 0x94, 0x2c, 0x5b, 0xd9,
};

static const unsigned char key6[AES_BLOCK_SIZE] = {
	0x50, 0xcb, 0xa7, 0xf0, 0xc2, 0xa7, 0xc0, 0xf6, 0xf3, 0x3a, 0x21, 0x43, 0x26, 0xac, 0x4e, 0xf3,
};

static const unsigned char cmd_4_14[] = {
	0x1e, 0x79, 0x18, 0x8e, 0x09, 0x3b, 0xc8, 0x77, 0x95, 0xb2, 0xcf, 0x2a, 0xe7, 0xaf, 0x9b, 0xb4,
	0x86, 0x80, 0x18, 0x28, 0xc2, 0xca, 0x05, 0xba, 0xd1, 0xf2, 0x78, 0xf1, 0x80, 0x1f, 0xea, 0xcb,
	0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x8d, 0xb3, 0x46, 0x93, 0x42, 0x64, 0x81, 0x60, 0x16, 0x8f, 0x51, 0xd1, 0x93, 0x76, 0x23, 0x95,
};

static struct option long_opts[] = {
	{ "device",	no_argument, NULL, 'd' },
	{ "verbose",	no_argument, NULL, 'v' },
	{ "rnd1",	no_argument, NULL, 'r' },
	{ "key1",	no_argument, NULL, 'k' },
	{ "key2",	no_argument, NULL, 'l' },
	{ NULL, 0, NULL, 0 }
};

static const char *device = "/dev/sr0";
static int verbose;
static unsigned char rnd1[AES_BLOCK_SIZE] = {
	0x06, 0xd7, 0x33, 0xcb, 0x22, 0x4a, 0x83, 0x56, 0xa3, 0xe8, 0x39, 0x78, 0x66, 0xe4, 0x3e, 0xc2,
};
static unsigned char key1[AES_BLOCK_SIZE];
static int key1_length;
static unsigned char key2[AES_BLOCK_SIZE];
static int key2_length;

static int
parse_hex(const char *s, unsigned char *b, int maxlen)
{
	int len;
	char c;
	int i;

	len = strlen(s);
	if (len % 2)
		return (-1);

	for (i = 0; (i < (len / 2)) && (i < maxlen); i++) {
		if (!isxdigit(s[2 * i + 0]) || !isxdigit(s[2 * i + 1]))
			return (-1);

		b[i] = 0;

		c = tolower(s[2 * i + 0]);
		if (isdigit(c))
			b[i] += (c - '0') * 16;
		else
			b[i] += (c - 'a' + 10) * 16;

		c = tolower(s[2 * i + 1]);
		if (isdigit(c))
			b[i] += c - '0';
		else
			b[i] += c - 'a' + 10;
	}

	return (i);
}

static int
parse_opts(int argc, char **argv)
{
	int c;
	char *endptr;

	while ((c = getopt_long(argc, argv, "d:vtr:s:k:l:", long_opts, NULL)) != -1) {
		switch (c) {
		case 'd':
			device = optarg;
		break;
		case 'v':
			verbose++;
		break;
		case 'r':
			if (parse_hex(optarg, rnd1, AES_BLOCK_SIZE) != AES_BLOCK_SIZE) {
				fprintf(stderr, "invalid rnd1 specified: %s\n", optarg);
				return (-1);
			}
		break;
		case 'k':
			key1_length = parse_hex(optarg, key1, 2 * AES_BLOCK_SIZE);
			if (key1_length != AES_BLOCK_SIZE) {
				fprintf(stderr, "invalid key1 specified: %s\n", optarg);
				return (-1);
			}
		break;
		case 'l':
			key2_length = parse_hex(optarg, key2, 2 * AES_BLOCK_SIZE);
			if (key2_length != AES_BLOCK_SIZE) {
				fprintf(stderr, "invalid key2 specified: %s\n", optarg);
				return (-1);
			}
		break;
		default:
			fprintf(stderr, "invalid option specified: %c\n", c);
			return (-1);
		break;
		}
	}

	if (key1_length <= 0) {
		fprintf(stderr, "no key1 specified\n");
		return (-1);
	}

	if (key2_length <= 0) {
		fprintf(stderr, "no key2 specified\n");
		return (-1);
	}

	return (0);
}

static void
aes_cbc_encrypt(const unsigned char *iv, const unsigned char *key, int key_length,
	const unsigned char *data, int data_length, unsigned char *out)
{
	AES_KEY aes_key;
	unsigned char cbc[AES_BLOCK_SIZE];
	int i;

	AES_set_encrypt_key(key, key_length, &aes_key);

	memcpy(cbc, iv, AES_BLOCK_SIZE);

	while (data_length >= AES_BLOCK_SIZE) {
		for (i = 0; i < AES_BLOCK_SIZE; i++)
			out[i] = cbc[i] ^ data[i];

		AES_encrypt(out, out, &aes_key);

		memcpy(cbc, out, AES_BLOCK_SIZE);

		data += AES_BLOCK_SIZE;
		out += AES_BLOCK_SIZE;
		data_length -= AES_BLOCK_SIZE;
	}
}

static void
aes_cbc_decrypt(const unsigned char *iv, const unsigned char *key, int key_length,
	const unsigned char *data, int data_length, unsigned char *out)
{
	AES_KEY aes_key;
	unsigned char cbc[AES_BLOCK_SIZE];
	unsigned char buf[AES_BLOCK_SIZE];
	int i;

	AES_set_decrypt_key(key, key_length, &aes_key);

	memcpy(cbc, iv, AES_BLOCK_SIZE);

	while (data_length >= AES_BLOCK_SIZE) {
		memcpy(buf, data, AES_BLOCK_SIZE);

		AES_decrypt(data, out, &aes_key);

		for (i = 0; i < AES_BLOCK_SIZE; i++)
			out[i] ^= cbc[i];

		memcpy(cbc, buf, AES_BLOCK_SIZE);

		data += AES_BLOCK_SIZE;
		out += AES_BLOCK_SIZE;
		data_length -= AES_BLOCK_SIZE;
	}
}

static void
triple_des_cbc_encrypt(const unsigned char *iv, const unsigned char *key,
	const unsigned char *data, int data_length, unsigned char *out)
{
	DES_cblock kcb1;
	DES_cblock kcb2;
	DES_cblock ivcb;
	DES_key_schedule ks1;
	DES_key_schedule ks2;

	memcpy(kcb1, key, DES_KEY_SZ);
	memcpy(kcb2, key + DES_KEY_SZ, DES_KEY_SZ);
	memcpy(ivcb, iv, DES_KEY_SZ);

	DES_set_key_unchecked(&kcb1, &ks1);
	DES_set_key_unchecked(&kcb2, &ks2);

	DES_ede2_cbc_encrypt(data, out, data_length, &ks1, &ks2, &ivcb, DES_ENCRYPT);
}

static void
triple_des_cbc_decrypt(const unsigned char *iv, const unsigned char *key,
	const unsigned char *data, int data_length, unsigned char *out)
{
	DES_cblock kcb1;
	DES_cblock kcb2;
	DES_cblock ivcb;
	DES_key_schedule ks1;
	DES_key_schedule ks2;

	memcpy(kcb1, key, DES_KEY_SZ);
	memcpy(kcb2, key + DES_KEY_SZ, DES_KEY_SZ);
	memcpy(ivcb, iv, DES_KEY_SZ);

	DES_set_key_unchecked(&kcb1, &ks1);
	DES_set_key_unchecked(&kcb2, &ks2);

	DES_ede2_cbc_encrypt(data, out, data_length, &ks1, &ks2, &ivcb, DES_DECRYPT);
}

static unsigned char
cksum(const unsigned char *data, int len)
{
	unsigned short sum = 0;

	while (len--)
		sum += *data++;

	return (~sum);
}

static void
hex_fprintf(FILE *fp, const char *buf, size_t len)
{
	int i;

	if (len <= 0)
		return;

	for (i = 0; i < len; i++) {
		if ((i > 0) && !(i % 16))
			fprintf(fp, "\n");

		fprintf(fp, "%02x ", buf[i]);
	}

	fprintf(fp, "\n");
}

static int
test_unit_ready(const char *device, unsigned int *req_sense)
{
	int fd;
	struct sg_io_hdr io_hdr;
	char cmd[256], sense[32];
	int ret;

	fd = open(device, O_RDWR | O_NONBLOCK);
	if (fd < 0)
		return (-1);

	memset(cmd, 0, 6);

	memset(&io_hdr, 0, sizeof(io_hdr));
	io_hdr.interface_id = 'S';
	io_hdr.dxfer_direction = SG_DXFER_TO_DEV;
	io_hdr.timeout = 20000;
	io_hdr.cmdp = cmd;
	io_hdr.cmd_len = 6;
	io_hdr.dxferp = NULL;
	io_hdr.dxfer_len = 0;
	io_hdr.sbp = sense;
	io_hdr.mx_sb_len = sizeof(sense);

	ret = ioctl(fd, SG_IO, &io_hdr);
	if (ret) {
		close(fd);
		return (-1);
	}

	close(fd);

	if (io_hdr.status) {
		fprintf(stderr, "TEST UNIT READY failed: status %d host status %d driver status %d\n",
		    io_hdr.status, io_hdr.host_status, io_hdr.driver_status);
		fprintf(stderr, "sense buffer: ");
		hex_fprintf(stderr, sense, 0x10);
		*req_sense = (sense[2] << 16) | (sense[12] << 8) | sense[13];
		return (-1);
	}

	*req_sense = 0;

	return (0);
}

static int
send_key(const char *device, unsigned char key_class, unsigned short param_list_length,
	unsigned char agid, unsigned char key_fmt, unsigned char *param_list,
	unsigned int *req_sense)
{
	int fd;
	struct sg_io_hdr io_hdr;
	char cmd[256], sense[32];
	int ret;

	fd = open(device, O_RDWR | O_NONBLOCK);
	if (fd < 0)
		return (-1);

	cmd[0] = 0xa3;
	cmd[1] = 0x00;
	cmd[2] = 0x00;
	cmd[3] = 0x00;
	cmd[4] = 0x00;
	cmd[5] = 0x00;
	cmd[6] = 0x00;
	cmd[7] = key_class;
	*(uint16_t *) (cmd + 8) = param_list_length;
	cmd[10] = ((agid & 0x3) << 6) | (key_fmt & 0x3f);
	cmd[11] = 0x00;

	if (verbose) {
		fprintf(stdout, "cdb: ");
		hex_fprintf(stdout, cmd, 12);
	}

	memset(&io_hdr, 0, sizeof(io_hdr));
	io_hdr.interface_id = 'S';
	io_hdr.dxfer_direction = SG_DXFER_TO_DEV;
	io_hdr.timeout = 20000;
	io_hdr.cmdp = cmd;
	io_hdr.cmd_len = 12;
	io_hdr.dxferp = param_list;
	io_hdr.dxfer_len = param_list_length;
	io_hdr.sbp = sense;
	io_hdr.mx_sb_len = sizeof(sense);

	ret = ioctl(fd, SG_IO, &io_hdr);
	if (ret) {
		close(fd);
		return (-1);
	}

	close(fd);

	if (io_hdr.status) {
		fprintf(stderr, "SEND KEY failed: status %d host status %d driver status %d\n",
		    io_hdr.status, io_hdr.host_status, io_hdr.driver_status);
		fprintf(stderr, "sense buffer: ");
		hex_fprintf(stderr, sense, 0x10);
		*req_sense = (sense[2] << 16) | (sense[12] << 8) | sense[13];
		return (-1);
	}

	*req_sense = 0;

	return (0);
}

static int
report_key(const char *device, unsigned char key_class, unsigned int alloc_length,
	unsigned char agid, unsigned char key_fmt, unsigned char *buf,
	unsigned int *req_sense)
{
	int fd;
	struct sg_io_hdr io_hdr;
	char cmd[256], sense[32];
	int ret;

	fd = open(device, O_RDWR | O_NONBLOCK);
	if (fd < 0)
		return (-1);

	cmd[0] = 0xa4;
	cmd[1] = 0x00;
	cmd[2] = 0x00;
	cmd[3] = 0x00;
	cmd[4] = 0x00;
	cmd[5] = 0x00;
	cmd[6] = 0x00;
	cmd[7] = key_class;
	*(uint16_t *) (cmd + 8) = alloc_length;
	cmd[10] = ((agid & 0x3) << 6) | (key_fmt & 0x3f);
	cmd[11] = 0x00;

	if (verbose) {
		fprintf(stdout, "cdb: ");
		hex_fprintf(stdout, cmd, 12);
	}

	memset(&io_hdr, 0, sizeof(io_hdr));
	io_hdr.interface_id = 'S';
	io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
	io_hdr.timeout = 20000;
	io_hdr.cmdp = cmd;
	io_hdr.cmd_len = 12;
	io_hdr.dxferp = buf;
	io_hdr.dxfer_len = alloc_length;
	io_hdr.sbp = sense;
	io_hdr.mx_sb_len = sizeof(sense);

	ret = ioctl(fd, SG_IO, &io_hdr);
	if (ret) {
		close(fd);
		return (-1);
	}

	close(fd);

	if (io_hdr.status) {
		fprintf(stderr, "REPORT KEY failed: status %d host status %d driver status %d\n",
		    io_hdr.status, io_hdr.host_status, io_hdr.driver_status);
		fprintf(stderr, "sense buffer: ");
		hex_fprintf(stderr, sense, 0x10);
		*req_sense = (sense[2] << 16) | (sense[12] << 8) | sense[13];
		return (-1);
	}

	*req_sense = 0;

	return (0);
}

static int
send_e1(const char *device, unsigned char param_list_length, const unsigned char cdb[8],
	unsigned char *param_list, unsigned int *req_sense)
{
	int fd;
	struct sg_io_hdr io_hdr;
	char cmd[256], sense[32];
	int ret;

	fd = open(device, O_RDWR | O_NONBLOCK);
	if (fd < 0)
		return (-1);

	memset(cmd, 0, 12);
	cmd[0] = 0xe1;
	cmd[1] = 0x00;
	cmd[2] = param_list_length;
	cmd[3] = 0x00;
	memcpy(cmd + 4, cdb, 8);

	if (verbose) {
		fprintf(stdout, "cdb: ");
		hex_fprintf(stdout, cmd, 12);
	}

	memset(&io_hdr, 0, sizeof(io_hdr));
	io_hdr.interface_id = 'S';
	io_hdr.dxfer_direction = SG_DXFER_TO_DEV;
	io_hdr.timeout = 20000;
	io_hdr.cmdp = cmd;
	io_hdr.cmd_len = 12;
	io_hdr.dxferp = param_list;
	io_hdr.dxfer_len = param_list_length;
	io_hdr.sbp = sense;
	io_hdr.mx_sb_len = sizeof(sense);

	ret = ioctl(fd, SG_IO, &io_hdr);
	if (ret) {
		close(fd);
		return (-1);
	}

	close(fd);

	if (io_hdr.status) {
		fprintf(stderr, "E1 failed: status %d host status %d driver status %d\n",
		    io_hdr.status, io_hdr.host_status, io_hdr.driver_status);
		fprintf(stderr, "sense buffer: ");
		hex_fprintf(stderr, sense, 0x10);
		*req_sense = (sense[2] << 16) | (sense[12] << 8) | sense[13];
		return (-1);
	}

	*req_sense = 0;

	return (0);
}

static int
send_e0(const char *device, unsigned int alloc_length, const unsigned char cdb[8],
	unsigned char *buf, unsigned int *req_sense)
{
	int fd;
	struct sg_io_hdr io_hdr;
	char cmd[256], sense[32];
	int ret;

	fd = open(device, O_RDWR | O_NONBLOCK);
	if (fd < 0)
		return (-1);

	cmd[0] = 0xe0;
	cmd[1] = 0x00;
	cmd[2] = alloc_length;
	cmd[3] = 0x00;
	memcpy(cmd + 4, cdb, 8);

	if (verbose) {
		fprintf(stdout, "cdb: ");
		hex_fprintf(stdout, cmd, 12);
	}

	memset(&io_hdr, 0, sizeof(io_hdr));
	io_hdr.interface_id = 'S';
	io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
	io_hdr.timeout = 20000;
	io_hdr.cmdp = cmd;
	io_hdr.cmd_len = 12;
	io_hdr.dxferp = buf;
	io_hdr.dxfer_len = alloc_length;
	io_hdr.sbp = sense;
	io_hdr.mx_sb_len = sizeof(sense);

	ret = ioctl(fd, SG_IO, &io_hdr);
	if (ret) {
		close(fd);
		return (-1);
	}

	close(fd);

	if (io_hdr.status) {
		fprintf(stderr, "E0 failed: status %d host status %d driver status %d\n",
		    io_hdr.status, io_hdr.host_status, io_hdr.driver_status);
		fprintf(stderr, "sense buffer: ");
		hex_fprintf(stderr, sense, 0x10);
		*req_sense = (sense[2] << 16) | (sense[12] << 8) | sense[13];
		return (-1);
	}

	*req_sense = 0;

	return (0);
}

int
main(int argc, char **argv)
{
	unsigned char cdb[256];
	unsigned char buf[256];
	unsigned char rnd2[AES_BLOCK_SIZE];
	unsigned char key7[AES_BLOCK_SIZE];
	unsigned char key8[AES_BLOCK_SIZE];
	unsigned int req_sense;
	int ret;

	ret = parse_opts(argc, argv);
	if (ret)
		exit(255);

	/* test unit ready */

	if (verbose)
		fprintf(stdout, "=== TEST UNIT READY (0x00) ===\n");

	ret = test_unit_ready(device, &req_sense);
	if (ret && (req_sense != 0x23a00))
		exit(1);

	/* security check */

	if (verbose)
		fprintf(stdout, "=== SEND KEY (0xA3) ===\n");

	memset(buf, 0, 0x14);

	ret = send_key(device, 0xe0, 0x14, 0x0, 0x0, buf, &req_sense);
	if (ret)
		exit(1);

	/* establish session */

	memset(buf, 0, 0x14);
	*(uint16_t *) buf = 0x10;
	memcpy(buf + 4, rnd1, sizeof(rnd1));

	fprintf(stdout, "rnd1: ");
	hex_fprintf(stdout, rnd1, sizeof(rnd1));

	if (verbose)
		hex_fprintf(stdout, buf, 0x14);

	aes_cbc_encrypt(iv1, key1, 8 * sizeof(key1), buf + 4, 0x10, buf + 4);

	if (verbose)
		hex_fprintf(stdout, buf, 0x14);

	/* send encrypted host random */

	if (verbose)
		fprintf(stdout, "=== SEND KEY (0xA3) ===\n");

	ret = send_key(device, 0xe0, 0x14, 0x0, 0x0, buf, &req_sense);
	if (ret)
		exit(1);

	/* receive encrypted host and drive randoms */

	if (verbose)
		fprintf(stdout, "=== REPORT KEY (0xA4) ===\n");

	ret = report_key(device, 0xe0, 0x24, 0x0, 0x0, buf, &req_sense);
	if (ret)
		exit(1);

	if (verbose)
		hex_fprintf(stdout, buf, 0x24);

	/* NB: decrypt received host and drive randoms separately */

	aes_cbc_decrypt(iv1, key2, 8 * sizeof(key2), buf + 4, 0x10, buf + 4);
	aes_cbc_decrypt(iv1, key2, 8 * sizeof(key2), buf + 0x14, 0x10, buf + 0x14);

	if (verbose)
		hex_fprintf(stdout, buf, 0x24);

	/* verify received host random */

	if (memcmp(rnd1, buf + 4, sizeof(rnd1))) {
		fprintf(stderr, "rnd1 mismatch\n");
		exit(1);
	}

	memcpy(rnd2, buf + 0x14, sizeof(rnd2));

	fprintf(stdout, "rnd2: ");
	hex_fprintf(stdout, rnd2, sizeof(rnd2));

	memset(buf, 0, 0x14);
	*(uint16_t *) buf = 0x10;
	memcpy(buf + 4, rnd2, sizeof(rnd2));

	if (verbose)
		hex_fprintf(stdout, buf, 0x14);

	aes_cbc_encrypt(iv1, key1, 8 * sizeof(key1), buf + 4, 0x10, buf + 4);

	if (verbose)
		hex_fprintf(stdout, buf, 0x14);

	/* send encrypted drive random */

	if (verbose)
		fprintf(stdout, "=== SEND KEY (0xA3) ===\n");

	ret = send_key(device, 0xe0, 0x14, 0x0, 0x2, buf, &req_sense);
	if (ret)
		exit(1);

	/* derive session keys from host and drive randoms */

	memcpy(key7, rnd1, 8);
	memcpy(key7 + 8, rnd2 + 8, 8);

	if (verbose)
		hex_fprintf(stdout, key7, sizeof(key7));

	aes_cbc_encrypt(iv1, key3, 8 * sizeof(key3), key7, sizeof(key7), key7);

	fprintf(stdout, "key7: ");
	hex_fprintf(stdout, key7, sizeof(key7));

	memcpy(key8, rnd1 + 8, 8);
	memcpy(key8 + 8, rnd2, 8);

	if (verbose)
		hex_fprintf(stdout, key8, sizeof(key8));

	aes_cbc_encrypt(iv1, key4, 8 * sizeof(key4), key8, sizeof(key8), key8);

	fprintf(stdout, "key8: ");
	hex_fprintf(stdout, key8, sizeof(key8));

	if (verbose)
		fprintf(stdout, "=== UNKNOWN (0xE1) ===\n");

	memset(cdb, 0, 8);
	cdb[6] = 0xe6;					/* random byte */
	cdb[7] = cksum(cdb, 7);

	if (verbose)
		hex_fprintf(stdout, cdb, 8);

	triple_des_cbc_encrypt(iv2, key7, cdb, 8, cdb);

	if (verbose)
		hex_fprintf(stdout, cdb, 8);

	memset(buf, 0, 0x54);
	*(uint16_t *) buf = 0x50;
	buf[5] = 0xee;					/* random byte */
	memcpy(buf + 8, cmd_4_14, sizeof(cmd_4_14));
	buf[4] = cksum(buf + 5, 0x4f);

	if (verbose)
		hex_fprintf(stdout, buf, 0x54);

	aes_cbc_encrypt(iv3, key7, 8 * sizeof(key7), buf + 4, 0x50, buf + 4);

	if (verbose)
		hex_fprintf(stdout, buf, 0x54);

	ret = send_e1(device, 0x54, cdb, buf, &req_sense);
	if (ret)
		exit(1);

	/* establish session again */

	memset(buf, 0, 0x14);
	*(uint16_t *) buf = 0x10;
	memcpy(buf + 4, rnd1, sizeof(rnd1));

	fprintf(stdout, "rnd1: ");
	hex_fprintf(stdout, rnd1, sizeof(rnd1));

	if (verbose)
		hex_fprintf(stdout, buf, 0x14);

	aes_cbc_encrypt(iv1, key5, 8 * sizeof(key5), buf + 4, 0x10, buf + 4);

	if (verbose)
		hex_fprintf(stdout, buf, 0x14);

	/* send encrypted host random */

	if (verbose)
		fprintf(stdout, "=== SEND KEY (0xA3) ===\n");

	ret = send_key(device, 0xe0, 0x14, 0x0, 0x1, buf, &req_sense);
	if (ret)
		exit(1);

	/* receive encrypted host and drive randoms */

	if (verbose)
		fprintf(stdout, "=== REPORT KEY (0xA4) ===\n");

	ret = report_key(device, 0xe0, 0x24, 0x0, 0x1, buf, &req_sense);
	if (ret)
		exit(1);

	if (verbose)
		hex_fprintf(stdout, buf, 0x24);

	/* NB: decrypt received host and drive randoms separately */

	aes_cbc_decrypt(iv1, key6, 8 * sizeof(key6), buf + 4, 0x10, buf + 4);
	aes_cbc_decrypt(iv1, key6, 8 * sizeof(key6), buf + 0x14, 0x10, buf + 0x14);

	if (verbose)
		hex_fprintf(stdout, buf, 0x24);

	/* verify received host random */

	if (memcmp(rnd1, buf + 4, sizeof(rnd1))) {
		fprintf(stderr, "rnd1 mismatch\n");
		exit(1);
	}

	memcpy(rnd2, buf + 0x14, sizeof(rnd2));

	fprintf(stdout, "rnd2: ");
	hex_fprintf(stdout, rnd2, sizeof(rnd2));

	memset(buf, 0, 0x14);
	*(uint16_t *) buf = 0x10;
	memcpy(buf + 4, rnd2, sizeof(rnd2));

	if (verbose)
		hex_fprintf(stdout, buf, 0x14);

	aes_cbc_encrypt(iv1, key5, 8 * sizeof(key5), buf + 4, 0x10, buf + 4);

	if (verbose)
		hex_fprintf(stdout, buf, 0x14);

	/* send encrypted drive random */

	if (verbose)
		fprintf(stdout, "=== SEND KEY (0xA3) ===\n");

	ret = send_key(device, 0xe0, 0x14, 0x0, 0x3, buf, &req_sense);
	if (ret)
		exit(1);

	/* derive session keys from host and drive randoms */

	memcpy(key7, rnd1, 8);
	memcpy(key7 + 8, rnd2 + 8, 8);

	if (verbose)
		hex_fprintf(stdout, key7, sizeof(key7));

	aes_cbc_encrypt(iv1, key3, 8 * sizeof(key3), key7, sizeof(key7), key7);

	fprintf(stdout, "key7: ");
	hex_fprintf(stdout, key7, sizeof(key7));

	memcpy(key8, rnd1 + 8, 8);
	memcpy(key8 + 8, rnd2, 8);

	if (verbose)
		hex_fprintf(stdout, key8, sizeof(key8));

	aes_cbc_encrypt(iv1, key4, 8 * sizeof(key4), key8, sizeof(key8), key8);

	fprintf(stdout, "key8: ");
	hex_fprintf(stdout, key8, sizeof(key8));

	/* retrieve response */

	if (verbose)
		fprintf(stdout, "=== UNKNOWN (0xE0) ===\n");

	memset(cdb, 0, 8);
	cdb[0] = 0x4;					/* random byte */
	cdb[6] = 0xe7;					/* random byte */
	cdb[7] = cksum(cdb, 7);

	if (verbose)
		hex_fprintf(stdout, cdb, 8);

	triple_des_cbc_encrypt(iv2, key7, cdb, 8, cdb);

	if (verbose)
		hex_fprintf(stdout, cdb, 8);

	ret = send_e0(device, 0x54, cdb, buf, &req_sense);
	if (ret)
		exit(1);

	if (verbose)
		hex_fprintf(stdout, buf, 0x54);

	aes_cbc_decrypt(iv3, key7, 8 * sizeof(key7), buf + 4, 0x50, buf + 4);

	if (verbose)
		hex_fprintf(stdout, buf, 0x54);

	if (buf[4] != cksum(buf + 5, 0x4f)) {
		fprintf(stderr, "checksum mismatch\n");
		exit(1);
	}

	fprintf(stdout, "version: ");
	hex_fprintf(stdout, buf + 6, 8);

	exit(0);
}