avformat/crypto: add seeking support on read
authorSimon Hailes <btsimonh@googlemail.com>
Tue, 30 Aug 2016 08:40:14 +0000 (09:40 +0100)
committerMichael Niedermayer <michael@niedermayer.cc>
Tue, 30 Aug 2016 15:14:28 +0000 (17:14 +0200)
cyrpto allows reading of data which has been aes-128-cbc encrypted given a key and an iv. But it did not handle filetypes which require seeking... e.g. it failed on an encrypted .mp4 file.

example:
take 25.mp4 created with:
ffmpeg -f lavfi -i sine=frequency=1000:beep_factor=2:r=48000:duration=720.0 -f lavfi -i testsrc=duration=720.0:rate=25 -vcodec libx264 -cmp 22 -timecode 10:00:00:00 -r 25 -y out\25.mp4

encrypt with:
openssl enc -aes-128-cbc -K 12345678901234567890123456789012 -iv 12345678901234567890123456789012 -in 25.mp4 -out 25.enc
then to transcode in ffmpeg:
ffmpeg -key 12345678901234567890123456789012 -iv 12345678901234567890123456789012 -i crypto:25.enc -vcodec mpeg4 -r 25 -y 25dec.mp4

prior to this modification, the transcode would fail.

Note also:  crypto previously maked both reads and writes as streamed, which caused the whole file
to be read before the transcode started.  Now, for read only, if the underlying layer is not marked as streamed,
then crypto is not.  This should enable efficient reading of encrypted containers which require seeking.

Signed-off-by: Michael Niedermayer <michael@niedermayer.cc>
libavformat/crypto.c

