Spec-Zone.ru › Ruby 2.3

класс OpenSSL::Cipher

Родитель:
Объект

Предоставляет симметричные алгоритмы для шифрования и дешифрования. Доступные алгоритмы зависят от установленной версии OpenSSL.

Список всех поддерживаемых алгоритмов

Список поддерживаемых алгоритмов можно получить, выполнив

puts OpenSSL::Cipher.ciphers

Создание экземпляра Cipher

Существует несколько способов создания экземпляра Cipher. В целом, алгоритм Cipher определяется его именем, длиной ключа в битах и режимом шифрования. Наиболее общий способ создания Cipher следующий

cipher = OpenSSL::Cipher.new('<name>-<key length>-<mode>')

То есть, строка, состоящая из склеенных через дефис имени, длины ключа и режима. Можно использовать как строчные, так и прописные буквы, например:

cipher = OpenSSL::Cipher.new('AES-128-CBC')

Для каждого поддерживаемого алгоритма определен класс под классом Cipher, который носит имя шифра, например, для получения экземпляра AES можно также использовать

# these are equivalent
cipher = OpenSSL::Cipher::AES.new(128, :CBC)
cipher = OpenSSL::Cipher::AES.new(128, 'CBC')
cipher = OpenSSL::Cipher::AES.new('128-CBC')

Наконец, из-за широкого использования также определены дополнительные классы для различных размеров ключей AES

cipher = OpenSSL::Cipher::AES128.new(:CBC)
cipher = OpenSSL::Cipher::AES192.new(:CBC)
cipher = OpenSSL::Cipher::AES256.new(:CBC)

Выбор режима шифрования или дешифрования

Шифрование и дешифрование для симметричных алгоритмов часто являются очень похожими операциями; это отражается в отсутствии необходимости выбирать разные классы для этих операций. Обе операции можно выполнить, используя один и тот же класс. Тем не менее, после получения экземпляра Cipher необходимо указать, что мы хотим сделать с ним, поэтому необходимо вызвать либо

cipher.encrypt

или

cipher.decrypt

для экземпляра Cipher. Это должно быть первое обращение после создания экземпляра, в противном случае уже установленные параметры могут быть потеряны в процессе.

Выбор ключа

Симметричное шифрование требует ключа, который одинаков для шифрования и дешифрования, и после первоначального установления ключа должен храниться в конфиденциальности. Существует много способов создания небезопасных ключей, наиболее заметным из которых является просто использование пароля в качестве ключа без дальнейшей обработки пароля. Простой и безопасный способ создания ключа для определенного Cipher это

cipher = OpenSSL::AES256.new(:CFB)
cipher.encrypt
key = cipher.random_key # also sets the generated key on the Cipher

Если вам абсолютно необходимо использовать пароли в качестве ключей шифрования, вы должны использовать функцию вывода ключа на основе пароля PBKDF2 (Password-Based Key Derivation Function 2), генерируя ключ с помощью функциональности, предоставляемой OpenSSL::PKCS5.pbkdf2_hmac_sha1 или OpenSSL::PKCS5.pbkdf2_hmac.

Хотя есть #pkcs5_keyivgen, его использование устарело, и его следует использовать только в устаревших приложениях, потому что он не использует новые алгоритмы PKCS#5 v2.

Выбор IV

Режим шифрования CBC, CFB, OFB и CTR нуждаются в «векторе инициализации» или, короче, IV. Режим ECB является единственным режимом, не требующим IV, но для него практически нет законных случаев использования из-за того, что он недостаточно скрывает шаблоны открытого текста. Поэтому

Вы никогда не должны использовать режим ECB, если не уверены, что вам он действительно необходим

Из-за этого вы получите режим, который явно требует IV в любом случае. Обратите внимание, что по соображениям совместимости с предыдущими версиями, установка IV не является явно обязательной в API Cipher. Если он не установлен, сам OpenSSL по умолчанию устанавливает IV со всеми нулями («\0», а не символ). Хотя IV можно рассматривать как публичную информацию, т. е. он может передаваться публично после генерации, он должен оставаться непредсказуемым, чтобы предотвратить определенные виды атак. Поэтому, в идеале

Всегда создавайте безопасный случайный IV для каждого шифрования вашего Cipher

