Skip to content

Latest commit

 

History

History

nicola-del-gobbo-how-i-ported-bcrypt-to-new-n-api

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

Как я портировал bcrypt на новое N-API

Перевод статьи Nicola Del Gobbo: How I ported bcrypt to new N-API. Опубликовано с разрешения автора.

Для простоты нативные модули можно рассматривать как C/C++ код, который вызывается из JavaScript.

Они являются мостом между привычным нам языком программирования JavaScript и родной средой, полностью написанной на C/C++.

Они позволяют нам вызывать функции и методы C/C++ непосредственно из JavaScript.

Нативные модули Node.js

Одной из наиболее важных экспериментальных функций, объявленных в Node.js 8, была поддержка N-API, направленная на снижение затрат на обслуживание нативных модулей Node.js.

Это API не зависит от среды исполнения JavaScript (V8) и поддерживается как часть самого Node.js.

Это API будет стабильным на уровне бинарного интерфейса приложений (Application Binary Interface или ABI) между версиями Node.js. Оно предназначено для изоляции модулей от изменений в движке JavaScript и позволяет использовать нативные модули различными версиями Node.js без перекомпиляции.

Такие модули создаются/упаковываются с использованием того же самого подхода и инструментов, описанных в разделе C++ Addons. Единственное отличие — это набор API, использующихся нативным кодом. Вместо использования API V8 или Native Abstractions используются функции, доступные в N-API.

Как вы можете понять из документации по N-API, оно не зависит от версии движка JavaScript и совместимость API и ABI между различными версиями Node.js гарантируется. Поэтому, если вы переключитесь на другую версию Node.js, вы не должны переустанавливать и перекомпилировать нативные модули.

Я был в восторге от N-API, особенно после того, как я посмотрел доклад, Майкла Доусона на Node Interactive 2017:

N-API — Next Generation Node API for Native Modules

Я обнаружил, что есть обёртка над N-API для C++, называемая node-addon-api, поэтому я просто начал экспериментировать с ней и через четыре или пять дней своего свободного времени я портировал bcrypt на N-API.

Я начал с изменения файла package.json, в который я добавил необходимые зависимости, как указано в документации.

...
"scripts": {
    "test": "npm install --build-from-source && nodeunit test",
    "install": "node-gyp rebuild"
  },
  "dependencies": {
    "bindings": "1.3.0",
    "node-addon-api": "1.1.0"
  },
  "gypfile": true,
  "devDependencies": {
    "nodeunit": "~0.9.1"
  }
...

https://gist.github.com/NickNaso/56d61bbf824077f2d084a5f14f794e61#file-package-json package.json

Следующим шагом было изменение файла binding.gyp, который содержит конфигурацию сборки для модуля bcrypt.

{
  'targets': [
    {
      'target_name': 'bcrypt_napi',
      'sources': [
        'src/blowfish.cc',
        'src/bcrypt.cc',
        'src/bcrypt_node.cc'
      ],
      'cflags!': [ '-fno-exceptions' ],
      'cflags_cc!': [ '-fno-exceptions' ],
      'include_dirs' : [
          "<!@(node -p \"require('node-addon-api').include\")"
      ], 
      'dependencies': ["<!(node -p \"require('node-addon-api').gyp\")"],
      'conditions': [
        ['OS=="win"', {
          "msvs_settings": {
            "VCCLCompilerTool": {
              "ExceptionHandling": 1
            }
          },
          'defines': [
            'uint=unsigned int',
          ]
        }],
        ['OS=="mac"', {
          "xcode_settings": {
            "CLANG_CXX_LIBRARY": "libc++",
            'GCC_ENABLE_CPP_EXCEPTIONS': 'YES',
            'MACOSX_DEPLOYMENT_TARGET': '10.7'
          }
        }]
      ]
    }
  ]
}

https://gist.github.com/NickNaso/7c512d06eb6bec27fce972df89fbc0ab#file-binding-gyp binding.gyp

Модуль bcrypt состоит из двух частей, одна написана на C++, вторая на JavaScript, и, как правило, JavaScript-часть использует нативный код. Я не хотел изменять JavaScript API, предоставляемый bcrypt, поэтому я сосредоточился на C++ части и изменил код, следуя документации node-addon-api.

Я начал с кода, который отвечает за регистрацию модуля с именем bcrypt_napi, и, кроме того, этот код гарантирует вызов функции init при подключении модуля.

