This repository has been archived by the owner on Apr 8, 2020. It is now read-only.
forked from enketo/enketo-express
-
Notifications
You must be signed in to change notification settings - Fork 90
/
encryptor.js
226 lines (199 loc) 路 9.14 KB
/
encryptor.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/**********************************************************************************************
* Just a word of warning. Be extra careful changing this code by always testing the decryption
* of submissions with and without media files in ODK Briefcase. If a regression is created it
* may be impossible to retrieve encrypted data (also the user likely cannot share the private
* key).
**********************************************************************************************/
import forge from 'node-forge';
import utils from './utils';
const SYMMETRIC_ALGORITHM = 'AES-CFB'; // JAVA: "AES/CFB/PKCS5Padding"
const ASYMMETRIC_ALGORITHM = 'RSA-OAEP'; // JAVA: "RSA/NONE/OAEPWithSHA256AndMGF1Padding"
const ASYMMETRIC_OPTIONS = {
md: forge.md.sha256.create(),
mgf: forge.mgf.mgf1.create( forge.md.sha1.create() )
};
function isSupported() {
return typeof ArrayBuffer !== 'undefined' &&
new ArrayBuffer( 8 ).byteLength === 8 &&
typeof Uint8Array !== 'undefined' &&
new Uint8Array( 8 ).length === 8;
}
/**
*
* @param {{id: string, version: string, encryptionKey: string}} form
* @param {{instanceId: string, xml: string, files?: [blob]}} record
*/
function encryptRecord( form, record ) {
const symmetricKey = _generateSymmetricKey();
const publicKeyPem = `-----BEGIN PUBLIC KEY-----${form.encryptionKey}-----END PUBLIC KEY-----`;
const forgePublicKey = forge.pki.publicKeyFromPem( publicKeyPem );
const base64EncryptedSymmetricKey = _rsaEncrypt( symmetricKey, forgePublicKey );
const seed = new Seed( record.instanceId, symmetricKey ); //_getIvSeedArray( record.instanceId, symmetricKey );
const manifest = new Manifest( form.id, form.version );
manifest.addElement( 'base64EncryptedKey', base64EncryptedSymmetricKey );
manifest.addMetaElement( 'instanceID', record.instanceId );
let elements = [ form.id ];
if ( form.version ) {
elements.push( form.version );
}
elements = elements.concat( [ base64EncryptedSymmetricKey, record.instanceId ] );
return _encryptMediaFiles( record.files, symmetricKey, seed )
.then( manifest.addMediaFiles )
.then( blobs => {
const submissionXmlEnc = _encryptSubmissionXml( record.xml, symmetricKey, seed );
manifest.addXmlSubmissionFile( submissionXmlEnc );
blobs.push( submissionXmlEnc );
return blobs;
} )
.then( blobs => {
const fileMd5s = blobs.map( blob => `${blob.name.substring( 0, blob.name.length - 4 )}::${blob.md5}` );
elements = elements.concat( fileMd5s );
manifest.addElement( 'base64EncryptedElementSignature', _getBase64EncryptedElementSignature( elements, forgePublicKey ) );
// overwrite record properties so it can be process as a regular submission
record.xml = manifest.getXmlStr();
record.files = blobs;
return record;
} );
}
function _generateSymmetricKey() {
// 256 bit key (32 bytes) for AES256
return forge.random.getBytesSync( 32 );
}
// Equivalent to "RSA/NONE/OAEPWithSHA256AndMGF1Padding"
function _rsaEncrypt( byteString, publicKey ) {
const encrypted = publicKey.encrypt( byteString, ASYMMETRIC_ALGORITHM, ASYMMETRIC_OPTIONS );
return forge.util.encode64( encrypted );
}
function _md5Digest( byteString ) {
const md = forge.md.md5.create();
md.update( byteString );
return md.digest();
}
function _getBase64EncryptedElementSignature( elements, publicKey ) {
// ODK Collect code also adds a newline character **at the end**!
const elementsStr = `${elements.join( '\n' )}\n`;
const messageDigest = _md5Digest( elementsStr ).getBytes();
return _rsaEncrypt( messageDigest, publicKey );
}
function _encryptMediaFiles( files, symmetricKey, seed ) {
files = files || [];
const funcs = files.map( file => () =>
/*
* Note using new fileReader().readAsBinaryString() is about 30% faster than using readAsDataURL
* However, readAsDataURL() works in IE11, and readAsBinaryString() is a bit frowned upon.
* Interestingly, readAsArrayBuffer() is significantly slower than both. That difference is
* caused by forge.util.createBuffer() (which accepts both types as parameter)
*/
utils.blobToDataUri( file )
.then( dataUri => {
const byteString = forge.util.decode64( dataUri.split( ',' )[ 1 ] );
const buffer = forge.util.createBuffer( byteString, 'raw' );
const mediaFileEnc = _encryptContent( buffer, symmetricKey, seed );
mediaFileEnc.name = `${file.name}.enc`;
mediaFileEnc.md5 = _md5Digest( byteString ).toHex();
return mediaFileEnc;
} ) );
// This needs to be sequential for seed array incrementation!
return funcs.reduce( ( prevPromise, func ) => prevPromise.then( result => func()
.then( blob => {
result.push( blob );
return result;
} ) ), Promise.resolve( [] ) );
}
function _encryptSubmissionXml( xmlStr, symmetricKey, seed ) {
const submissionXmlEnc = _encryptContent( forge.util.createBuffer( xmlStr, 'utf8' ), symmetricKey, seed );
submissionXmlEnc.name = 'submission.xml.enc';
submissionXmlEnc.md5 = _md5Digest( xmlStr ).toHex();
return submissionXmlEnc;
}
/**
* Symmetric encryption equivalent to Java "AES/CFB/PKCS5Padding"
* @param {ByteBuffer} content
* @param {*} symmetricKey
* @param {Seed} seed
*/
function _encryptContent( content, symmetricKey, seed ) {
const cipher = forge.cipher.createCipher( SYMMETRIC_ALGORITHM, symmetricKey );
const iv = seed.getIncrementedSeedByteString();
cipher.mode.pad = forge.cipher.modes.cbc.prototype.pad.bind( cipher.mode );
cipher.start( {
iv
} );
cipher.update( content );
const pass = cipher.finish();
const byteString = cipher.output.getBytes();
if ( !pass ) {
throw new Error( 'Encryption failed.' );
}
// Write the bytes of the string to an ArrayBuffer
const buffer = new ArrayBuffer( byteString.length );
const array = new Uint8Array( buffer );
for ( let i = 0; i < byteString.length; i++ ) {
array[ i ] = byteString.charCodeAt( i );
}
// Write the ArrayBuffer to a blob
return new Blob( [ array ] );
}
function Seed( instanceId, symmetricKey ) {
// iv is the 16-byte md5 hash of the instanceID and the symmetric key
const messageDigest = _md5Digest( instanceId + symmetricKey ).getBytes();
const ivSeedArray = messageDigest.split( '' ).map( item => item.charCodeAt( 0 ) );
let ivCounter = 0;
this.getIncrementedSeedByteString = () => {
++ivSeedArray[ ivCounter % ivSeedArray.length ];
++ivCounter;
return ivSeedArray.map( code => String.fromCharCode( code ) ).join( '' );
};
}
function Manifest( formId, formVersion ) {
const ODK_SUBMISSION_NS = 'http://opendatakit.org/submissions';
const OPENROSA_XFORMS_NS = 'http://openrosa.org/xforms';
const manifestEl = document.createElementNS( ODK_SUBMISSION_NS, 'data' );
// move to constructor after ES6 class conversion
manifestEl.setAttribute( 'encrypted', 'yes' );
manifestEl.setAttribute( 'id', formId );
if ( formVersion ) {
manifestEl.setAttribute( 'version', formVersion );
}
this.getXmlStr = () => new XMLSerializer().serializeToString( manifestEl );
this.addElement = ( nodeName, content ) => {
const el = document.createElementNS( ODK_SUBMISSION_NS, nodeName );
el.textContent = content;
manifestEl.appendChild( el );
};
this.addMetaElement = ( nodeName, content ) => {
const metaPresent = manifestEl.querySelector( 'meta' );
const metaEl = metaPresent || document.createElementNS( OPENROSA_XFORMS_NS, 'meta' );
const childEl = document.createElementNS( OPENROSA_XFORMS_NS, nodeName );
childEl.textContent = content;
metaEl.appendChild( childEl );
if ( !metaPresent ) {
manifestEl.appendChild( metaEl );
}
};
this.addMediaFiles = blobs => blobs.map( _addMediaFile );
this.addXmlSubmissionFile = blob => {
const xmlFileEl = document.createElementNS( ODK_SUBMISSION_NS, 'encryptedXmlFile' );
xmlFileEl.setAttribute( 'type', 'file' ); // temporary, used in HTTP submission logic
xmlFileEl.textContent = blob.name;
manifestEl.appendChild( xmlFileEl );
};
function _addMediaFile( blob ) {
// For now we put each media file under its own <media> element due a bug in Aggregate
// https://github.com/opendatakit/aggregate/issues/319
// Once, we no longer need compatibility with old Aggregate servers, we can change that
// by putting all <file> elements under 1 <media> element
const mediaEl = document.createElementNS( ODK_SUBMISSION_NS, 'media' );
const fileEl = document.createElementNS( ODK_SUBMISSION_NS, 'file' );
fileEl.setAttribute( 'type', 'file' ); // temporary, used in HTTP submission logic
fileEl.textContent = blob.name;
mediaEl.appendChild( fileEl );
manifestEl.appendChild( mediaEl );
return blob;
}
}
export default {
isSupported,
encryptRecord,
Seed
};