Новый случайный IV должен создаваться для каждого шифрования данных. Подумайте об IV как о номере, используемом один раз (nonce) — он публичен, но случайный и непредсказуемый. Безопасный случайный IV можно создать следующим образом

cipher = ...
cipher.encrypt
key = cipher.random_key
iv = cipher.random_iv # also sets the generated IV on the Cipher

Although the key is generally a random value, too, it is a bad choice
as an IV. There are elaborate ways how an attacker can take advantage
of such an IV. As a general rule of thumb, exposing the key directly
or indirectly should be avoided at all cost and exceptions only be
made with good reason.

Вызов #final

Режимы ECB (который не следует использовать) и CBC являются блочными режимами. Это означает, что в отличие от других потоковых режимов, они работают с блоками данных фиксированного размера и, следовательно, требуют шага «окончания», чтобы обработать или правильно дешифровать последний блок данных, правильно обрабатывая какой-либо вид заполнения. Поэтому очень важно добавить результат #final в свой буфер шифрования/дешифрования, иначе вы получите ошибки дешифрования или усеченные данные.

Хотя это не совсем необходимо для шифров в потоковом режиме, все же рекомендуется применять ту же схему добавления результата #final и там — это также позволит вам легче переключаться между режимами в будущем.

Шифрование и дешифрование некоторых данных

data = "Very, very confidential data"

cipher = OpenSSL::Cipher::AES.new(128, :CBC)
cipher.encrypt
key = cipher.random_key
iv = cipher.random_iv

encrypted = cipher.update(data) + cipher.final
...
decipher = OpenSSL::Cipher::AES.new(128, :CBC)
decipher.decrypt
decipher.key = key
decipher.iv = iv

plain = decipher.update(encrypted) + decipher.final

puts data == plain #=> true

Аутентифицированное шифрование и связанные данные (AEAD)

Если используемая версия OpenSSL поддерживает это, режим аутентифицированного шифрования (например, GCM или CCM) всегда следует предпочитать любому неаутентифицированному режиму. В настоящее время OpenSSL поддерживает режим AE только в сочетании с ассоциированными данными (AEAD), где дополнительные ассоциированные данные включаются в процесс шифрования для вычисления тега в конце шифрования. Этот тег также будет использоваться в процессе дешифрования и, проверив его валидность, устанавливается подлинность данного шифрованного текста.

Это превосходит неаутентифицированные режимы тем, что позволяет обнаружить, если кто-то фактически изменил шифрованный текст после его шифрования. Это предотвращает вредоносные изменения шифрованного текста, которые в противном случае могли быть использованы для изменения шифрованных текстов, выгодных потенциальным злоумышленникам.

Если для шифрования и последующего дешифрования не нужны ассоциированные данные, библиотека OpenSSL все равно требует установки значения — «» может быть использовано в случае отсутствия данных. Пример с использованием режима GCM (Galois Counter Mode):

cipher = OpenSSL::Cipher::AES.new(128, :GCM)
cipher.encrypt
key = cipher.random_key
iv = cipher.random_iv
cipher.auth_data = ""

encrypted = cipher.update(data) + cipher.final
tag = cipher.auth_tag

decipher = OpenSSL::Cipher::AES.new(128, :GCM)
decipher.decrypt
decipher.key = key
decipher.iv = iv
decipher.auth_tag = tag
decipher.auth_data = ""

plain = decipher.update(encrypted) + decipher.final

puts data == plain #=> true

Общедоступные методы класса

OpenSSL::Cipher.ciphers → массив[строка...] Показать исходный код
static VALUE
ossl_s_ciphers(VALUE self)
{
    VALUE ary;

    ary = rb_ary_new();
    OBJ_NAME_do_all_sorted(OBJ_NAME_TYPE_CIPHER_METH,
                    (void(*)(const OBJ_NAME*,void*))add_cipher_name_to_ary,
                    (void*)ary);

    return ary;
}

Возвращает имена всех доступных шифров в виде массива.

