diff --git a/pywidevine/main.py b/pywidevine/main.py
index 60efd00..83cab13 100644
--- a/pywidevine/main.py
+++ b/pywidevine/main.py
@@ -1,14 +1,17 @@
 import logging
 from datetime import datetime
 from pathlib import Path
+from typing import Optional
+from zlib import crc32
 
 import click
 import requests
+from unidecode import unidecode, UnidecodeError
 
 from pywidevine import __version__
 from pywidevine.cdm import Cdm
 from pywidevine.device import Device
-from pywidevine.license_protocol_pb2 import LicenseType
+from pywidevine.license_protocol_pb2 import LicenseType, FileHashes
 
 
 @click.group(invoke_without_command=True)
@@ -150,3 +153,85 @@ def test(ctx: click.Context, device: Path):
         type_=LicenseType.Name(license_type),
         raw=raw
     )
+
+
+@main.command()
+@click.option("-t", "--type", "type_", type=click.Choice([x.name for x in Device.Types], case_sensitive=False),
+              required=True, help="Device Type")
+@click.option("-l", "--level", type=click.IntRange(1, 3), required=True, help="Device Security Level")
+@click.option("-k", "--key", type=Path, required=True, help="Device RSA Private Key in PEM or DER format")
+@click.option("-c", "--client_id", type=Path, required=True, help="Widevine ClientIdentification Blob file")
+@click.option("-v", "--vmp", type=Path, required=True, help="Widevine FileHashes Blob file")
+@click.option("-o", "--output", type=Path, default=None, help="Output Directory")
+@click.pass_context
+def create_device(
+    ctx: click.Context,
+    type_: str,
+    level: int,
+    key: Path,
+    client_id: Path,
+    vmp: Optional[Path] = None,
+    output: Optional[Path] = None
+) -> None:
+    """
+    Create a Widevine Device (.wvd) file from an RSA Private Key (PEM or DER) and Client ID Blob.
+    Optionally also a VMP (Verified Media Path) Blob, which will be stored in the Client ID.
+    The Name argument should be the Device name corresponding to the provided data. E.g., "Nexus 6".
+    It's only used for the output filename.
+    """
+    if not key.is_file():
+        raise click.UsageError("key: Not a path to a file, or it doesn't exist.", ctx)
+    if not client_id.is_file():
+        raise click.UsageError("client_id: Not a path to a file, or it doesn't exist.", ctx)
+    if vmp and not vmp.is_file():
+        raise click.UsageError("vmp: Not a path to a file, or it doesn't exist.", ctx)
+
+    log = logging.getLogger("create-device")
+
+    device = Device(
+        type_=Device.Types[type_.upper()],
+        security_level=level,
+        flags=None,
+        private_key=key.read_bytes(),
+        client_id=client_id.read_bytes()
+    )
+
+    if vmp:
+        new_vmp_data = vmp.read_bytes()
+        if device.client_id.vmp_data and device.client_id.vmp_data != new_vmp_data:
+            log.warning("Client ID already has Verified Media Path data")
+        device.client_id.vmp_data = new_vmp_data
+
+    client_info = {}
+    for entry in device.client_id.client_info:
+        client_info[entry.name] = entry.value
+
+    wvd_bin = device.dumps()
+
+    name = f"{client_info['company_name']} {client_info['model_name']}"
+    if client_info.get("widevine_cdm_version"):
+        name += f" {client_info['widevine_cdm_version']}"
+    name += f" {crc32(wvd_bin).to_bytes(4, 'big').hex()}"
+
+    try:
+        name = unidecode(name.strip().lower().replace(" ", "_"))
+    except UnidecodeError as e:
+        raise click.ClickException(f"Failed to sanitize name, {e}")
+
+    out_path = (output or Path.cwd()) / f"{name}_{device.system_id}_l{device.security_level}.wvd"
+    out_path.write_bytes(wvd_bin)
+
+    log.info(f"Created Widevine Device (.wvd) file, {out_path.name}")
+    log.info(f" + Type: {device.type.name}")
+    log.info(f" + System ID: {device.system_id}")
+    log.info(f" + Security Level: {device.security_level}")
+    log.info(f" + Flags: {device.flags}")
+    log.info(f" + Private Key: {bool(device.private_key)} ({device.private_key.size_in_bits()} bit)")
+    log.info(f" + Client ID: {bool(device.client_id)} ({len(device.client_id.SerializeToString())} bytes)")
+    if device.client_id.vmp_data:
+        file_hashes_ = FileHashes()
+        file_hashes_.ParseFromString(device.client_id.vmp_data)
+        log.info(f" + VMP: True ({len(file_hashes_.signatures)} signatures)")
+    else:
+        log.info(f" + VMP: False")
+    log.info(f" + Saved to: {out_path.absolute()}")