// Copyright 2015 Joyent, Inc.

module.exports = {

read: read,
readSSHPrivate: readSSHPrivate,
write: write

};

var assert = require('assert-plus'); var asn1 = require('asn1'); var Buffer = require('safer-buffer').Buffer; var algs = require('../algs'); var utils = require('../utils'); var crypto = require('crypto');

var Key = require('../key'); var PrivateKey = require('../private-key'); var pem = require('./pem'); var rfc4253 = require('./rfc4253'); var SSHBuffer = require('../ssh-buffer'); var errors = require('../errors');

var bcrypt;

function read(buf, options) {

return (pem.read(buf, options));

}

var MAGIC = 'openssh-key-v1';

function readSSHPrivate(type, buf, options) {

buf = new SSHBuffer({buffer: buf});

var magic = buf.readCString();
assert.strictEqual(magic, MAGIC, 'bad magic string');

var cipher = buf.readString();
var kdf = buf.readString();
var kdfOpts = buf.readBuffer();

var nkeys = buf.readInt();
if (nkeys !== 1) {
        throw (new Error('OpenSSH-format key file contains ' +
            'multiple keys: this is unsupported.'));
}

var pubKey = buf.readBuffer();

if (type === 'public') {
        assert.ok(buf.atEnd(), 'excess bytes left after key');
        return (rfc4253.read(pubKey));
}

var privKeyBlob = buf.readBuffer();
assert.ok(buf.atEnd(), 'excess bytes left after key');

var kdfOptsBuf = new SSHBuffer({ buffer: kdfOpts });
switch (kdf) {
case 'none':
        if (cipher !== 'none') {
                throw (new Error('OpenSSH-format key uses KDF "none" ' +
                     'but specifies a cipher other than "none"'));
        }
        break;
case 'bcrypt':
        var salt = kdfOptsBuf.readBuffer();
        var rounds = kdfOptsBuf.readInt();
        var cinf = utils.opensshCipherInfo(cipher);
        if (bcrypt === undefined) {
                bcrypt = require('bcrypt-pbkdf');
        }

        if (typeof (options.passphrase) === 'string') {
                options.passphrase = Buffer.from(options.passphrase,
                    'utf-8');
        }
        if (!Buffer.isBuffer(options.passphrase)) {
                throw (new errors.KeyEncryptedError(
                    options.filename, 'OpenSSH'));
        }

        var pass = new Uint8Array(options.passphrase);
        var salti = new Uint8Array(salt);
        /* Use the pbkdf to derive both the key and the IV. */
        var out = new Uint8Array(cinf.keySize + cinf.blockSize);
        var res = bcrypt.pbkdf(pass, pass.length, salti, salti.length,
            out, out.length, rounds);
        if (res !== 0) {
                throw (new Error('bcrypt_pbkdf function returned ' +
                    'failure, parameters invalid'));
        }
        out = Buffer.from(out);
        var ckey = out.slice(0, cinf.keySize);
        var iv = out.slice(cinf.keySize, cinf.keySize + cinf.blockSize);
        var cipherStream = crypto.createDecipheriv(cinf.opensslName,
            ckey, iv);
        cipherStream.setAutoPadding(false);
        var chunk, chunks = [];
        cipherStream.once('error', function (e) {
                if (e.toString().indexOf('bad decrypt') !== -1) {
                        throw (new Error('Incorrect passphrase ' +
                            'supplied, could not decrypt key'));
                }
                throw (e);
        });
        cipherStream.write(privKeyBlob);
        cipherStream.end();
        while ((chunk = cipherStream.read()) !== null)
                chunks.push(chunk);
        privKeyBlob = Buffer.concat(chunks);
        break;
default:
        throw (new Error(
            'OpenSSH-format key uses unknown KDF "' + kdf + '"'));
}

buf = new SSHBuffer({buffer: privKeyBlob});

var checkInt1 = buf.readInt();
var checkInt2 = buf.readInt();
if (checkInt1 !== checkInt2) {
        throw (new Error('Incorrect passphrase supplied, could not ' +
            'decrypt key'));
}

var ret = {};
var key = rfc4253.readInternal(ret, 'private', buf.remainder());

buf.skip(ret.consumed);

var comment = buf.readString();
key.comment = comment;

return (key);

}

