UNPKG

shifro

Version:

A lightweight, dependency-free MP4 decrypter

16 lines (14 loc) 15.7 kB
#!/usr/bin/env node "use strict";var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));const c=s(require(`node:util`)),l=s(require(`node:fs`)),u=s(require(`node:fs/promises`)),d=s(require(`node:stream`)),f=async({key:e,iv:t,data:n,algorithm:r=`AES-CTR`})=>{let i=r.toUpperCase(),a=await crypto.subtle.importKey(`raw`,e,{name:i},!1,[`decrypt`]),o=await crypto.subtle.decrypt({name:i,counter:t,length:64},a,n);return new Uint8Array(o)},p=e=>{let t=e.buffer,n=new DataView(t,e.byteOffset,e.byteLength);return n},m=(e,t=0,n)=>new Uint8Array(e.buffer,e.byteOffset+t,n);var h=class extends Error{constructor(e,t,n,r){super(r),this.severity=e,this.category=t,this.code=n,this.name=`DataViewReaderError`}},g=class{position=0;constructor(e,t=`BE`){this.dataView=p(e),this.littleEndian=t===`LE`}getDataView(){return this.dataView}hasMoreData(){return this.position<this.dataView.byteLength}getPosition(){return this.position}getLength(){return this.dataView.byteLength}readUint8(){this.checkBounds(1);let e=this.dataView.getUint8(this.position);return this.position+=1,e}readUint16(){this.checkBounds(2);let e=this.dataView.getUint16(this.position,this.littleEndian);return this.position+=2,e}readUint32(){this.checkBounds(4);let e=this.dataView.getUint32(this.position,this.littleEndian);return this.position+=4,e}readInt32(){this.checkBounds(4);let e=this.dataView.getInt32(this.position,this.littleEndian);return this.position+=4,e}readUint64(){let e,t;try{this.littleEndian?(e=this.dataView.getUint32(this.position,!0),t=this.dataView.getUint32(this.position+4,!0)):(t=this.dataView.getUint32(this.position,!1),e=this.dataView.getUint32(this.position+4,!1))}catch{throw this.outOfBounds()}if(t>2097151)throw new h(`CRITICAL`,`MEDIA`,`JS_INTEGER_OVERFLOW`,`64-bit integer overflow`);return this.position+=8,t*2**32+e}readBytes(e){this.checkBounds(e);let t=m(this.dataView,this.position,e);return this.position+=e,t}skip(e){this.checkBounds(e),this.position+=e}rewind(e){if(this.position<e)throw this.outOfBounds();this.position-=e}seek(e){if(e<0||e>this.dataView.byteLength)throw this.outOfBounds();this.position=e}readTerminatedString(){let e=this.position;for(;this.hasMoreData()&&this.dataView.getUint8(this.position)!==0;)this.position++;if(!this.hasMoreData())throw this.outOfBounds();let t=new Uint8Array(this.dataView.buffer,e,this.position-e);return this.position++,new TextDecoder().decode(t)}checkBounds(e){if(this.position+e>this.dataView.byteLength)throw this.outOfBounds()}outOfBounds(){return new h(`CRITICAL`,`MEDIA`,`BUFFER_READ_OUT_OF_BOUNDS`,`Read operation out of bounds`)}};let _=function(e){return e[e.BASIC_BOX=0]=`BASIC_BOX`,e[e.FULL_BOX=1]=`FULL_BOX`,e}({});var v=class e{headers=new Map;boxDefinitions=new Map;done=!1;box(t,n){let r=e.typeFromString(t);return this.headers.set(r,_.BASIC_BOX),this.boxDefinitions.set(r,n),this}fullBox(t,n){let r=e.typeFromString(t);return this.headers.set(r,_.FULL_BOX),this.boxDefinitions.set(r,n),this}stop(){this.done=!0}parse(e,t=!1,n=!1){let r=new g(e);for(this.done=!1;r.hasMoreData()&&!this.done;)this.parseNext(0,r,t,n)}parseNext(t,n,r,i){let a=n.getPosition();if(i&&a+8>n.getLength()){this.done=!0;return}let o=n.readUint32(),s=n.readUint32(),c=e.typeToString(s),l=!1;switch(o){case 0:o=n.getLength()-a;break;case 1:if(i&&n.getPosition()+8>n.getLength()){this.done=!0;return}o=Number(n.readUint64()),l=!0;break}let u=this.boxDefinitions.get(s);if(u){let e=null,d=null;if(this.headers.get(s)===_.FULL_BOX){if(i&&n.getPosition()+4>n.getLength()){this.done=!0;return}let t=n.readUint32();e=t>>>24,d=t&16777215}let f=a+o;if(r&&f>n.getLength()&&(f=n.getLength()),i&&f>n.getLength()){this.done=!0;return}let p=f-n.getPosition(),m=p>0?n.readBytes(p):new Uint8Array,h=new g(m),v={name:c,parser:this,partialOkay:!!r,version:e,flags:d,reader:h,size:o,start:a+t,has64BitSize:l};u(v)}else{let e=Math.min(a+o-n.getPosition(),n.getLength()-n.getPosition());n.skip(e)}}static children(t){let n=e.headerSize(t);for(;t.reader.hasMoreData()&&!t.parser.done;)t.parser.parseNext(t.start+n,t.reader,t.partialOkay)}static sampleDescription(t){let n=e.headerSize(t),r=t.reader.readUint32();for(let e=0;e<r&&(t.parser.parseNext(t.start+n,t.reader,t.partialOkay),!t.parser.done);e++);}static visualSampleEntry(t){let n=e.headerSize(t);for(t.reader.skip(78);t.reader.hasMoreData()&&!t.parser.done;)t.parser.parseNext(t.start+n,t.reader,t.partialOkay)}static audioSampleEntry(t){let n=e.headerSize(t);t.reader.skip(8);let r=t.reader.readUint16();for(t.reader.skip(6),r===2?t.reader.skip(48):t.reader.skip(12),r===1&&t.reader.skip(16);t.reader.hasMoreData()&&!t.parser.done;)t.parser.parseNext(t.start+n,t.reader,t.partialOkay)}static allData(e){return t=>{let n=t.reader.getLength()-t.reader.getPosition();e(t.reader.readBytes(n))}}static typeFromString(e){if(e.length!==4)throw Error(`MP4 box names must be 4 characters long`);let t=0;for(let n of e)t=t<<8|n.charCodeAt(0);return t}static typeToString(e){return String.fromCharCode(e>>24&255,e>>16&255,e>>8&255,e&255)}static headerSize(e){let t=8,n=e.has64BitSize?8:0,r=e.flags===null?0:4;return t+n+r}};const ee=(e,t)=>{if(t&&t&1){let t=e.readUint8(),n=e.readUint8(),r=e.readBytes(16)}let n=e.readUint32(),r=[];for(let i=0;i<n;i++){let n=8,i=new Uint8Array(16);i.set(e.readBytes(n));let a={iv:i,subsamples:[]},o=t&&t&2;if(o){let t=e.readUint16();for(let n=0;n<t;n++){let t=e.readUint16(),n=e.readUint32();a.subsamples.push({bytesOfClearData:t,bytesOfEncryptedData:n})}}r.push(a)}return{samples:r}},y=(e,t,n)=>{let r=e.readUint32(),i=[],a=null;t&1&&(a=e.readInt32()),t&4&&e.skip(4);for(let a=0;a<r;a++){let r={};t&256&&(r.duration=e.readUint32()),t&512&&(r.size=e.readUint32()),t&1024&&e.skip(4),t&2048&&(r.compositionTimeOffset=n==0?e.readUint32():e.readInt32()),i.push(r)}return{samples:i,dataOffset:a}},te=(e,t)=>{let n,r,i,a,o=e.readUint32();return t&1&&(i=e.readUint64()),t&2&&(a=e.readUint32()),t&8&&(n=e.readUint32()),t&16&&(r=e.readUint32()),{trackId:o,defaultSampleDuration:n,defaultSampleSize:r,baseDataOffset:i,sampleDescriptionIndex:a}},b=(e,t,n)=>{if(t.length===0)return-1;let r=e.length-t.length+1;for(let i=n;i<r;i++){let n=!0;for(let r=0;r<t.length;r++)if(e[i+r]!==t[r]){n=!1;break}if(n)return i}return-1},x=(e,t,n)=>{let r=new TextEncoder,i=r.encode(t),a=r.encode(n);if(i.length!==a.length)throw Error(`Original and replacement must have the same byte length`);let o=0;do o=b(e,i,0),o!==-1&&e.set(a,o);while(o!==-1)},S=(e,t,n=0)=>{let r=new TextEncoder,i;return i=typeof t==`string`?r.encode(t):typeof t==`number`?new Uint8Array([t]):t,i.length===0?!0:e.length<i.length?!1:b(e,i,n)!==-1},C=(e,t)=>{if(e.length===0)return new Uint8Array;let n=t??e.reduce((e,t)=>e+t.length,0);n=Math.min(n,e.reduce((e,t)=>e+t.length,0));let r=new Uint8Array(n),i=0;for(let t of e){if(i>=n)break;let e=t.subarray(0,Math.min(t.length,n-i));r.set(e,i),i+=e.length}return r},ne=(e,t,n=0,r=0,i=e.length)=>{n=Math.max(0,Math.min(n,t.length)),r=Math.max(0,Math.min(r,e.length)),i=Math.min(i,e.length),i<r&&(i=r);let a=Math.min(i-r,t.length-n);return a<=0?0:(t.set(e.subarray(r,r+a),n),a)},re=(e,t,n=0,r)=>{let i=new TextEncoder,a=i.encode(t),o=Math.max(0,Math.min(n,e.length)),s=e.length-o,c=a.length;return typeof r==`number`&&(c=Math.min(c,r)),c=Math.min(c,s),c<=0?0:(e.set(a.subarray(0,c),o),c)},w=e=>Array.from(e).map(e=>e.toString(16).padStart(2,`0`)).join(``),ie=e=>{let t=Array.from(e,e=>String.fromCodePoint(e)).join(``);return btoa(t)},ae=e=>e.match(/.{1,2}/g)?.map(e=>parseInt(e,16))??[],T=e=>{e.readUint8(),e.readUint8(),e.readUint8(),e.readUint8();let t=w(e.readBytes(16));return{defaultKID:t}},E=e=>{let t=`edef8ba979d64acea3c827dcd51d21ed`,n=e.version??0;if(n!==0&&n!==1)throw Error(`PSSH version can only be 0 or 1`);let r=e.reader.readBytes(16),i=w(r),a=[i.slice(0,8),i.slice(8,12),i.slice(12,16),i.slice(16,20),i.slice(20)].join(`-`),o=e.reader.getDataView(),s=new Uint8Array(o.buffer,o.byteOffset-12,e.size),c=ie(s),l=[];if(n>=1){let t=e.reader.readUint32();for(let n=0;n<t;n++)l.push(w(e.reader.readBytes(16)))}let u=e.reader.readUint32(),d=e.reader.readBytes(u),f=i===t;return{version:n,systemId:a,pssh:c,keyIds:l,systemData:d}},D=e=>{try{let t=!1,n=!1;return new v().box(`moov`,()=>{t=!0}).box(`moof`,()=>{n=!0}).parse(e,!0,!0),t&&!n}catch(e){return console.log(e),!1}},O=e=>{let t=null;return new v().box(`moov`,v.children).box(`trak`,v.children).box(`mdia`,v.children).box(`minf`,v.children).box(`stbl`,v.children).fullBox(`stsd`,v.sampleDescription).box(`encv`,v.visualSampleEntry).box(`enca`,v.audioSampleEntry).box(`sinf`,v.children).box(`frma`,e=>{let n=e.reader.readBytes(4);t=new TextDecoder().decode(n)}).parse(e,!0,!0),t},k=async e=>{let t=O(e);if(x(e,`sinf`,`skip`),x(e,`pssh`,`skip`),t)x(e,`encv`,t),x(e,`enca`,t);else{console.warn(`Could not detect original codec, using fallback values`);let t=S(e,`encv`)?`video`:`audio`;t===`video`?x(e,`encv`,`avc1`):x(e,`enca`,`mp4a`)}return e},A=e=>{let t={schemeType:``,defaultKID:``,psshList:[]};return new v().box(`moov`,v.children).box(`trak`,v.children).box(`mdia`,v.children).box(`minf`,v.children).box(`stbl`,v.children).fullBox(`stsd`,v.sampleDescription).fullBox(`pssh`,e=>t.psshList.push(E(e))).box(`encv`,v.visualSampleEntry).box(`enca`,v.audioSampleEntry).box(`sinf`,v.children).box(`schi`,v.children).fullBox(`schm`,e=>{t.schemeType=new TextDecoder().decode(e.reader.readBytes(4))}).fullBox(`tenc`,e=>{let{defaultKID:n}=T(e.reader);t.defaultKID=n}).parse(e,!1,!0),t},j=async(e,t)=>{let n=D(e);if(n)return k(e);let r,i,a,o;if(new v().box(`moof`,v.children).box(`traf`,v.children).fullBox(`tfhd`,e=>{a=te(e.reader,e.flags)}).fullBox(`trun`,e=>{i=y(e.reader,e.flags,e.version)}).fullBox(`senc`,e=>{r=ee(e.reader,e.flags)}).parse(e,!0,!0),o=i.dataOffset,r.samples.length!==i.samples.length)throw Error(`sample count mismatch: trun has ${i.samples.length}, senc has ${r.samples.length}`);let s=0,c=0;for(let n=0;n<r.samples.length;n++){let l=r.samples[n],u=i.samples[n],d=i.samples[n].size||a.defaultSampleSize||0;l.subsamples.length||l.subsamples.push({bytesOfClearData:0,bytesOfEncryptedData:d});let f=l.subsamples.some(e=>e.bytesOfEncryptedData>0);if(f){let n=0,r=[];for(let t of l.subsamples){if(n+=t.bytesOfClearData,t.bytesOfEncryptedData>0){let i=e.subarray(o+s+n,o+s+n+t.bytesOfEncryptedData);r.push(i)}n+=t.bytesOfEncryptedData}let i=C(r),a={iv:l.iv,data:i,timestamp:c},u=await t(a);if(u){n=0;let t=0,r=[];for(let i of l.subsamples){if(i.bytesOfClearData>0){let t=e.subarray(o+s+n,o+s+n+i.bytesOfClearData);r.push(t),n+=i.bytesOfClearData}if(i.bytesOfEncryptedData>0){let e=u.subarray(t,t+i.bytesOfEncryptedData);r.push(e),t+=i.bytesOfEncryptedData,n+=i.bytesOfEncryptedData}}let i=C(r);ne(i,e,o+s)}}s+=d,c+=u.duration||a.defaultSampleDuration||0}return new v().box(`moof`,v.children).box(`traf`,v.children).box(`senc`,t=>{let n=`skip`,r=t.start+n.length;re(e,n,r)}).parse(e,!0,!0),e},M=e=>{let t=null;new v().box(`moov`,e=>{t=e.start+e.size}).parse(e,!0,!0);let n=t?e.subarray(0,t):null;return{moovEnd:t,init:n}},N=e=>{let t=null,n=null;new v().box(`moof`,e=>{t===null&&(t=e.start)}).box(`mdat`,e=>{n===null&&(n=e.start+e.size)}).parse(e,!0,!0);let r=t!==null&&n!==null,i=r?e.subarray(t,n):null;return{segment:i,moofStart:t,mdatEnd:n}};var P=class{buffer=new Uint8Array;isProcessingInit=!0;scheme=null;processedBytes=0;constructor(e){this.options=e}async transform(e,t){try{this.buffer=C([this.buffer,e]);let n=!0;for(;n;){if(this.isProcessingInit){let{init:e,moovEnd:n}=M(this.buffer);if(!n||this.buffer.length<n)break;if(e&&D(e)){let r=A(e);this.scheme=r.schemeType;let i=await k(e);t.enqueue(i),this.updateProgress(i.byteLength),this.buffer=this.buffer.subarray(n),this.isProcessingInit=!1;continue}}let{moofStart:e,mdatEnd:r,segment:i}=N(this.buffer);if(e===null||r===null||!i||this.buffer.length<r)break;let a=e=>(e.encryptionScheme=this.scheme,this.options.transformSample(e)),o=await j(i,a);t.enqueue(o),this.updateProgress(o.byteLength),this.buffer=this.buffer.subarray(r),n=this.buffer.length>=8}}catch(e){t.error(e)}}async flush(e){this.buffer.length>0&&(e.enqueue(this.buffer),this.updateProgress(this.buffer.byteLength))}updateProgress(e){this.processedBytes+=e,this.options.onProgress&&this.options.onProgress(this.processedBytes)}};const F=async(e,t,{transformSample:n,onProgress:r,preventClose:i})=>{let a=new P({transformSample:n,onProgress:r}),o=new TransformStream({transform:(e,t)=>a.transform(e,t),flush:e=>a.flush(e)});await e.pipeThrough(o).pipeTo(t,{preventClose:i})},I=async(e,t)=>{let n=t.encryptionScheme,r=t.iv,i=t.data;return f(n===`cbcs`?{key:e,iv:r,data:i,algorithm:`AES-CBC`}:{key:e,iv:r,data:i,algorithm:`AES-CTR`})},L=async(e,t,{preventClose:n,onProgress:r,...i})=>{let a=`key`in i,o=new Uint8Array(ae(a?i.key:``)),s=async e=>I(o,e),c=a?s:i.transformSample;await F(e,t,{preventClose:n,onProgress:r,transformSample:c})},R=async(e,t,n)=>{let r=await(0,u.stat)(e),i=(0,l.createReadStream)(e,{highWaterMark:1024*1024*10}),a=d.Readable.toWeb(i),o=(0,l.createWriteStream)(t),s=d.Writable.toWeb(o);console.time(` Done!`),await L(a,s,{key:`key`in n?n.key:``,keyId:n.keyId,onProgress:e=>{process.stdout.write(`\rDecrypting... [${e}/${r.size}]`),e===r.size&&process.stdout.write(` `)}}),console.timeEnd(` Done!`)};var z=`shifro`,B=`0.1.2`,V=`A lightweight, dependency-free MP4 decrypter`,H=[`dist`],U={start:`node ./dist/cli.cjs`,web:`vite`,build:`npm run build:lib && npm run build:cli`,"build:cli":`tsdown ./lib/node/cli.ts --format cjs --dts --minify --no-clean`,"build:lib":`tsdown shifro.ts --format cjs --format esm --dts --minify`,typecheck:`tsc --noEmit`,test:`vitest`,prepublishOnly:`npm run build`},W={shifro:`./dist/cli.cjs`},oe=`module`,G=`./dist/shifro.js`,K=`./dist/shifro.d.ts`,q={".":{require:{types:`./dist/shifro.d.cts`,default:`./dist/shifro.cjs`},import:{types:`./dist/shifro.d.ts`,default:`./dist/shifro.js`}}},se={type:`git`,url:`git+https://github.com/streamyx-labs/shifro.git`},ce=[`mp4`,`mpeg`,`cenc`,`cbcs`,`decrypt`,`decryption`,`decrypter`,`decryptor`,`dash`,`drm`,`mp4decrypt`,`mp4box`,`shaka`,`shaka-packager`],le={url:`https://github.com/streamyx-labs/shifro/issues`,email:`vitalygashkov@vk.com`},ue=`Vitaly Gashkov <vitalygashkov@vk.com>`,de=`AGPL-3.0`,J=`README.md`,fe=[{type:`individual`,url:`https://boosty.to/vitalygashkov`},{type:`patreon`,url:`https://www.patreon.com/vitalygashkov`}],pe={node:`20 || 21 || 22 || 23`},me={"@types/node":`^22.15.3`,tsdown:`^0.11.0-beta.3`,typescript:`^5.8.3`,vitest:`^3.1.2`},he={name:z,version:B,description:V,files:H,scripts:U,bin:W,type:oe,main:G,types:K,exports:q,repository:se,keywords:ce,bugs:le,author:ue,license:de,readmeFilename:J,funding:fe,engines:pe,devDependencies:me};const Y=(0,c.parseArgs)({allowPositionals:!0,strict:!1,options:{key:{short:`k`,type:`string`,multiple:!0},help:{short:`h`,type:`boolean`}}}),ge=Array.isArray(Y.values.key)?Y.values.key:[],[X,Z]=ge[0]?.split(`:`)??[],[Q,$]=Y.positionals;(async()=>{Q&&$&&X&&Z?await R(Q,$,{key:Z,keyId:X}):console.log(` shifro version ${he.version} (c) 2024 Vitaly Gashkov <vitalygashkov@vk.com> Usage: shifro [options] <input> <output> Options: --key <id>:<k> <id> is either a track ID in decimal or a 128-bit KID in hex, <k> is a 128-bit key in hex (several --key options can be used, one for each track or KID) `.trim())})();