• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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