Napi::Object init(Napi::Env env, Napi::Object exports) {
    exports.Set(Napi::String::New(env, "gen_salt_sync"), Napi::Function::New(env, GenerateSaltSync));
    exports.Set(Napi::String::New(env, "encrypt_sync"), Napi::Function::New(env, EncryptSync));
    exports.Set(Napi::String::New(env, "compare_sync"), Napi::Function::New(env, CompareSync));
    exports.Set(Napi::String::New(env, "get_rounds"), Napi::Function::New(env, GetRounds));
    exports.Set(Napi::String::New(env, "gen_salt"), Napi::Function::New(env, GenerateSalt));
    exports.Set(Napi::String::New(env, "encrypt"), Napi::Function::New(env, Encrypt));
    exports.Set(Napi::String::New(env, "compare"), Napi::Function::New(env, Compare));
    return exports;
};

NODE_API_MODULE(bcrypt_napi, init);

https://gist.github.com/NickNaso/c97df8ef7482bf3202520bf0af87d1cd#file-bcrypt_node-cc Код инициализации

bcrypt предоставляет синхронное и асинхронное API, поэтому метод за методом я проделал рефакторинг, и теперь код выглядит так, как показано ниже.

class SaltAsyncWorker : public Napi::AsyncWorker {
    public:
        SaltAsyncWorker(Napi::Function& callback, std::string seed, ssize_t rounds)
            : Napi::AsyncWorker(callback), seed(seed), rounds(rounds) {
        }

        ~SaltAsyncWorker() {}

        void Execute() {
            char salt[_SALT_LEN];
            bcrypt_gensalt((char) rounds, (u_int8_t *)&seed[0], salt);
            this->salt = std::string(salt);
        }

        void OnOK() {
            Napi::HandleScope scope(Env());   
            Callback().Call({Env().Undefined(), Napi::String::New(Env(), salt)});
        }

    private:
        std::string seed;
        std::string salt;
        ssize_t rounds;
        
};

Napi::Value GenerateSalt(const Napi::CallbackInfo& info) {
    if (info.Length() < 3) {
        throw Napi::TypeError::New(info.Env(), "3 arguments expected");
    }
    if (!info[1].IsBuffer() || (info[1].As<Napi::Buffer<char>>()).Length() != 16) {
        throw Napi::TypeError::New(info.Env(), "Second argument must be a 16 byte Buffer");
    }
    const int32_t rounds = info[0].As<Napi::Number>();
    Napi::Function callback = info[2].As<Napi::Function>();
    Napi::Buffer<char> seed = info[1].As<Napi::Buffer<char>>();
    SaltAsyncWorker* saltWorker = new SaltAsyncWorker(callback, std::string(seed.Data(), 16), rounds);
    saltWorker->Queue();
    return info.Env().Undefined();
}

Napi::Value GenerateSaltSync (const Napi::CallbackInfo& info) {
    if (info.Length() < 2) {
        throw Napi::TypeError::New(info.Env(), "2 arguments expected");
    }
    if (!info[1].IsBuffer() || (info[1].As<Napi::Buffer<char>>()).Length() != 16) {
        throw Napi::TypeError::New(info.Env(), "Second argument must be a 16 byte Buffer");
    }
    const int32_t rounds = info[0].As<Napi::Number>();
    Napi::Buffer<u_int8_t> buffer = info[1].As<Napi::Buffer<u_int8_t>>();
    u_int8_t* seed = (u_int8_t*) buffer.Data();
    char salt[_SALT_LEN];
    bcrypt_gensalt(rounds, seed, salt);
    return Napi::String::New(info.Env(), salt, strlen(salt));
}

/* ENCRYPT DATA - USED TO BE HASHPW */

class EncryptAsyncWorker : public Napi::AsyncWorker {
  public:
    EncryptAsyncWorker(Napi::Function& callback, std::string input, std::string salt)
        : Napi::AsyncWorker(callback), input(input), salt(salt) {
    }

    ~EncryptAsyncWorker() {}

    void Execute() {
        if (!(ValidateSalt(salt.c_str()))) {
            error = "Invalid salt. Salt must be in the form of: $Vers$log2(NumRounds)$saltvalue";
        }
        char bcrypted[_PASSWORD_LEN];
        bcrypt(input.c_str(), salt.c_str(), bcrypted);
        output = std::string(bcrypted);
    }

    void OnOK() {
        Napi::HandleScope scope(Env());  
        if (!error.empty()) {
            Callback().Call({
                Napi::Error::New(Env(), error.c_str()).Value(),
                Env().Undefined()                       
            });
        } else {
            Callback().Call({
                Env().Undefined(), 
                Napi::String::New(Env(), output)
            });
        }   
    }

  private:
    std::string input;
    std::string salt;
    std::string error;
    std::string output;
};

Napi::Value Encrypt(const Napi::CallbackInfo& info) {
    if (info.Length() < 3) {
        throw Napi::TypeError::New(info.Env(), "3 arguments expected");
    }
    std::string data = info[0].As<Napi::String>();;
    std::string salt = info[1].As<Napi::String>();;
    Napi::Function callback = info[2].As<Napi::Function>();
    EncryptAsyncWorker* encryptWorker = new EncryptAsyncWorker(callback, data, salt);
    encryptWorker->Queue();
    return info.Env().Undefined();
}