new(строка) → шифр Показать исходный код
static VALUE
ossl_cipher_initialize(VALUE self, VALUE str)
{
    EVP_CIPHER_CTX *ctx;
    const EVP_CIPHER *cipher;
    char *name;

    name = StringValuePtr(str);
    GetCipherInit(self, ctx);
    if (ctx) {
        ossl_raise(rb_eRuntimeError, "Cipher already inititalized!");
    }
    AllocCipher(self, ctx);
    EVP_CIPHER_CTX_init(ctx);
    if (!(cipher = EVP_get_cipherbyname(name))) {
        ossl_raise(rb_eRuntimeError, "unsupported cipher algorithm (%s)", name);
    }
    if (EVP_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, -1) != 1)
        ossl_raise(eCipherError, NULL);

    return self;
}

Строка должна содержать допустимое имя шифра, например, “AES-128-CBC” или “3DES”.

Список имен шифров доступен, вызвав ::ciphers.

Методы экземпляра общего доступа

auth_data = строка → строка Показать исходный код
static VALUE
ossl_cipher_set_auth_data(VALUE self, VALUE data)
{
    EVP_CIPHER_CTX *ctx;
    unsigned char *in;
    long in_len, out_len;

    StringValue(data);

    in = (unsigned char *) RSTRING_PTR(data);
    in_len = RSTRING_LEN(data);

    GetCipher(self, ctx);
    if (!(EVP_CIPHER_flags(EVP_CIPHER_CTX_cipher(ctx)) & EVP_CIPH_FLAG_AEAD_CIPHER))
        ossl_raise(eCipherError, "AEAD not supported by this cipher");

    if (!ossl_cipher_update_long(ctx, NULL, &out_len, in, in_len))
        ossl_raise(eCipherError, "couldn't set additional authenticated data");

    return data;
}

Устанавливает дополнительные данные для проверки подлинности шифра. Это поле должно быть установлено при использовании режимов шифрования AEAD, таких как GCM или CCM. Если не требуется использование связанных данных, этот метод должен все равно вызываться со значением “”. Содержимое этого поля должно быть данными, не требующими защиты, которые будут добавлены к шифрованному тексту, для генерации тега аутентификации, который подтверждает содержимое шифрованного текста.

AAD должны быть установлены до шифрования или дешифрования. В режиме шифрования его нужно установить после вызова #encrypt и установки #key= и #iv=. При дешифровании данные для проверки подлинности должны быть установлены после ключа, IV и, особенно, после установки тега проверки подлинности. То есть, установите их только после вызова #decrypt, #key=, #iv= и #auth_tag= сначала.

