├── .gitignore ├── extra ├── uglifyexcept.json ├── README.md ├── f5extract.min.js └── f5extract.js ├── uglifyexcept.json ├── package.json ├── Gruntfile.js ├── bin ├── stegodctdump ├── stegodcthist └── f5stego ├── LICENSE.txt ├── README.md ├── f5stego.min.js └── f5stego.js /.gitignore: -------------------------------------------------------------------------------- 1 | temp/ 2 | tmp/ 3 | node_modules/ 4 | *.jpeg 5 | *.jpg 6 | -------------------------------------------------------------------------------- /extra/uglifyexcept.json: -------------------------------------------------------------------------------- 1 | { 2 | "vars": ["f5extract"], 3 | "props": ["subarray", "set", "push", "splice", "toFixed", "slice", "length", "ceil"] 4 | } 5 | 6 | -------------------------------------------------------------------------------- /uglifyexcept.json: -------------------------------------------------------------------------------- 1 | { 2 | "vars": [ "define", "module", "f5stego"], 3 | "props": [ "f5stego", "subarray", "set", "push", "splice", "toFixed", "slice", "length", "ceil", "prototype", "amd", "exports", "stats", "app", "data", "capacity", "coeff_total", "coeff_large", "coeff_zero", "coeff_one", "coeff_one_ratio", "k", "embedded", "examined", "changed", "thrown", "efficiency", "analyze", "f5put", "f5get", "parse", "pack", "clearTail", "addTail", "getTail", "clearAPPs", "getAPPn", "setAPPn", "strip", "embed", "extract"] 4 | } 5 | 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "f5stegojs", 3 | "version": "0.1.2", 4 | "description": "JPEG steganography for browser and node. F5 algo in pure javascript.", 5 | "main": "f5stego.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "bin": { 10 | "f5stego": "bin/f5stego", 11 | "stegodcthist": "bin/stegodcthist", 12 | "stegodctdump": "bin/stegodctdump" 13 | 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/desudesutalk/f5stegojs.git" 18 | }, 19 | "keywords": [ 20 | "jpeg", 21 | "steganography" 22 | ], 23 | "author": "desudesutalk", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/desudesutalk/f5stegojs/issues" 27 | }, 28 | "homepage": "https://github.com/desudesutalk/f5stegojs#readme", 29 | "devDependencies": { 30 | "grunt": "^1.0.1", 31 | "grunt-contrib-jshint": "^1.0.0", 32 | "grunt-contrib-uglify": "^2.0.0" 33 | }, 34 | "dependencies": { 35 | "minimist": "^1.2.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /extra/README.md: -------------------------------------------------------------------------------- 1 | f5extract.min.js 2 | ================ 3 | 4 | 4.5kb of javascript what can decode Huffman-coded baseline and progressive jpegs and extract from them data hidden with f5 algorithm. 5 | 6 | To use this function just include file 7 | ```html 8 | 9 | ``` 10 | or copy and paste its contents to your scripts. 11 | 12 | Usage 13 | ----- 14 | 15 | ```js 16 | var stegKey = [1,2,3,4,5,6,7,8,9]; 17 | 18 | var f5get = f5extract(stegKey); // init stegger with key. Decoding function is returned. 19 | 20 | //extract message from image 21 | var message = f5get(secretImage); 22 | ``` 23 | 24 | Here `stegKey` is an array of byte values which is used for initialization of f5 shuffle. `secretImage` is `Uint8Array` instances with cover jpeg image data. Extracted message will be also returned as `Uint8Array`. 25 | 26 | Note: this function will throw in case of errors. And can return zero length array or array of garbage if image has no embedded message or incorrect `stegKey` was used. 27 | 28 | For more information and license refer to [main readme](../README.md) 29 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.loadNpmTasks('grunt-contrib-uglify'); 3 | grunt.loadNpmTasks('grunt-contrib-jshint'); 4 | 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | 8 | uglify: { 9 | options: { 10 | mangle: { 11 | toplevel: true 12 | }, 13 | compress: { 14 | drop_console: true 15 | }, 16 | mangleProperties: 2, 17 | exceptionsFiles: [ 'uglifyexcept.json' ], 18 | screwIE8: true, 19 | banner: '/*! <%=pkg.name%> v<%=pkg.version%> | <%=pkg.license%> license | <%=pkg.homepage%> */\n' 20 | }, 21 | default: { 22 | files: { 23 | "f5stego.min.js": ["f5stego.js"] 24 | } 25 | }, 26 | extra: { 27 | options:{ 28 | exceptionsFiles: [ 'extra/uglifyexcept.json' ] 29 | }, 30 | files: { 31 | "extra/f5extract.min.js": ["extra/f5extract.js"] 32 | } 33 | } 34 | }, 35 | 36 | jshint: { 37 | all: ["f5stego.js", "extra/f5extract.js"], 38 | options: { 39 | strict: true, 40 | browser: true, 41 | devel: true, 42 | esversion: 5, 43 | } 44 | }, 45 | }); 46 | 47 | grunt.registerTask('default', ['jshint', 'uglify']); 48 | } 49 | -------------------------------------------------------------------------------- /bin/stegodctdump: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var f5stego = require('../'), 4 | fs = require('fs'); 5 | 6 | function usage(){ 7 | console.log(' Dump DCT coefficients from JPEG images.\n\n'+ 8 | ' usage:\n\n'+ 9 | ' stegodctdump \n\n'+ 10 | ' output:\n\n'+ 11 | ' For each component of image separate file with extension DCT_n will be created. Where n is ID of component\n'+ 12 | ' Components are not dequantized or dezigzagged.\n'+ 13 | ' Each component is two byte signed int.'); 14 | } 15 | 16 | var cover, j, out, out16, i, k, c; 17 | 18 | if(!process.argv[2]){ 19 | usage(); 20 | process.exit(1); 21 | } 22 | 23 | var imgfile = process.argv[2]; 24 | 25 | try{ 26 | cover = fs.readFileSync(imgfile); 27 | }catch(e){ 28 | console.error('Unable read image "' + imgfile.toString() + '".'); 29 | console.error(e); 30 | process.exit(1); 31 | } 32 | 33 | try{ 34 | j = new f5stego([1,2,3]); 35 | j.parse(cover); 36 | }catch(e){ 37 | console.error('Image parsing error.'); 38 | console.error(e); 39 | process.exit(1); 40 | } 41 | 42 | for (i = 0; i < j.frame.components.length; i++) { 43 | c = j.frame.components[i]; 44 | out = new Uint8Array(c.blocks.length * 2); 45 | out16 = new Int16Array(out.buffer, out.byteOffset, c.blocks.length); 46 | 47 | out16.set(c.blocks); 48 | 49 | for (k = 0; k < c.blocksDC.length; k++) { 50 | out16[k*64] = c.blocksDC[k]; 51 | } 52 | 53 | try{ 54 | fs.writeFileSync(imgfile + '.DCT_' + c.componentId, new Buffer(out)); 55 | }catch(e){ 56 | console.error('Unable to write output to "' + imgfile + '.DCT_' + c.componentId + '".'); 57 | console.error(e); 58 | process.exit(1); 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /bin/stegodcthist: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var f5stego = require('../'), 4 | fs = require('fs'); 5 | 6 | function usage(){ 7 | console.log(' Dump DCT coefficients histogram from JPEG images.\n\n'+ 8 | ' usage:\n\n'+ 9 | ' stegodumpdct [n]\n\n'+ 10 | ' n is optional parameter what controls maximum (absolute) value of counted coefficients\n'+ 11 | ' output:\n\n'+ 12 | ' For each component of image separate file with extension DCT_hist_n will be created. Where n is ID of component\n'+ 13 | ' Components are not dequantized or dezigzagged.\n'+ 14 | ' Each component is two byte signed int.'); 15 | } 16 | 17 | var cover, j, out, out16, i, k, c; 18 | 19 | if(!process.argv[2]){ 20 | usage(); 21 | process.exit(1); 22 | } 23 | 24 | var imgfile = process.argv[2]; 25 | var n = parseInt(process.argv[3]) || 32768; 26 | 27 | try{ 28 | cover = fs.readFileSync(imgfile); 29 | }catch(e){ 30 | console.error('Unable read image "' + imgfile.toString() + '".'); 31 | console.error(e); 32 | process.exit(1); 33 | } 34 | 35 | try{ 36 | j = new f5stego([1,2,3]); 37 | j.parse(cover); 38 | }catch(e){ 39 | console.error('Image parsing error.'); 40 | console.error(e); 41 | process.exit(1); 42 | } 43 | 44 | var cnt = new Uint32Array(65536); 45 | 46 | for (i = 0; i < j.frame.components.length; i++) { 47 | cnt.fill(0); 48 | 49 | c = j.frame.components[i]; 50 | 51 | for (k = 0; k < c.blocks.length; k++) { 52 | if(k%64 === 0) continue; 53 | 54 | cnt[32768 + c.blocks[k]]++; 55 | } 56 | 57 | s = []; 58 | 59 | for(k = 32768 - n; k < 32768+n; k++){ 60 | s.push(cnt[k]); 61 | } 62 | 63 | console.log(k, n, s.length); 64 | try{ 65 | fs.writeFileSync(imgfile + '.DCT_hist_' + c.componentId, s.join(';')); 66 | }catch(e){ 67 | console.error('Unable to write output to "' + imgfile + '.DCT_hist_' + c.componentId + '".'); 68 | console.error(e); 69 | process.exit(1); 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /bin/f5stego: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var f5stego = require('../'), 4 | fs = require('fs'), 5 | argv = require('minimist')(process.argv.slice(2),{ 6 | strings: 'p', 7 | boolean: ['x', 'e', 's', 'h'], 8 | alias: { 9 | 'x': 'extract', 10 | 'e': 'embed', 11 | 'p': 'password', 12 | 's': 'strip', 13 | 'h': 'help' 14 | } 15 | }); 16 | 17 | function usage(){ 18 | console.log(' Embed and extract data from JPEG images using F5 algorithm.\n\n'+ 19 | ' usage:\n\n'+ 20 | ' embed: f5stegojs -e -p password \n'+ 21 | ' extract: f5stegojs -x -p password \n\n'+ 22 | ' options:\n\n'+ 23 | ' -p, --password password for F5 algorithm shuffle. REQUIRED.\n'+ 24 | ' -e, --embed hidde data in image.\n'+ 25 | ' -x, --extract extract hidden data from image.\n'+ 26 | ' -s, --strip strip metadata from image (e.g. EXIF).\n'+ 27 | ' -h, --help show usage information.\n'); 28 | } 29 | var cover, data, j, out, stats; 30 | 31 | if(argv.embed){ 32 | if(!argv.password){ 33 | console.log('ERROR: you must provide password with -p option.\n\n'); 34 | usage(); 35 | process.exit(1); 36 | } 37 | 38 | if(argv._.length < 3){ 39 | console.log('ERROR: you must provide cover image, data for embedding and out file.\n\n'); 40 | usage(); 41 | process.exit(1); 42 | } 43 | 44 | try{ 45 | cover = fs.readFileSync(argv._[0]); 46 | }catch(e){ 47 | console.error('Unable read cover image "' + argv._[0].toString() + '".'); 48 | console.error(e); 49 | process.exit(1); 50 | } 51 | 52 | try{ 53 | data = fs.readFileSync(argv._[1]); 54 | }catch(e){ 55 | console.error('Unable read data from "' + argv._[1].toString() + '".'); 56 | console.error(e); 57 | process.exit(1); 58 | } 59 | 60 | try{ 61 | j = new f5stego(new Buffer(argv.password.toString(), 'utf8')); 62 | j.parse(cover); 63 | if(argv.strip){ 64 | j.strip(); 65 | } 66 | s = j.f5put(data); 67 | out = j.pack(); 68 | }catch(e){ 69 | console.error('Error while embedding.'); 70 | console.error(e); 71 | process.exit(1); 72 | } 73 | 74 | try{ 75 | fs.writeFileSync(argv._[2].toString(), new Buffer(out)); 76 | }catch(e){ 77 | console.error('Unable to write output to "' + argv._[2].toString() + '".'); 78 | console.error(e); 79 | process.exit(1); 80 | } 81 | 82 | console.log('Total bytes embedded: ' + s.embedded + ' (' +(100*s.embedded/s.stats.capacity[1]).toFixed(2) +'% of estimated capacity), coefficients changed: ' + s.changed + ' (' + s.thrown + ' thrown), efficiency: ' + s.efficiency + ' bits per change with '+((1<=8;)j[l++]=255&k^g[h++],m-=8,k>>=8;for(;m>0;)j[l++]=255&k^g[h++],m-=8,k>>=8;var p=2,q=j[0];return 128&j[1]?(p++,q+=((127&j[1])<<8)+(j[2]<<15)):q+=j[1]<<8,j.subarray(p,p+q)}function e(a,b){for(var c=0,d=0,e=new Uint16Array(65536),f=0;f<16;f++){for(var g=0;g>>u-16&65535],!c)throw"invalid huffman sequence";return u-=c>>>8,255&c}function k(c){for(;u>u-c&(1<>>u-c&(1<>>--u&1}function n(a,b){var c=j(a.a),d=0;0!==c&&(d=k(c)),a.b+=d;for(var e,f,g=1;g<64;)if(e=j(a.c),f=e>>4,e&=15,0!==e)g+=f,a.d[b+g]=k(e),g++;else{if(f<15)break;g+=16}}function o(a){var b=0,c=j(a.a);0!==c&&(b=k(c)),a.b+=b<0)return void v--;for(var c,d,e=f;e<=g;)if(c=j(a.c),d=c>>4,c&=15,0!==c)e+=d,a.d[b+e]=k(c)*w,e++;else{if(15!=d){v=(1<>4,d&=15){if(1!=d)throw"bad jpeg";d=m()?w:x}else if(15!=c){v=1<=0?w:x);else if(--c<0)break;e++}d&&(a.d[b+e]=d),e++}if(v){for(;e<=g;)a.d[b+e]&&(a.d[b+e]+=m()*(a.d[b+e]>=0?w:x)),e++;v--}}var r,s=b,t=0,u=0,v=0,w=1<=65488&&y<=65495)){if(y<=65280)throw"bad jpeg";break}b+=2}for(D--,A=0;A=65488&&y<=65495)){if(y<=65280)throw"bad jpeg";break}b+=2}for(D--,A=0;A=a.length)throw"unexpected EOF";if(j=a[n++],k=a[n++],255==j){if((k>=224&&k<240||254==k||219==k)&&(n+=c()),k>=192&&k<=194){if(g)throw"Only single frame JPEGs supported";if(g={},c(),n++,g.e=194===k,g.m=c(),g.n=c(),g.o=[],g.p={},g.m*g.n>b)throw"Image is too big.";var q,r=a[n++],s=0,t=0;for(h=0;h>4,v=15&a[n+1];s>4===0?p:o)[15&D]=e(E,G)}}if(221==k&&(l=c()),218==k){if(!g)throw"bad jpeg";c();var H=a[n++],I=[];for(h=0;h>4],m.c=o[15&K],I.push(m)}var L=a[n++],M=a[n++],N=a[n++];n+=f(a,n,g,I,l,L,M,N>>4,15&N)}if(217==k)break}else{for(255==a[n-3]&&a[n-2]>=192&&a[n-2]<=254&&(n-=3);255!=a[n]&&n