function write(key, options) {

var pubKey;
if (PrivateKey.isPrivateKey(key))
        pubKey = key.toPublic();
else
        pubKey = key;

var cipher = 'none';
var kdf = 'none';
var kdfopts = Buffer.alloc(0);
var cinf = { blockSize: 8 };
var passphrase;
if (options !== undefined) {
        passphrase = options.passphrase;
        if (typeof (passphrase) === 'string')
                passphrase = Buffer.from(passphrase, 'utf-8');
        if (passphrase !== undefined) {
                assert.buffer(passphrase, 'options.passphrase');
                assert.optionalString(options.cipher, 'options.cipher');
                cipher = options.cipher;
                if (cipher === undefined)
                        cipher = 'aes128-ctr';
                cinf = utils.opensshCipherInfo(cipher);
                kdf = 'bcrypt';
        }
}

var privBuf;
if (PrivateKey.isPrivateKey(key)) {
        privBuf = new SSHBuffer({});
        var checkInt = crypto.randomBytes(4).readUInt32BE(0);
        privBuf.writeInt(checkInt);
        privBuf.writeInt(checkInt);
        privBuf.write(key.toBuffer('rfc4253'));
        privBuf.writeString(key.comment || '');

        var n = 1;
        while (privBuf._offset % cinf.blockSize !== 0)
                privBuf.writeChar(n++);
        privBuf = privBuf.toBuffer();
}

switch (kdf) {
case 'none':
        break;
case 'bcrypt':
        var salt = crypto.randomBytes(16);
        var rounds = 16;
        var kdfssh = new SSHBuffer({});
        kdfssh.writeBuffer(salt);
        kdfssh.writeInt(rounds);
        kdfopts = kdfssh.toBuffer();

        if (bcrypt === undefined) {
                bcrypt = require('bcrypt-pbkdf');
        }
        var pass = new Uint8Array(passphrase);
        var salti = new Uint8Array(salt);
        /* Use the pbkdf to derive both the key and the IV. */
        var out = new Uint8Array(cinf.keySize + cinf.blockSize);
        var res = bcrypt.pbkdf(pass, pass.length, salti, salti.length,
            out, out.length, rounds);
        if (res !== 0) {
                throw (new Error('bcrypt_pbkdf function returned ' +
                    'failure, parameters invalid'));
        }
        out = Buffer.from(out);
        var ckey = out.slice(0, cinf.keySize);
        var iv = out.slice(cinf.keySize, cinf.keySize + cinf.blockSize);

        var cipherStream = crypto.createCipheriv(cinf.opensslName,
            ckey, iv);
        cipherStream.setAutoPadding(false);
        var chunk, chunks = [];
        cipherStream.once('error', function (e) {
                throw (e);
        });
        cipherStream.write(privBuf);
        cipherStream.end();
        while ((chunk = cipherStream.read()) !== null)
                chunks.push(chunk);
        privBuf = Buffer.concat(chunks);
        break;
default:
        throw (new Error('Unsupported kdf ' + kdf));
}

var buf = new SSHBuffer({});

buf.writeCString(MAGIC);
buf.writeString(cipher);        /* cipher */
buf.writeString(kdf);           /* kdf */
buf.writeBuffer(kdfopts);       /* kdfoptions */

buf.writeInt(1);                /* nkeys */
buf.writeBuffer(pubKey.toBuffer('rfc4253'));

if (privBuf)
        buf.writeBuffer(privBuf);

buf = buf.toBuffer();

var header;
if (PrivateKey.isPrivateKey(key))
        header = 'OPENSSH PRIVATE KEY';
else
        header = 'OPENSSH PUBLIC KEY';

var tmp = buf.toString('base64');
var len = tmp.length + (tmp.length / 70) +
    18 + 16 + header.length*2 + 10;
buf = Buffer.alloc(len);
var o = 0;
o += buf.write('-----BEGIN ' + header + '-----\n', o);
for (var i = 0; i < tmp.length; ) {
        var limit = i + 70;
        if (limit > tmp.length)
                limit = tmp.length;
        o += buf.write(tmp.slice(i, limit), o);
        buf[o++] = 10;
        i = limit;
}
o += buf.write('-----END ' + header + '-----\n', o);

return (buf.slice(0, o));

}