1// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved. 2 3var assert = require('assert'); 4var Buffer = require('safer-buffer').Buffer; 5var ASN1 = require('./types'); 6var errors = require('./errors'); 7 8 9// --- Globals 10 11var newInvalidAsn1Error = errors.newInvalidAsn1Error; 12 13var DEFAULT_OPTS = { 14 size: 1024, 15 growthFactor: 8 16}; 17 18 19// --- Helpers 20 21function merge(from, to) { 22 assert.ok(from); 23 assert.equal(typeof (from), 'object'); 24 assert.ok(to); 25 assert.equal(typeof (to), 'object'); 26 27 var keys = Object.getOwnPropertyNames(from); 28 keys.forEach(function (key) { 29 if (to[key]) 30 return; 31 32 var value = Object.getOwnPropertyDescriptor(from, key); 33 Object.defineProperty(to, key, value); 34 }); 35 36 return to; 37} 38 39 40 41// --- API 42 43function Writer(options) { 44 options = merge(DEFAULT_OPTS, options || {}); 45 46 this._buf = Buffer.alloc(options.size || 1024); 47 this._size = this._buf.length; 48 this._offset = 0; 49 this._options = options; 50 51 // A list of offsets in the buffer where we need to insert 52 // sequence tag/len pairs. 53 this._seq = []; 54} 55 56Object.defineProperty(Writer.prototype, 'buffer', { 57 get: function () { 58 if (this._seq.length) 59 throw newInvalidAsn1Error(this._seq.length + ' unended sequence(s)'); 60 61 return (this._buf.slice(0, this._offset)); 62 } 63}); 64 65Writer.prototype.writeByte = function (b) { 66 if (typeof (b) !== 'number') 67 throw new TypeError('argument must be a Number'); 68 69 this._ensure(1); 70 this._buf[this._offset++] = b; 71}; 72 73 74Writer.prototype.writeInt = function (i, tag) { 75 if (typeof (i) !== 'number') 76 throw new TypeError('argument must be a Number'); 77 if (typeof (tag) !== 'number') 78 tag = ASN1.Integer; 79 80 var sz = 4; 81 82 while ((((i & 0xff800000) === 0) || ((i & 0xff800000) === 0xff800000 >> 0)) && 83 (sz > 1)) { 84 sz--; 85 i <<= 8; 86 } 87 88 if (sz > 4) 89 throw newInvalidAsn1Error('BER ints cannot be > 0xffffffff'); 90 91 this._ensure(2 + sz); 92 this._buf[this._offset++] = tag; 93 this._buf[this._offset++] = sz; 94 95 while (sz-- > 0) { 96 this._buf[this._offset++] = ((i & 0xff000000) >>> 24); 97 i <<= 8; 98 } 99 100}; 101 102 103Writer.prototype.writeNull = function () { 104 this.writeByte(ASN1.Null); 105 this.writeByte(0x00); 106}; 107 108 109Writer.prototype.writeEnumeration = function (i, tag) { 110 if (typeof (i) !== 'number') 111 throw new TypeError('argument must be a Number'); 112 if (typeof (tag) !== 'number') 113 tag = ASN1.Enumeration; 114 115 return this.writeInt(i, tag); 116}; 117 118 119Writer.prototype.writeBoolean = function (b, tag) { 120 if (typeof (b) !== 'boolean') 121 throw new TypeError('argument must be a Boolean'); 122 if (typeof (tag) !== 'number') 123 tag = ASN1.Boolean; 124 125 this._ensure(3); 126 this._buf[this._offset++] = tag; 127 this._buf[this._offset++] = 0x01; 128 this._buf[this._offset++] = b ? 0xff : 0x00; 129}; 130 131 132Writer.prototype.writeString = function (s, tag) { 133 if (typeof (s) !== 'string') 134 throw new TypeError('argument must be a string (was: ' + typeof (s) + ')'); 135 if (typeof (tag) !== 'number') 136 tag = ASN1.OctetString; 137 138 var len = Buffer.byteLength(s); 139 this.writeByte(tag); 140 this.writeLength(len); 141 if (len) { 142 this._ensure(len); 143 this._buf.write(s, this._offset); 144 this._offset += len; 145 } 146}; 147 148 149Writer.prototype.writeBuffer = function (buf, tag) { 150 if (typeof (tag) !== 'number') 151 throw new TypeError('tag must be a number'); 152 if (!Buffer.isBuffer(buf)) 153 throw new TypeError('argument must be a buffer'); 154 155 this.writeByte(tag); 156 this.writeLength(buf.length); 157 this._ensure(buf.length); 158 buf.copy(this._buf, this._offset, 0, buf.length); 159 this._offset += buf.length; 160}; 161 162 163Writer.prototype.writeStringArray = function (strings) { 164 if ((!strings instanceof Array)) 165 throw new TypeError('argument must be an Array[String]'); 166 167 var self = this; 168 strings.forEach(function (s) { 169 self.writeString(s); 170 }); 171}; 172 173// This is really to solve DER cases, but whatever for now 174Writer.prototype.writeOID = function (s, tag) { 175 if (typeof (s) !== 'string') 176 throw new TypeError('argument must be a string'); 177 if (typeof (tag) !== 'number') 178 tag = ASN1.OID; 179 180 if (!/^([0-9]+\.){3,}[0-9]+$/.test(s)) 181 throw new Error('argument is not a valid OID string'); 182 183 function encodeOctet(bytes, octet) { 184 if (octet < 128) { 185 bytes.push(octet); 186 } else if (octet < 16384) { 187 bytes.push((octet >>> 7) | 0x80); 188 bytes.push(octet & 0x7F); 189 } else if (octet < 2097152) { 190 bytes.push((octet >>> 14) | 0x80); 191 bytes.push(((octet >>> 7) | 0x80) & 0xFF); 192 bytes.push(octet & 0x7F); 193 } else if (octet < 268435456) { 194 bytes.push((octet >>> 21) | 0x80); 195 bytes.push(((octet >>> 14) | 0x80) & 0xFF); 196 bytes.push(((octet >>> 7) | 0x80) & 0xFF); 197 bytes.push(octet & 0x7F); 198 } else { 199 bytes.push(((octet >>> 28) | 0x80) & 0xFF); 200 bytes.push(((octet >>> 21) | 0x80) & 0xFF); 201 bytes.push(((octet >>> 14) | 0x80) & 0xFF); 202 bytes.push(((octet >>> 7) | 0x80) & 0xFF); 203 bytes.push(octet & 0x7F); 204 } 205 } 206 207 var tmp = s.split('.'); 208 var bytes = []; 209 bytes.push(parseInt(tmp[0], 10) * 40 + parseInt(tmp[1], 10)); 210 tmp.slice(2).forEach(function (b) { 211 encodeOctet(bytes, parseInt(b, 10)); 212 }); 213 214 var self = this; 215 this._ensure(2 + bytes.length); 216 this.writeByte(tag); 217 this.writeLength(bytes.length); 218 bytes.forEach(function (b) { 219 self.writeByte(b); 220 }); 221}; 222 223 224Writer.prototype.writeLength = function (len) { 225 if (typeof (len) !== 'number') 226 throw new TypeError('argument must be a Number'); 227 228 this._ensure(4); 229 230 if (len <= 0x7f) { 231 this._buf[this._offset++] = len; 232 } else if (len <= 0xff) { 233 this._buf[this._offset++] = 0x81; 234 this._buf[this._offset++] = len; 235 } else if (len <= 0xffff) { 236 this._buf[this._offset++] = 0x82; 237 this._buf[this._offset++] = len >> 8; 238 this._buf[this._offset++] = len; 239 } else if (len <= 0xffffff) { 240 this._buf[this._offset++] = 0x83; 241 this._buf[this._offset++] = len >> 16; 242 this._buf[this._offset++] = len >> 8; 243 this._buf[this._offset++] = len; 244 } else { 245 throw newInvalidAsn1Error('Length too long (> 4 bytes)'); 246 } 247}; 248 249Writer.prototype.startSequence = function (tag) { 250 if (typeof (tag) !== 'number') 251 tag = ASN1.Sequence | ASN1.Constructor; 252 253 this.writeByte(tag); 254 this._seq.push(this._offset); 255 this._ensure(3); 256 this._offset += 3; 257}; 258 259 260Writer.prototype.endSequence = function () { 261 var seq = this._seq.pop(); 262 var start = seq + 3; 263 var len = this._offset - start; 264 265 if (len <= 0x7f) { 266 this._shift(start, len, -2); 267 this._buf[seq] = len; 268 } else if (len <= 0xff) { 269 this._shift(start, len, -1); 270 this._buf[seq] = 0x81; 271 this._buf[seq + 1] = len; 272 } else if (len <= 0xffff) { 273 this._buf[seq] = 0x82; 274 this._buf[seq + 1] = len >> 8; 275 this._buf[seq + 2] = len; 276 } else if (len <= 0xffffff) { 277 this._shift(start, len, 1); 278 this._buf[seq] = 0x83; 279 this._buf[seq + 1] = len >> 16; 280 this._buf[seq + 2] = len >> 8; 281 this._buf[seq + 3] = len; 282 } else { 283 throw newInvalidAsn1Error('Sequence too long'); 284 } 285}; 286 287 288Writer.prototype._shift = function (start, len, shift) { 289 assert.ok(start !== undefined); 290 assert.ok(len !== undefined); 291 assert.ok(shift); 292 293 this._buf.copy(this._buf, start + shift, start, start + len); 294 this._offset += shift; 295}; 296 297Writer.prototype._ensure = function (len) { 298 assert.ok(len); 299 300 if (this._size - this._offset < len) { 301 var sz = this._size * this._options.growthFactor; 302 if (sz - this._offset < len) 303 sz += len; 304 305 var buf = Buffer.alloc(sz); 306 307 this._buf.copy(buf, 0, 0, this._offset); 308 this._buf = buf; 309 this._size = sz; 310 } 311}; 312 313 314 315// --- Exported API 316 317module.exports = Writer; 318