Napi::Value EncryptSync(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    if (info.Length() < 2) {
        throw Napi::TypeError::New(info.Env(), "2 arguments expected");   
    }
    std::string data = info[0].As<Napi::String>();;
    std::string salt = info[1].As<Napi::String>();;
    if (!(ValidateSalt(salt.c_str()))) {
        throw Napi::Error::New(info.Env(), "Invalid salt. Salt must be in the form of: $Vers$log2(NumRounds)$saltvalue");
    }
    char bcrypted[_PASSWORD_LEN];
    bcrypt(data.c_str(), salt.c_str(), bcrypted);
    return Napi::String::New(env, bcrypted, strlen(bcrypted));
}

/* COMPARATOR */

bool CompareStrings(const char* s1, const char* s2) {

    bool eq = true;
    int s1_len = strlen(s1);
    int s2_len = strlen(s2);

    if (s1_len != s2_len) {
        eq = false;
    }

    const int max_len = (s2_len < s1_len) ? s1_len : s2_len;

    // to prevent timing attacks, should check entire string
    // don't exit after found to be false
    for (int i = 0; i < max_len; ++i) {
      if (s1_len >= i && s2_len >= i && s1[i] != s2[i]) {
        eq = false;
      }
    }

    return eq;
}

class CompareAsyncWorker : public Napi::AsyncWorker {
  public:
    CompareAsyncWorker(Napi::Function& callback, std::string input, std::string encrypted)
        : Napi::AsyncWorker(callback), input(input), encrypted(encrypted) {
        result = false;
    }

    ~CompareAsyncWorker() {}

    void Execute() {
        char bcrypted[_PASSWORD_LEN];
        if (ValidateSalt(encrypted.c_str())) {
            bcrypt(input.c_str(), encrypted.c_str(), bcrypted);
            result = CompareStrings(bcrypted, encrypted.c_str());
        }
    }

    void OnOK() {
        Napi::HandleScope scope(Env());   
        Callback().Call({Env().Undefined(), Napi::Boolean::New(Env(), result)});      
    }

  private:
    std::string input;
    std::string encrypted;
    bool result;
};

Napi::Value Compare(const Napi::CallbackInfo& info) {
    if (info.Length() < 3) {
        throw Napi::TypeError::New(info.Env(), "3 arguments expected");
    }
    std::string input = info[0].As<Napi::String>();
    std::string encrypted = info[1].As<Napi::String>();
    Napi::Function callback = info[2].As<Napi::Function>();
    CompareAsyncWorker* compareWorker = new CompareAsyncWorker(callback, input, encrypted);
    compareWorker->Queue();
    return info.Env().Undefined();
}

Napi::Value CompareSync(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    if (info.Length() < 2) {
        throw Napi::TypeError::New(info.Env(), "2 arguments expected");    
    }
    std::string pw = info[0].As<Napi::String>();
    std::string hash = info[1].As<Napi::String>();
    char bcrypted[_PASSWORD_LEN];
    if (ValidateSalt(hash.c_str())) {
        bcrypt(pw.c_str(), hash.c_str(), bcrypted);
        return Napi::Boolean::New(env, CompareStrings(bcrypted, hash.c_str()));
    } else {
        return Napi::Boolean::New(env, false);
    }
}

Napi::Value GetRounds(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    if (info.Length() < 1) {
        throw Napi::TypeError::New(info.Env(), "1 argument expected");    
    }
    Napi::String hashed = info[0].As<Napi::String>();
    std::string hash = hashed.ToString();
    const char* bcrypt_hash = hash.c_str();
    u_int32_t rounds;
    if (!(rounds = bcrypt_get_rounds(bcrypt_hash))) {
        throw Napi::Error::New(info.Env(), "invalid hash provided");
    }
    return Napi::Number::New(env, rounds);
}

https://gist.github.com/NickNaso/3a7d1997be59c010c6cde4353d873f14#file-bcrypt_node-cc

Последний шаг состоял в том, чтобы прогнать весь набор тестов, и к моему удовольствию все тесты прошли успешно, и они были выполнены быстрее, чем у модуля, созданного с использованием NAN. В ближайшие дни я буду прогонять самые эффективные тесты производительности, после чего я разверну своё первое Node.js-приложение, которое будет использовать эту версию bcrypt, поэтому следите за обновлениями.

Для тех из вас, кто хочет начать писать свои собственные Node.js-аддоны с использованием N-API, я оставлю список полезных ресурсов, которые мне очень помогли:


Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.

Статья на Medium