/*
* This file is part of libaacs
* Copyright (C) 2009-2010 Obliter0n
* Copyright (C) 2010 npzacs
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see
* .
*/
#if HAVE_CONFIG_H
#include "config.h"
#endif
#include "mmc.h"
#include "crypto.h"
#include "util/macro.h"
#include "util/logging.h"
#include
#include
#include
#include
#include
#ifdef HAVE_MNTENT_H
#include
#endif
#ifdef HAVE_LIMITS_H
#include
#endif
#ifdef HAVE_LINUX_CDROM_H
#include
#include
#endif
#if defined(_WIN32)
#include
#include
#endif
/* define in CFLAGS to skip drive certificate checks */
#ifndef PATCHED_DRIVE
#define PATCHED_DRIVE 0
#endif
#ifndef DEBUG_KEYS
#define DEBUG_KEYS 0
#endif
#if defined(_WIN32)
/*
* from ntddscsi.h, Windows DDK
*/
# define SCSI_IOCTL_DATA_OUT 0
# define SCSI_IOCTL_DATA_IN 1
# define SCSI_IOCTL_DATA_UNSPECIFIED 2
# define IOCTL_SCSI_PASS_THROUGH_DIRECT 0x4D014
# define MAX_SENSE_LEN 18
typedef struct _SCSI_PASS_THROUGH_DIRECT {
USHORT Length;
UCHAR ScsiStatus;
UCHAR PathId;
UCHAR TargetId;
UCHAR Lun;
UCHAR CdbLength;
UCHAR SenseInfoLength;
UCHAR DataIn;
ULONG DataTransferLength;
ULONG TimeOutValue;
PVOID DataBuffer;
ULONG SenseInfoOffset;
UCHAR Cdb[16];
} SCSI_PASS_THROUGH_DIRECT, *PSCSI_PASS_THROUGH_DIRECT;
#endif // defined(_WIN32)
struct mmc {
#if defined(_WIN32)
HANDLE fd;
#else
int fd;
#endif
uint8_t host_nonce[20];
uint8_t host_key[20];
uint8_t host_key_point[40];
};
static int _mmc_send_cmd(MMC *mmc, const uint8_t *cmd, uint8_t *buf, size_t tx,
size_t rx)
{
#if defined(HAVE_LINUX_CDROM_H)
if (mmc->fd >= 0) {
struct cdrom_generic_command cgc;
struct request_sense sense;
memset(&cgc, 0, sizeof(cgc));
memcpy(cgc.cmd, cmd, CDROM_PACKET_SIZE);
cgc.sense = &sense;
cgc.timeout = 5000;
if (buf) {
if (tx) {
cgc.data_direction = CGC_DATA_WRITE;
cgc.buflen = tx;
cgc.buffer = buf;
} else if (rx) {
cgc.data_direction = CGC_DATA_READ;
cgc.buflen = rx;
cgc.buffer = buf;
}
} else {
cgc.data_direction = CGC_DATA_NONE;
cgc.buflen = 0;
cgc.buffer = NULL;
}
int a = ioctl(mmc->fd, CDROM_SEND_PACKET, &cgc);
char str[512];
DEBUG(DBG_MMC, "Send LINUX MMC cmd %s: (%p)\n",
print_hex(str, cmd, 16), mmc);
if (tx) {
DEBUG(DBG_MMC, " Buffer: %s -> (%p)\n", print_hex(str, buf, tx), mmc);
} else {
DEBUG(DBG_MMC, " Buffer: %s <- (%p)\n", print_hex(str, buf, rx), mmc);
}
if (a >= 0) {
DEBUG(DBG_MMC, " Send succeeded! [%d] (%p)\n", a, mmc);
return 1;
}
DEBUG(DBG_MMC, " Send failed! [%d] %s (%p)\n", a, strerror(errno),
mmc);
}
#elif defined(_WIN32)
DWORD dwBytesReturned;
struct {
SCSI_PASS_THROUGH_DIRECT sptd;
UCHAR SenseBuf[MAX_SENSE_LEN];
} sptd_sb;
if (mmc->fd == INVALID_HANDLE_VALUE) {
return 0;
}
sptd_sb.sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
sptd_sb.sptd.PathId = 0;
sptd_sb.sptd.TargetId = 0;
sptd_sb.sptd.Lun = 0;
sptd_sb.sptd.CdbLength = 12;
sptd_sb.sptd.SenseInfoLength = MAX_SENSE_LEN;
sptd_sb.sptd.TimeOutValue = 5;
sptd_sb.sptd.SenseInfoOffset = sizeof(SCSI_PASS_THROUGH_DIRECT);
if (buf) {
if (tx) {
sptd_sb.sptd.DataIn = SCSI_IOCTL_DATA_OUT;
sptd_sb.sptd.DataTransferLength = tx;
sptd_sb.sptd.DataBuffer = buf;
} else if (rx) {
sptd_sb.sptd.DataIn = SCSI_IOCTL_DATA_IN;
sptd_sb.sptd.DataTransferLength = rx;
sptd_sb.sptd.DataBuffer = buf;
}
} else {
sptd_sb.sptd.DataIn = SCSI_IOCTL_DATA_UNSPECIFIED;
sptd_sb.sptd.DataTransferLength = 0;
sptd_sb.sptd.DataBuffer = NULL;
}
memcpy(sptd_sb.sptd.Cdb, cmd, 16);
ZeroMemory(sptd_sb.SenseBuf, MAX_SENSE_LEN);
if (DeviceIoControl(mmc->fd,
IOCTL_SCSI_PASS_THROUGH_DIRECT,
(void*)&sptd_sb, sizeof(sptd_sb),
(void*)&sptd_sb, sizeof(sptd_sb),
&dwBytesReturned, NULL)) {
if (sptd_sb.sptd.ScsiStatus == 0 /* STATUS_GOOD */) {
DEBUG(DBG_MMC, " Send succeeded! (%p)\n", mmc);
return 1;
}
}
DEBUG(DBG_MMC, " Send failed! (%p)\n", mmc);
#endif
return 0;
}
static int _mmc_report_key(MMC *mmc, uint8_t agid, uint32_t addr,
uint8_t blocks, uint8_t format, uint8_t *buf,
uint16_t len)
{
uint8_t cmd[16];
memset(cmd, 0, sizeof(cmd));
memset(buf, 0, len);
DEBUG(DBG_MMC, "MMC report key... (%p)\n", mmc);
cmd[0] = 0xa4;
cmd[2] = (addr >> 24) & 0xff;
cmd[3] = (addr >> 16) & 0xff;
cmd[4] = (addr >> 8) & 0xff;
cmd[5] = addr & 0xff;
cmd[6] = blocks;
cmd[7] = 0x02;
cmd[8] = (len >> 8) & 0xff;
cmd[9] = len & 0xff;
cmd[10] = (agid << 6) | (format & 0x3f);
return _mmc_send_cmd(mmc, cmd, buf, 0, len);
}
static int _mmc_send_key(MMC *mmc, uint8_t agid, uint8_t format, uint8_t *buf,
uint16_t len)
{
uint8_t cmd[16];
char str[512];
memset(cmd, 0, sizeof(cmd));
DEBUG(DBG_MMC, "MMC send key [%d] %s... (%p)\n", len, print_hex(str, buf, len),
mmc);
cmd[0] = 0xa3;
cmd[7] = 0x02;
cmd[8] = (len >> 8) & 0xff;
cmd[9] = len & 0xff;
cmd[10] = (agid << 6) | (format & 0x3f);
DEBUG(DBG_MMC, "cmd: %s (%p)\n", print_hex(str, cmd, 16), mmc);
return _mmc_send_cmd(mmc, cmd, buf, len, 0);
}
static int _mmc_invalidate_agid(MMC *mmc, uint8_t agid)
{
uint8_t buf[2];
memset(buf, 0, sizeof(buf));
return _mmc_report_key(mmc, agid, 0, 0, 0x3f, buf, 2);
}
static void _mmc_invalidate_agids(MMC *mmc)
{
int agid;
/* invalidate all agids */
for (agid = 0; agid < 4; agid++) {
_mmc_invalidate_agid(mmc, agid);
}
}
static int _mmc_report_agid(MMC *mmc, uint8_t *agid)
{
uint8_t buf[8];
memset(buf, 0, sizeof(buf));
int result = _mmc_report_key(mmc, 0, 0, 0, 0, buf, 8);
if (result) {
*agid = (buf[7] & 0xff) >> 6;
}
return result;
}
static int _mmc_send_host_cert(MMC *mmc, uint8_t agid,
const uint8_t *host_nonce,
const uint8_t *host_cert)
{
uint8_t buf[116];
memset(buf, 0, sizeof(buf));
buf[1] = 0x72;
memcpy(buf + 4, host_nonce, 20);
memcpy(buf + 24, host_cert, 92);
return _mmc_send_key(mmc, agid, 0x01, buf, 116);
}
static int _mmc_read_drive_cert(MMC *mmc, uint8_t agid, uint8_t *drive_nonce,
uint8_t *drive_cert)
{
uint8_t buf[116];
memset(buf, 0, sizeof(buf));
if (_mmc_report_key(mmc, agid, 0, 0, 0x01, buf, 116)) {
memcpy(drive_nonce, buf + 4, 20);
memcpy(drive_cert, buf + 24, 92);
return 1;
}
return 0;
}
static int _mmc_read_drive_key(MMC *mmc, uint8_t agid, uint8_t *drive_key_point,
uint8_t *drive_key_signature)
{
uint8_t buf[84];
memset(buf, 0, sizeof(buf));
if (_mmc_report_key(mmc, agid, 0, 0, 0x02, buf, 84)) {
memcpy(drive_key_point, buf + 4, 40);
memcpy(drive_key_signature, buf + 44, 40);
return 1;
}
return 0;
}
static int _mmc_send_host_key(MMC *mmc, uint8_t agid,
const uint8_t *host_key_point,
const uint8_t *host_key_signature)
{
uint8_t buf[84];
memset(buf, 0, sizeof(buf));
buf[1] = 0x52;
memcpy(buf + 4, host_key_point, 40);
memcpy(buf + 44, host_key_signature, 40);
return _mmc_send_key(mmc, agid, 0x02, buf, 84);
}
static int _mmc_read_vid(MMC *mmc, uint8_t agid, uint8_t *volume_id,
uint8_t *mac)
{
uint8_t buf[36];
uint8_t cmd[16];
memset(cmd, 0, sizeof(cmd));
memset(buf, 0, 36);
cmd[0] = 0xad;
cmd[1] = 1; // 1 = BLURAY
cmd[7] = 0x80;
cmd[9] = 0x24;
cmd[10] = (agid << 6) & 0xc0;
if (_mmc_send_cmd(mmc, cmd, buf, 0, 36)) {
memcpy(volume_id, buf + 4, 16);
memcpy(mac, buf + 20, 16);
return 1;
}
return 0;
}
MMC *mmc_open(const char *path)
{
MMC *mmc = calloc(1, sizeof(MMC));
crypto_create_nonce(mmc->host_nonce, sizeof(mmc->host_nonce));
if (DEBUG_KEYS) {
char str[sizeof(mmc->host_nonce)*2 + 1];
DEBUG(DBG_MMC, "Created host nonce (Hn): %s\n",
print_hex(str, mmc->host_nonce, sizeof(mmc->host_nonce)));
}
crypto_create_host_key_pair(mmc->host_key, mmc->host_key_point);
if (DEBUG_KEYS) {
char str[sizeof(mmc->host_key_point)*2 + 1];
DEBUG(DBG_MMC, "Created host key (Hk): %s\n",
print_hex(str, mmc->host_key, sizeof(mmc->host_key)));
DEBUG(DBG_MMC, "Created host key point (Hv): %s\n",
print_hex(str, mmc->host_key_point, sizeof(mmc->host_key_point)));
}
#if defined(HAVE_MNTENT_H)
#ifdef HAVE_REALPATH
char *file_path = malloc(PATH_MAX);
if (!file_path || !realpath(path, file_path)) {
DEBUG(DBG_MMC, "Failed resolving path %s (%p)\n", path, mmc);
X_FREE(mmc);
X_FREE(file_path);
return NULL;
}
#else
char *file_path = (char*)malloc(strlen(path) + 1);
strcpy(file_path, path);
#endif
int path_len = strlen(file_path);
FILE *proc_mounts;
mmc->fd = -1;
// strip trailing '/'s
while (path_len > 0 && file_path[--path_len] == '/') {
file_path[path_len] = '\0';
}
DEBUG(DBG_MMC, "Opening LINUX MMC drive %s... (%p)\n", file_path, mmc);
if ((proc_mounts = setmntent("/proc/mounts", "r"))) {
struct mntent* mount_entry;
while ((mount_entry = getmntent(proc_mounts)) != NULL) {
if (strcmp(mount_entry->mnt_dir, file_path) == 0) {
mmc->fd = open(mount_entry->mnt_fsname, O_RDONLY | O_NONBLOCK);
if (mmc->fd >= 0) {
DEBUG(DBG_MMC, "LINUX MMC drive %s opened - fd: %d (%p)\n",
mount_entry->mnt_fsname, mmc->fd, mmc);
break;
}
DEBUG(DBG_MMC, "Failed opening LINUX MMC drive %s mounted to %s (%p)\n",
mount_entry->mnt_fsname, file_path, mmc);
}
}
endmntent(proc_mounts);
} else {
DEBUG(DBG_MMC, "Error opening /proc/mounts (%p)\n", mmc);
}
if (mmc->fd < 0) {
DEBUG(DBG_MMC, "Error opening LINUX MMC drive mounted to %s (%p)\n", file_path, mmc);
X_FREE(mmc);
}
X_FREE(file_path);
#elif defined(_WIN32)
char drive[] = { path[0], ':', '\\', 0 };
char volume[] = {'\\', '\\', '.', '\\', path[0], ':', 0};
DEBUG(DBG_MMC, "Opening Windows MMC drive %s... (%p)\n", drive, mmc);
UINT type = GetDriveType(drive);
if (type != DRIVE_CDROM) {
DEBUG(DBG_MMC, "Drive %s is not CD/DVD drive !\n", drive);
X_FREE(mmc);
return NULL;
}
mmc->fd = CreateFile(volume, GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (mmc->fd == INVALID_HANDLE_VALUE) {
mmc->fd = CreateFile(volume, GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (mmc->fd == INVALID_HANDLE_VALUE) {
DEBUG(DBG_MMC, "Failed opening Windows MMC drive %s (%p)\n", volume, mmc);
X_FREE(mmc);
return NULL;
}
}
DEBUG(DBG_MMC, "Windows MMC drive %s opened (%p)\n", volume, mmc);
#else
DEBUG(DBG_MMC, "No MMC drive support !\n");
X_FREE(mmc);
#endif
return mmc;
}
void mmc_close(MMC *mmc)
{
if (mmc) {
#if defined(HAVE_LINUX_CDROM_H)
if (mmc->fd >= 0) {
close(mmc->fd);
}
#elif defined(_WIN32)
if (mmc->fd != INVALID_HANDLE_VALUE) {
CloseHandle(mmc->fd);
}
#endif
DEBUG(DBG_MMC, "Closed MMC drive (%p)\n", mmc);
X_FREE(mmc);
}
}
static int _verify_signature(const uint8_t *cert, const uint8_t *signature,
const uint8_t *nonce, const uint8_t *point)
{
uint8_t data[60];
memcpy(data, nonce, 20);
memcpy(data + 20, point, 40);
return crypto_aacs_verify(cert, signature, data, 60);
}
int mmc_read_vid(MMC *mmc, const uint8_t *host_priv_key, const uint8_t *host_cert, uint8_t *vid)
{
uint8_t agid = 0, hks[40], dn[20], dc[92], dkp[40], dks[40], mac[16];
char str[512];
int error_code = MMC_ERROR;
memset(hks, 0, sizeof(hks));
DEBUG(DBG_MMC, "Reading VID from drive... (%p)\n", mmc);
_mmc_invalidate_agids(mmc);
if (!_mmc_report_agid(mmc, &agid)) {
DEBUG(DBG_MMC | DBG_CRIT, "Didn't get AGID from drive (%p)\n", mmc);
return MMC_ERROR;
}
DEBUG(DBG_MMC, "Got AGID from drive: %d (%p)\n", agid, mmc);
if (!PATCHED_DRIVE) do {
if (DEBUG_KEYS) {
DEBUG(DBG_MMC, "Host certificate : %s (%p)\n", print_hex(str, host_cert, 92), mmc);
DEBUG(DBG_MMC, "Host nonce : %s (%p)\n", print_hex(str, mmc->host_nonce, 20), mmc);
}
// send host cert + nonce
if (!_mmc_send_host_cert(mmc, agid, mmc->host_nonce, host_cert)) {
DEBUG(DBG_MMC | DBG_CRIT,
"Host key / Certificate has been revoked by your drive ? "
"(%p)\n", mmc);
error_code = MMC_ERROR_CERT_REVOKED;
break;
}
// receive mmc cert + nonce
if (!_mmc_read_drive_cert(mmc, agid, dn, dc)) {
DEBUG(DBG_MMC | DBG_CRIT,
"Drive doesn't give its certificate (%p)\n", mmc);
break;
}
if (DEBUG_KEYS) {
DEBUG(DBG_MMC, "Drive certificate : %s (%p)\n", print_hex(str, dc, 92), mmc);
DEBUG(DBG_MMC, "Drive nonce : %s (%p)\n", print_hex(str, dn, 20), mmc);
}
// verify drive certificate
if (!crypto_aacs_verify_drive_cert(dc)) {
DEBUG(DBG_MMC | DBG_CRIT, "Drive certificate is invalid (%p)\n", mmc);
break;
}
// receive mmc key
if (!_mmc_read_drive_key(mmc, agid, dkp, dks)) {
DEBUG(DBG_MMC | DBG_CRIT, "Drive doesn't give its drive key (%p)\n",
mmc);
break;
}
if (DEBUG_KEYS) {
DEBUG(DBG_MMC, "Drive key point : %s (%p)\n", print_hex(str, dkp, 40), mmc);
DEBUG(DBG_MMC, "Drive key signature : %s (%p)\n", print_hex(str, dks, 40), mmc);
}
// verify drive signature
if (!_verify_signature(dc, dks, mmc->host_nonce, dkp)) {
DEBUG(DBG_MMC | DBG_CRIT, "Drive signature is invalid\n");
break;
}
// sign
crypto_aacs_sign(host_cert, host_priv_key, hks, dn,
mmc->host_key_point);
// verify own signature
if (!_verify_signature(host_cert, hks, dn, mmc->host_key_point)) {
DEBUG(DBG_MMC | DBG_CRIT, "Created signature is invalid ?\n");
break;
}
// send signed host key and point
if (!_mmc_send_host_key(mmc, agid, mmc->host_key_point, hks)) {
DEBUG(DBG_MMC | DBG_CRIT, "Error sending host signature (%p)\n",
mmc);
DEBUG(DBG_MMC, "Host key signature : %s (%p)\n",
print_hex(str, hks, 40), mmc);
break;
}
} while (0);
if (_mmc_read_vid(mmc, agid, vid, mac)) {
DEBUG(DBG_MMC, "VID: %s (%p)\n", print_hex(str, vid, 16), mmc);
DEBUG(DBG_MMC, "MAC: %s (%p)\n", print_hex(str, mac, 16), mmc);
_mmc_invalidate_agid(mmc, agid);
return MMC_SUCCESS;
}
DEBUG(DBG_MMC | DBG_CRIT, "Unable to read VID from drive! (%p)\n", mmc);
_mmc_invalidate_agid(mmc, agid);
return error_code;
}