auth_tag([ tag_len ] → строка Показать исходный код
static VALUE
ossl_cipher_get_auth_tag(int argc, VALUE *argv, VALUE self)
{
    VALUE vtag_len;
    EVP_CIPHER_CTX *ctx;
    int nid, tag_len;

    if (rb_scan_args(argc, argv, "01", &vtag_len) == 0) {
        tag_len = 16;
    } else {
        tag_len = NUM2INT(vtag_len);
    }

    GetCipher(self, ctx);
    nid = EVP_CIPHER_CTX_nid(ctx);

    if (ossl_is_gcm(nid)) {
        return ossl_get_gcm_auth_tag(ctx, tag_len);
    } else {
        ossl_raise(eCipherError, "authentication tag not supported by this cipher");
        return Qnil; /* dummy */
    }
}

Возвращает тег аутентификации, сгенерированный режимами аутентифицированного шифрования Cipher (например, GCM). Этот тег может храниться вместе с зашифрованным текстом, затем устанавливаться в дешифрующем шифре для аутентификации содержимого зашифрованного текста относительно изменений. Если необязательный целочисленный параметр tag_len указан, возвращаемый тег будет tag_len байтами длиннее. Если параметр опущен, будет возвращён максимальный размер 16 байт. Для максимальной безопасности рекомендуется использовать значение по умолчанию в 16 байт.

Тег может быть получен только после вызова #final.

auth_tag = строка → строка Показать исходный код
static VALUE
ossl_cipher_set_auth_tag(VALUE self, VALUE vtag)
{
    EVP_CIPHER_CTX *ctx;
    int nid;
    unsigned char *tag;
    int tag_len;

    StringValue(vtag);
    tag = (unsigned char *) RSTRING_PTR(vtag);
    tag_len = RSTRING_LENINT(vtag);

    GetCipher(self, ctx);
    nid = EVP_CIPHER_CTX_nid(ctx);

    if (ossl_is_gcm(nid)) {
        ossl_set_gcm_auth_tag(ctx, tag, tag_len);
    } else {
        ossl_raise(eCipherError, "authentication tag not supported by this cipher");
    }

    return vtag;
}

Устанавливает тег аутентификации для проверки содержимого зашифрованного текста. Тег должен быть установлен после вызова #decrypt, #key= и #iv=, но перед назначением связанных данных для проверки подлинности с помощью #auth_data= и, конечно же, перед дешифрованием любого из зашифрованного текста. После выполнения всей дешифровки тег автоматически проверяется в вызове #final.

authenticated? → boolean Показать исходный код
static VALUE
ossl_cipher_is_authenticated(VALUE self)
{
    EVP_CIPHER_CTX *ctx;
    int nid;

    GetCipher(self, ctx);
    nid = EVP_CIPHER_CTX_nid(ctx);

    if (ossl_is_gcm(nid)) {
        return Qtrue;
    } else {
        return Qfalse;
    }
}

Указывает, использует ли этот экземпляр Cipher режим аутентифицированного шифрования.

decrypt → self Показать исходный код
static VALUE
ossl_cipher_decrypt(int argc, VALUE *argv, VALUE self)
{
    return ossl_cipher_init(argc, argv, self, 0);
}

Инициализирует Cipher для дешифрования.

Убедитесь, что вы вызвали #encrypt или #decrypt перед использованием следующих методов:

  • key=, iv=, #random_key, #random_iv, #pkcs5_keyivgen

Внутри вызывается EVP_CipherInit_ex(ctx, NULL, NULL, NULL, NULL, 0).

encrypt → self Показать исходный код
static VALUE
ossl_cipher_encrypt(int argc, VALUE *argv, VALUE self)
{
    return ossl_cipher_init(argc, argv, self, 1);
}

Инициализирует Cipher для шифрования.

Убедитесь, что вы вызвали #encrypt или #decrypt перед использованием следующих методов:

  • key=, iv=, #random_key, #random_iv, #pkcs5_keyivgen

Внутри вызывается EVP_CipherInit_ex(ctx, NULL, NULL, NULL, NULL, 1).

final → строка Показать исходный код
static VALUE
ossl_cipher_final(VALUE self)
{
    EVP_CIPHER_CTX *ctx;
    int out_len;
    VALUE str;

    GetCipher(self, ctx);
    str = rb_str_new(0, EVP_CIPHER_CTX_block_size(ctx));
    if (!EVP_CipherFinal_ex(ctx, (unsigned char *)RSTRING_PTR(str), &out_len))
        ossl_raise(eCipherError, NULL);
    assert(out_len <= RSTRING_LEN(str));
    rb_str_set_len(str, out_len);

    return str;
}

Возвращает оставшиеся данные, хранящиеся в объекте шифра. Дальнейшие вызовы #update или #final вернут мусор. Этот вызов должен всегда выполняться в качестве последнего вызова операции шифрования или дешифрования после передачи всего открытого текста или зашифрованного текста экземпляру Cipher.

Если использовался аутентифицированный шифр, при невозможности успешной аутентификации тега генерируется CipherError. Вызывайте этот метод только после установки тега аутентификации и передачи всего содержимого шифрованного текста в шифр.

iv = строка → строка Показать исходный код
static VALUE
ossl_cipher_set_iv(VALUE self, VALUE iv)
{
    EVP_CIPHER_CTX *ctx;

    StringValue(iv);
    GetCipher(self, ctx);

    if (RSTRING_LEN(iv) < EVP_CIPHER_CTX_iv_length(ctx))
        ossl_raise(eCipherError, "iv length too short");

    if (EVP_CipherInit_ex(ctx, NULL, NULL, NULL, (unsigned char *)RSTRING_PTR(iv), -1) != 1)
        ossl_raise(eCipherError, NULL);

    return iv;
}

Устанавливает IV шифра. Обратите внимание, что так как вы никогда не должны использовать режим ECB, IV всегда явно требуется и должен быть установлен до шифрования. Сам IV можно безопасно передавать открыто, но он должен быть непредсказуемым, чтобы предотвратить определенные виды атак. Для создания безопасного случайного IV можно использовать #random_iv.

Вызывайте этот метод только после вызова #encrypt или #decrypt.

Если не установлен явно, используется значение по умолчанию OpenSSL – все нули (“\0”) IV.

key = строка → строка Показать исходный код
static VALUE
ossl_cipher_set_key(VALUE self, VALUE key)
{
    EVP_CIPHER_CTX *ctx;

    StringValue(key);
    GetCipher(self, ctx);

    if (RSTRING_LEN(key) < EVP_CIPHER_CTX_key_length(ctx))
        ossl_raise(eCipherError, "key length too short");

    if (EVP_CipherInit_ex(ctx, NULL, NULL, (unsigned char *)RSTRING_PTR(key), NULL, -1) != 1)
        ossl_raise(eCipherError, NULL);

    rb_ivar_set(self, id_key_set, Qtrue);

    return key;
}

Устанавливает ключ шифра. Для генерации ключа вы должны либо использовать безопасную случайную строку байтов, либо, если ключ должен быть получен из пароля, полагаться на функциональность PBKDF2, предоставляемую OpenSSL::PKCS5. Для генерации безопасного случайного ключа можно использовать #random_key.

Вызывайте этот метод только после вызова #encrypt или #decrypt.

key_len = целое → целое Показать исходный код
static VALUE
ossl_cipher_set_key_length(VALUE self, VALUE key_length)
{
    int len = NUM2INT(key_length);
    EVP_CIPHER_CTX *ctx;

    GetCipher(self, ctx);
    if (EVP_CIPHER_CTX_set_key_length(ctx, len) != 1)
        ossl_raise(eCipherError, NULL);

    return key_length;
}

Устанавливает длину ключа шифра. Если шифр является шифром фиксированной длины, то попытка установить длину ключа на любое значение, отличное от фиксированного, приводит к ошибке.

В обычных ситуациях вызывать этот метод не нужно (и, вероятно, не следует).

См. EVP_CIPHER_CTX_set_key_length для получения дополнительной информации.

name → строка Показать исходный код
static VALUE
ossl_cipher_name(VALUE self)
{
    EVP_CIPHER_CTX *ctx;

    GetCipher(self, ctx);

    return rb_str_new2(EVP_CIPHER_name(EVP_CIPHER_CTX_cipher(ctx)));
}

Возвращает имя шифра, которое может незначительно отличаться от исходного предоставленного имени.

padding = целое → целое Показать исходный код
static VALUE
ossl_cipher_set_padding(VALUE self, VALUE padding)
{
    EVP_CIPHER_CTX *ctx;
    int pad = NUM2INT(padding);

    GetCipher(self, ctx);
    if (EVP_CIPHER_CTX_set_padding(ctx, pad) != 1)
        ossl_raise(eCipherError, NULL);
    return padding;
}

Включает или выключает дополнение. По умолчанию операции шифрования дополняются стандартным блочным дополнением, и дополнение проверяется и удаляется при дешифровании. Если параметр pad равен нулю, дополнение не выполняется, а общий объём зашифрованных или дешифрованных данных должен быть кратен размеру блока, в противном случае возникнет ошибка.

См. EVP_CIPHER_CTX_set_padding для получения дополнительной информации.

pkcs5_keyivgen(pass [, salt [, iterations [, digest]]] ) → nil Показать исходный код
static VALUE
ossl_cipher_pkcs5_keyivgen(int argc, VALUE *argv, VALUE self)
{
    EVP_CIPHER_CTX *ctx;
    const EVP_MD *digest;
    VALUE vpass, vsalt, viter, vdigest;
    unsigned char key[EVP_MAX_KEY_LENGTH], iv[EVP_MAX_IV_LENGTH], *salt = NULL;
    int iter;

    rb_scan_args(argc, argv, "13", &vpass, &vsalt, &viter, &vdigest);
    StringValue(vpass);
    if(!NIL_P(vsalt)){
        StringValue(vsalt);
        if(RSTRING_LEN(vsalt) != PKCS5_SALT_LEN)
            ossl_raise(eCipherError, "salt must be an 8-octet string");
        salt = (unsigned char *)RSTRING_PTR(vsalt);
    }
    iter = NIL_P(viter) ? 2048 : NUM2INT(viter);
    digest = NIL_P(vdigest) ? EVP_md5() : GetDigestPtr(vdigest);
    GetCipher(self, ctx);
    EVP_BytesToKey(EVP_CIPHER_CTX_cipher(ctx), digest, salt,
                   (unsigned char *)RSTRING_PTR(vpass), RSTRING_LENINT(vpass), iter, key, iv);
    if (EVP_CipherInit_ex(ctx, NULL, NULL, key, iv, -1) != 1)
        ossl_raise(eCipherError, NULL);
    OPENSSL_cleanse(key, sizeof key);
    OPENSSL_cleanse(iv, sizeof iv);

    rb_ivar_set(self, id_key_set, Qtrue);

    return Qnil;
}

Генерирует и устанавливает ключ/IV на основе пароля.

ПРЕДУПРЕЖДЕНИЕ: Этот метод совместим только с PKCS5 v1.5 при использовании RC2, RC4-40 или DES с MD5 или SHA1. Использование чего-либо еще (например, AES) сгенерирует ключ/IV с помощью метода, специфичного для OpenSSL. Этот метод устарел и больше не должен использоваться. Используйте метод генерации ключей PKCS5 v2 из OpenSSL::PKCS5 вместо этого.

Параметры

salt должен быть строкой длиной 8 байтов, если указан. iterations является целым числом со значением по умолчанию 2048. digest - объект Digest, по умолчанию 'MD5'.

Рекомендуется минимум 1000 итераций.

random_iv() Показать исходный код
# File ext/openssl/lib/openssl/cipher.rb, line 48
def random_iv
  str = OpenSSL::Random.random_bytes(self.iv_len)
  self.iv = str
  return str
end

Генерирует, устанавливает и возвращает случайный iv. Вы должны вызвать cipher.encrypt или cipher.decrypt перед вызовом этого метода.

END_OF_DOCUMENT_MARKER
random_key() Показать исходный код
# File ext/openssl/lib/openssl/cipher.rb, line 40
def random_key
  str = OpenSSL::Random.random_bytes(self.key_len)
  self.key = str
  return str
end

Генерирует, устанавливает и возвращает случайный ключ. Необходимо вызвать cipher.encrypt или cipher.decrypt перед вызовом этого метода.

reset → self Показать исходный код
static VALUE
ossl_cipher_reset(VALUE self)
{
    EVP_CIPHER_CTX *ctx;

    GetCipher(self, ctx);
    if (EVP_CipherInit_ex(ctx, NULL, NULL, NULL, NULL, -1) != 1)
        ossl_raise(eCipherError, NULL);

    return self;
}

Полностью сбрасывает внутреннее состояние Cipher. Это позволяет использовать один экземпляр Cipher для нескольких задач шифрования или дешифрования.

Внутренне вызывает EVP_CipherInit_ex(ctx, NULL, NULL, NULL, NULL, -1).

update(data [, buffer]) → string or buffer Показать исходный код
static VALUE
ossl_cipher_update(int argc, VALUE *argv, VALUE self)
{
    EVP_CIPHER_CTX *ctx;
    unsigned char *in;
    long in_len, out_len;
    VALUE data, str;

    rb_scan_args(argc, argv, "11", &data, &str);

    if (!RTEST(rb_attr_get(self, id_key_set)))
        ossl_raise(eCipherError, "key not set");

    StringValue(data);
    in = (unsigned char *)RSTRING_PTR(data);
    if ((in_len = RSTRING_LEN(data)) == 0)
        ossl_raise(rb_eArgError, "data must not be empty");
    GetCipher(self, ctx);
    out_len = in_len+EVP_CIPHER_CTX_block_size(ctx);
    if (out_len <= 0) {
        ossl_raise(rb_eRangeError,
                   "data too big to make output buffer: %ld bytes", in_len);
    }

    if (NIL_P(str)) {
        str = rb_str_new(0, out_len);
    } else {
        StringValue(str);
        rb_str_resize(str, out_len);
    }

    if (!ossl_cipher_update_long(ctx, (unsigned char *)RSTRING_PTR(str), &out_len, in, in_len))
        ossl_raise(eCipherError, NULL);
    assert(out_len < RSTRING_LEN(str));
    rb_str_set_len(str, out_len);

    return str;
}

Шифрует данные потоковым способом. Передавайте последовательные блоки данных методу update для их шифрования. Возвращает зашифрованный фрагмент данных. По завершении необходимо добавить результат вызова #final к результату.

Параметры

data - это непустая строка. buffer - это необязательная строка для хранения результата.

Ruby Core © 1993–2017 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API