index 31a438c..346c706 100644 (file)
@@ -37,6 +37,8 @@ typedef struct CryptoContext {
             outbuffer[BLOCKSIZE*MAX_BUFFER_BLOCKS];
     uint8_t *outptr;
     int indata, indata_used, outdata;
+    int64_t position;  // position in file - used in seek
+    int flags;
     int eof;
     uint8_t *key;
     int keylen;
@@ -110,6 +112,7 @@ static int crypto_open2(URLContext *h, const char *uri, int flags, AVDictionary
     const char *nested_url;
     int ret = 0;
     CryptoContext *c = h->priv_data;
+    c->flags = flags;
 
     if (!av_strstart(uri, "crypto+", &nested_url) &&
         !av_strstart(uri, "crypto:", &nested_url)) {
@@ -118,6 +121,8 @@ static int crypto_open2(URLContext *h, const char *uri, int flags, AVDictionary
         goto err;
     }
 
+    c->position = 0;
+
     if (flags & AVIO_FLAG_READ) {
         if ((ret = set_aes_arg(c, &c->decrypt_key, &c->decrypt_keylen,
                                c->key, c->keylen, "decryption key")) < 0)
@@ -153,6 +158,10 @@ static int crypto_open2(URLContext *h, const char *uri, int flags, AVDictionary
         ret = av_aes_init(c->aes_decrypt, c->decrypt_key, BLOCKSIZE*8, 1);
         if (ret < 0)
             goto err;
+
+        // pass back information about the context we openned
+        if (c->hd->is_streamed)
+            h->is_streamed = c->hd->is_streamed;
     }
 
     if (flags & AVIO_FLAG_WRITE) {
@@ -164,12 +173,13 @@ static int crypto_open2(URLContext *h, const char *uri, int flags, AVDictionary
         ret = av_aes_init(c->aes_encrypt, c->encrypt_key, BLOCKSIZE*8, 0);
         if (ret < 0)
             goto err;
+        // for write, we must be streamed
+        // - linear write only for crytpo aes-128-cbc
+        h->is_streamed = 1;
     }
 
     c->pad_len = 0;
 
-    h->is_streamed = 1;
-
 err:
     return ret;
 }
@@ -184,6 +194,7 @@ retry:
         memcpy(buf, c->outptr, size);
         c->outptr  += size;
         c->outdata -= size;
+        c->position = c->position + size;
         return size;
     }
     // We avoid using the last block until we've found EOF,
@@ -223,6 +234,106 @@ retry:
     goto retry;
 }
 
+static int64_t crypto_seek(URLContext *h, int64_t pos, int whence)
+{
+    CryptoContext *c = h->priv_data;
+    int64_t block;
+    int64_t newpos;
+
+    if (c->flags & AVIO_FLAG_WRITE) {
+        av_log(h, AV_LOG_ERROR,
+            "Crypto: seek not supported for write\r\n");
+        /* seems the most appropriate error to return */
+        return AVERROR(ESPIPE);
+    }
+
+    // reset eof, else we won't read it correctly if we already hit eof.
+    c->eof = 0;
+
+    switch (whence) {
+    case SEEK_SET:
+        break;
+    case SEEK_CUR:
+        pos = pos + c->position;
+        break;
+    case SEEK_END: {
+        int64_t newpos = ffurl_seek( c->hd, pos, AVSEEK_SIZE );
+        if (newpos < 0) {
+            av_log(h, AV_LOG_ERROR,
+                "Crypto: seek_end - can't get file size (pos=%lld)\r\n", (long long int)pos);
+            return newpos;
+        }
+        pos = newpos - pos;
+        }
+        break;
+    case AVSEEK_SIZE: {
+        int64_t newpos = ffurl_seek( c->hd, pos, AVSEEK_SIZE );
+        return newpos;
+        }
+        break;
+    default:
+        av_log(h, AV_LOG_ERROR,
+            "Crypto: no support for seek where 'whence' is %d\r\n", whence);
+        return AVERROR(EINVAL);
+    }
+
+    c->outdata = 0;
+    c->indata = 0;
+    c->indata_used = 0;
+    c->outptr = c->outbuffer;
+
+    // identify the block containing the IV for the
+    // next block we will decrypt
+    block = pos/BLOCKSIZE;
+    if (block == 0) {
+        // restore the iv to the seed one - this is the iv for the FIRST block
+        memcpy( c->decrypt_iv, c->iv, c->ivlen );
+        c->position = 0;
+    } else {
+        // else, go back one block - we will get av_cyrpt to read this block
+        // which it will then store use as the iv.
+        // note that the DECRYPTED result will not be correct,
+        // but will be discarded
+        block--;
+        c->position = (block * BLOCKSIZE);
+    }
+
+    newpos = ffurl_seek( c->hd, c->position, SEEK_SET );
+    if (newpos < 0) {
+        av_log(h, AV_LOG_ERROR,
+            "Crypto: nested protocol no support for seek or seek failed\n");
+        return newpos;
+    }
+
+    // read and discard from here up to required position
+    // (which will set the iv correctly to it).
+    if (pos - c->position) {
+        uint8_t buff[BLOCKSIZE*2]; // maximum size of pos-c->position
+        int len = pos - c->position;
+        int res;
+
+        while (len > 0) {
+            // note: this may not return all the bytes first time
+            res = crypto_read(h, buff, len);
+            if (res < 0)
+                break;
+            len -= res;
+        }
+
+        // if we did not get all the bytes
+        if (len != 0) {
+            char errbuf[100] = "unknown error";
+            av_strerror(res, errbuf, sizeof(errbuf));
+            av_log(h, AV_LOG_ERROR,
+                "Crypto: discard read did not get all the bytes (%d remain) - read returned (%d)-%s\n",
+                len, res, errbuf);
+            return AVERROR(EINVAL);
+        }
+    }
+
+    return c->position;
+}
+
 static int crypto_write(URLContext *h, const unsigned char *buf, int size)
 {
     CryptoContext *c = h->priv_data;
@@ -288,6 +399,7 @@ static int crypto_close(URLContext *h)
 const URLProtocol ff_crypto_protocol = {
     .name            = "crypto",
     .url_open2       = crypto_open2,
+    .url_seek        = crypto_seek,
     .url_read        = crypto_read,
     .url_write       = crypto_write,
     .url_close       = crypto_close,