[PATCH] Security: Keys: Added derived keytype

From: Kirill Marinushkin
Date: Mon Mar 21 2016 - 20:46:28 EST


For details see
Documentation/security/keys-derived.txt

Signed-off-by: Kirill Marinushkin <k.marinushkin@xxxxxxxxx>
---
Documentation/security/keys-derived.txt | 82 +++++
include/keys/derived-type.h | 33 ++
security/keys/Kconfig | 11 +
security/keys/Makefile | 1 +
security/keys/derived.c | 577 ++++++++++++++++++++++++++++++++
5 files changed, 704 insertions(+)
create mode 100644 Documentation/security/keys-derived.txt
create mode 100644 include/keys/derived-type.h
create mode 100644 security/keys/derived.c

diff --git a/Documentation/security/keys-derived.txt b/Documentation/security/keys-derived.txt
new file mode 100644
index 0000000..3c1d65c
--- /dev/null
+++ b/Documentation/security/keys-derived.txt
@@ -0,0 +1,82 @@
+ Derived Keys
+
+Derived is a keytype of the kernel keyring facility.
+The key secret is derived from the secret value given by user.
+Optionally user may specify:
+ - hash function used for derivation;
+ - salt value;
+ - number of iterations.
+Both secret value and salt value may be given in one of the formats:
+ - plain data;
+ - hex string;
+ - size of data to generate randomly.
+If no optional parameters are specified, the key is derived from
+the plain secret value with sha256, no salt, 1 iteration.
+Derived keys store as a payload:
+ - derived key;
+ - salt;
+ - number of iterations;
+ - name of derivation algorithm;
+ - name of RNG algorithm.
+From userspace only the derived key value is returned on read.
+
+Usage:
+ keyctl add derived name "key [options]" ring
+
+ mandatory parameter:
+ key - key secret value
+
+ options:
+ kf=, keyformat= - key secret value format, see dataformat below
+ s=, salt= - salt value,
+ default is empty (no salt)
+ sf=, saltformat= - salt value format, see dataformat below
+ i=, iterations= - number of itaretions,
+ default is 1, maximum is 0x000FFFFF
+ a=, algorithm= - name of crypto API hash derivation algorithm,
+ default is sha256
+ r=, rng= - name of crypto API RNG algorithm,
+ default is stdrng
+
+ dataformat:
+ plain - data is a plain value, used by default
+ hex - data is a hex string
+ rand - data is a size of random value to be generated
+
+Examples:
+
+Create a simple derived key
+
+ $ keyctl add derived key0 secret0 @u
+ 925448848
+
+ $ keyctl read 925448848
+ 32 bytes of data in key:
+ 97699b7c c0a0ed83 b78b2002 f0e57046 ee561be6 942bec25 6fe201ab ba552a9e
+
+Create a derived key from plain secret, hex salt
+
+ $ keyctl add derived key0 "secret0 s=65a4fe09 sf=hex" @u
+ 847728695
+
+ $ keyctl read 847728695
+ 32 bytes of data in key:
+ 1c64cbb9 cc4dffff a94f8efe dce813d0 5def4a28 97c02336 6c95737b f2b152be
+
+Create a derived key from hex secret value, 32-byte random salt, 65536 iterations
+
+ $keyctl add derived key0 "09afde6781ff kf=hex s=32 sf=rand i=65536" @u
+ 604146072
+
+ $ keyctl read 604146072
+ 32 bytes of data in key:
+ a5b494b3 b6e3e26c bb9511b1 b16ce60e 99edf63e d8fbc3c2 ba38b195 229e3f43
+
+Create a derived key with sha1 algorithm
+
+ $ keyctl add derived key0 "secret0 a=sha1" @u
+ 56670858
+
+ $ keyctl read 56670858
+ 20 bytes of data in key:
+ d16cd26f bc3d44a6 16b8d0b2 ce8b5ddc c93e964d
diff --git a/include/keys/derived-type.h b/include/keys/derived-type.h
new file mode 100644
index 0000000..24772a1
--- /dev/null
+++ b/include/keys/derived-type.h
@@ -0,0 +1,33 @@
+/*
+ * Derived key type
+ *
+ * For details see
+ * Documentation/security/keys-derived.txt
+ *
+ * Copyright (C) 2016
+ * Written by Kirill Marinushkin (kmarinushkin@xxxxxxxxx)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public Licence
+ * as published by the Free Software Foundation; either version
+ * 2 of the Licence, or (at your option) any later version.
+ *
+ */
+
+#ifndef INCLUDE_KEYS_DERIVED_TYPE_H_
+#define INCLUDE_KEYS_DERIVED_TYPE_H_
+
+#include <linux/key.h>
+
+extern struct key_type key_type_derived;
+
+extern int derived_instantiate(struct key *key,
+ struct key_preparsed_payload *prep);
+extern int derived_update(struct key *key,
+ struct key_preparsed_payload *prep);
+extern long derived_read(const struct key *key,
+ char __user *buffer, size_t buflen);
+extern void derived_revoke(struct key *key);
+extern void derived_destroy(struct key *key);
+
+#endif /* INCLUDE_KEYS_DERIVED_TYPE_H_ */
diff --git a/security/keys/Kconfig b/security/keys/Kconfig
index fe4d74e..261994f 100644
--- a/security/keys/Kconfig
+++ b/security/keys/Kconfig
@@ -81,3 +81,14 @@ config ENCRYPTED_KEYS
Userspace only ever sees/stores encrypted blobs.

If you are unsure as to whether this is required, answer N.
+
+config DERIVED_KEYS
+ tristate "Derived keys"
+ depends on KEYS
+ select CRYPTO
+ select CRYPTO_SHA256
+ select CRYPTO_RNG
+ help
+ This option provides support for derived key type.
+
+ If you are unsure as to whether this is required, answer N.
diff --git a/security/keys/Makefile b/security/keys/Makefile
index dfb3a7b..fbe954d 100644
--- a/security/keys/Makefile
+++ b/security/keys/Makefile
@@ -26,3 +26,4 @@ obj-$(CONFIG_PERSISTENT_KEYRINGS) += persistent.o
obj-$(CONFIG_BIG_KEYS) += big_key.o
obj-$(CONFIG_TRUSTED_KEYS) += trusted.o
obj-$(CONFIG_ENCRYPTED_KEYS) += encrypted-keys/
+obj-$(CONFIG_DERIVED_KEYS) += derived.o
diff --git a/security/keys/derived.c b/security/keys/derived.c
new file mode 100644
index 0000000..18085ce
--- /dev/null
+++ b/security/keys/derived.c
@@ -0,0 +1,577 @@
+/*
+ * Derived key type
+ *
+ * For details see
+ * Documentation/security/keys-derived.txt
+ *
+ * Copyright (C) 2016
+ * Written by Kirill Marinushkin (kmarinushkin@xxxxxxxxx)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public Licence
+ * as published by the Free Software Foundation; either version
+ * 2 of the Licence, or (at your option) any later version.
+ *
+ */
+
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/slab.h>
+#include <linux/seq_file.h>
+#include <linux/err.h>
+#include <linux/parser.h>
+#include <linux/key.h>
+#include <linux/string.h>
+#include <linux/uaccess.h>
+#include <keys/user-type.h>
+#include <keys/derived-type.h>
+#include <crypto/hash.h>
+#include <crypto/rng.h>
+#include "internal.h"
+
+/* KERN_ERR prefix */
+#define PREFIX "derived: "
+
+/* Limits */
+#define ITER_MAX_VAL 0x000FFFFF
+#define SALT_MAX_SIZE 1024
+#define RAND_MAX_SIZE 1024
+
+/* Default values */
+#define ITER_DEFAULT 1
+#define ALG_NAME_DEFAULT "sha256"
+#define RNG_NAME_DEFAULT "stdrng"
+
+/* Options */
+enum {
+ OPT_SHORT_SALT,
+ OPT_LONG_SALT,
+ OPT_SHORT_ITER,
+ OPT_LONG_ITER,
+ OPT_SHORT_ALG,
+ OPT_LONG_ALG,
+ OPT_SHORT_RNG,
+ OPT_LONG_RNG,
+ OPT_SHORT_KEY_F,
+ OPT_LONG_KEY_F,
+ OPT_SHORT_SALT_F,
+ OPT_LONG_SALT_F
+};
+
+/* Options data formats */
+enum derived_opt_format {
+ OPT_FORMAT_ERR = -1,
+ OPT_FORMAT_PLAIN,
+ OPT_FORMAT_HEX,
+ OPT_FORMAT_RAND
+};
+
+/* Options data index */
+enum {
+ OPT_IND_KEY = 0,
+ OPT_IND_SALT,
+ OPT_IND_NUM /* number of indexes */
+};
+
+struct derived_blob {
+ u8 *data;
+ size_t *lenp;
+};
+
+struct derived_f_blob {
+ enum derived_opt_format format;
+ struct derived_blob *b;
+};
+
+struct derived_key_payload {
+ struct rcu_head rcu; /* RCU destructor */
+ char *alg_name; /* null-terminated digest algorithm name */
+ char *rng_name; /* null-terminated random generator algorithm name */
+ u64 iter; /* number of iterations */
+ unsigned int saltlen; /* length of salt */
+ unsigned char *salt; /* salt */
+ unsigned int datalen; /* length of derived data */
+ unsigned char *data; /* derived data */
+};
+
+/* Get option data format specified by user */
+static enum derived_opt_format get_opt_format(const char *arg)
+{
+ if (!strcmp(arg, "plain"))
+ return OPT_FORMAT_PLAIN;
+ if (!strcmp(arg, "hex"))
+ return OPT_FORMAT_HEX;
+ if (!strcmp(arg, "rand"))
+ return OPT_FORMAT_RAND;
+ return OPT_FORMAT_ERR;
+}
+
+/* Generate random data */
+static int gen_random(const char *rnd_name, u8 *buf, unsigned int len)
+{
+ int ret = -EINVAL;
+ struct crypto_rng *rng = NULL;
+
+ rng = crypto_alloc_rng(rnd_name, 0, 0);
+ if (IS_ERR(rng)) {
+ pr_err(PREFIX "RNG alloc failed");
+ return -EINVAL;
+ }
+
+ ret = crypto_rng_get_bytes(rng, buf, len);
+ if (ret < 0) {
+ pr_err(PREFIX "RNG get bytes failed");
+ ret = -EFAULT;
+ }
+
+ if (rng)
+ crypto_free_rng(rng);
+ return ret;
+}
+
+/* Parse options specified by user */
+static int parse_options(char **args_str,
+ struct derived_key_payload *payload, struct derived_blob *ukey)
+{
+ int ret = -EINVAL;
+ substring_t args[MAX_OPT_ARGS];
+ char *p = *args_str;
+ int token;
+ int i;
+ unsigned short templen;
+ unsigned int tempu;
+ u64 tempul;
+ struct derived_blob usalt = {NULL};
+ struct derived_f_blob v[OPT_IND_NUM] = {
+ {OPT_FORMAT_PLAIN, NULL}
+ };
+ const match_table_t key_tokens = {
+ {OPT_SHORT_SALT, "s=%s"},
+ {OPT_LONG_SALT, "salt=%s"},
+ {OPT_SHORT_ITER, "i=%u"},
+ {OPT_LONG_ITER, "iterations=%u"},
+ {OPT_SHORT_ALG, "a=%s"},
+ {OPT_LONG_ALG, "algorithm=%s"},
+ {OPT_SHORT_RNG, "r=%s"},
+ {OPT_LONG_RNG, "rng=%s"},
+ {OPT_SHORT_KEY_F, "kf=%s"},
+ {OPT_LONG_KEY_F, "keyformat=%s"},
+ {OPT_SHORT_SALT_F, "sf=%s"},
+ {OPT_LONG_SALT_F, "saltformat=%s"}
+ };
+
+ /* set defaults */
+ payload->iter = ITER_DEFAULT;
+ payload->alg_name = kstrdup(ALG_NAME_DEFAULT, GFP_KERNEL);
+ if (!payload->alg_name) {
+ pr_err(PREFIX "default algorithm name alloc failed");
+ return -ENOMEM;
+ }
+ payload->rng_name = kstrdup(RNG_NAME_DEFAULT, GFP_KERNEL);
+ if (!payload->rng_name) {
+ pr_err(PREFIX "default RNG name alloc failed");
+ return -ENOMEM;
+ }
+
+ /* parse key */
+ ukey->data = strsep(args_str, " \t");
+ if (!ukey->data) {
+ pr_err(PREFIX "input string separation failed");
+ return -EINVAL;
+ }
+ ukey->lenp = kmalloc(sizeof(*ukey->lenp), GFP_KERNEL);
+ if (!ukey->lenp) {
+ pr_err(PREFIX "input key secret alloc failed");
+ return -ENOMEM;
+ }
+ *ukey->lenp = strlen(ukey->data);
+
+ /* prepare format blob array */
+ v[OPT_IND_KEY].b = ukey;
+ v[OPT_IND_SALT].b = &usalt;
+
+ /* parse options */
+ while ((p = strsep(args_str, " \t"))) {
+ if (*p == '\0' || *p == ' ' || *p == '\t')
+ continue;
+
+ token = match_token(p, key_tokens, args);
+
+ switch (token) {
+
+ case OPT_SHORT_SALT: /* salt */
+ case OPT_LONG_SALT:
+ templen = args[0].to - args[0].from;
+ if (templen < 0 || templen > SALT_MAX_SIZE) {
+ pr_err(PREFIX "invalid salt length");
+ return -EINVAL;
+ }
+ payload->salt = kstrndup(args[0].from, templen, GFP_KERNEL);
+ if (!payload->salt) {
+ pr_err(PREFIX "salt alloc failed");
+ return -ENOMEM;
+ }
+ payload->saltlen = templen;
+ usalt.data = payload->salt;
+ usalt.lenp = &payload->saltlen;
+ break;
+
+ case OPT_SHORT_ITER: /* iterations */
+ case OPT_LONG_ITER:
+ if (kstrtou64(args[0].from, 0, &tempul)
+ || tempul == 0
+ || tempul > ITER_MAX_VAL) {
+ pr_err(PREFIX "invalid iterations number");
+ return -EINVAL;
+ }
+ payload->iter = tempul;
+ break;
+
+ case OPT_SHORT_ALG: /* alg name */
+ case OPT_LONG_ALG:
+ payload->alg_name = kstrdup(args[0].from, GFP_KERNEL);
+ if (!payload->alg_name) {
+ pr_err(PREFIX "algorithm name alloc failed");
+ return -ENOMEM;
+ }
+ break;
+
+ case OPT_SHORT_RNG: /* rng name */
+ case OPT_LONG_RNG:
+ payload->rng_name = kstrdup(args[0].from, GFP_KERNEL);
+ if (!payload->rng_name) {
+ pr_err(PREFIX "RNG name alloc failed");
+ return -ENOMEM;
+ }
+ break;
+
+ case OPT_SHORT_KEY_F: /* key format */
+ case OPT_LONG_KEY_F:
+ v[OPT_IND_KEY].format = get_opt_format(args[0].from);
+ if (v[OPT_IND_KEY].format == OPT_FORMAT_ERR) {
+ pr_err(PREFIX "invalid key format");
+ return -EINVAL;
+ }
+ break;
+
+ case OPT_SHORT_SALT_F: /* salt format */
+ case OPT_LONG_SALT_F:
+ v[OPT_IND_SALT].format = get_opt_format(args[0].from);
+ if (v[OPT_IND_SALT].format == OPT_FORMAT_ERR) {
+ pr_err(PREFIX "invalid salt format");
+ return -EINVAL;
+ }
+ break;
+
+ default:
+ pr_err(PREFIX "unsupported option");
+ return -EINVAL;
+ }
+ }
+
+ /* modify options according to format */
+ for (i = 0; i < OPT_IND_NUM; i++) {
+ if (!v[i].b || !v[i].b->data)
+ continue;
+
+ switch (v[i].format) {
+
+ case OPT_FORMAT_HEX:
+ if (*v[i].b->lenp % 2) {
+ pr_err(PREFIX "invalid hex string");
+ return -EINVAL;
+ }
+ *v[i].b->lenp /= 2;
+ ret = hex2bin(v[i].b->data, v[i].b->data, *v[i].b->lenp);
+ if (ret) {
+ pr_err(PREFIX "invalid hex string");
+ return -EINVAL;
+ }
+ break;
+
+ case OPT_FORMAT_RAND:
+ if (kstrtouint(v[i].b->data, 0, &tempu)
+ || tempu == 0
+ || tempu > RAND_MAX_SIZE) {
+ pr_err(PREFIX "invalid random size");
+ return -EINVAL;
+ }
+ v[i].b->data = kmalloc(tempu, GFP_KERNEL);
+ if (!v[i].b->data) {
+ pr_err(PREFIX "random data alloc failed");
+ return -ENOMEM;
+ }
+ *v[i].b->lenp = tempu;
+ ret = gen_random(payload->rng_name, v[i].b->data, *v[i].b->lenp);
+ if (ret)
+ return ret;
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ return 0;
+}
+
+/* Free and zero payload fields */
+static void free_payload_content(struct derived_key_payload *payload)
+{
+ if (payload->alg_name)
+ kzfree(payload->alg_name);
+ if (payload->rng_name)
+ kzfree(payload->rng_name);
+ if (payload->data)
+ kzfree(payload->data);
+ if (payload->salt)
+ kzfree(payload->salt);
+}
+
+/* Fill derived key payload with data specified by user */
+static int fill_payload(struct derived_key_payload *payload,
+ struct key_preparsed_payload *prep)
+{
+ int ret = -EINVAL;
+ char *args_str = NULL;
+ struct derived_blob ukey = {NULL};
+ struct crypto_shash *sh = NULL;
+ struct shash_desc *sdesc = NULL;
+ unsigned int i;
+
+ if (!payload || prep->datalen <= 0 || prep->datalen > 32767 || !prep->data) {
+ pr_err(PREFIX "invalid data for payload");
+ return -EINVAL;
+ }
+
+ args_str = kstrndup(prep->data, prep->datalen, GFP_KERNEL);
+ if (!args_str) {
+ pr_err(PREFIX "input arguments alloc failed");
+ return -EINVAL;
+ }
+
+ ret = parse_options(&args_str, payload, &ukey);
+ if (ret)
+ return ret;
+ if (!ukey.data || !ukey.lenp) {
+ pr_err(PREFIX "invalid key input parsed");
+ return -EINVAL;
+ }
+
+ /* start derivation */
+ sh = crypto_alloc_shash(payload->alg_name, 0, 0);
+ if (IS_ERR(sh)) {
+ pr_err(PREFIX "shash alloc failed");
+ ret = -EINVAL;
+ goto out;
+ }
+
+ sdesc = kzalloc(sizeof(struct shash_desc) + crypto_shash_descsize(sh), GFP_KERNEL);
+ if (!sdesc) {
+ pr_err(PREFIX "sdesc alloc failed");
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ sdesc->tfm = sh;
+ sdesc->flags = 0;
+
+ payload->datalen = crypto_shash_digestsize(sh);
+ if (payload->data)
+ kzfree(payload->data);
+ payload->data = kmalloc(payload->datalen, GFP_KERNEL);
+ if (!payload->data) {
+ pr_err(PREFIX "payload data alloc failed");
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ for (i = 0; i < payload->iter; i++) {
+ ret = crypto_shash_init(sdesc);
+ if (ret) {
+ pr_err(PREFIX "shash init failed");
+ goto out;
+ }
+
+ if (i == 0) {
+ /* first iteration */
+ ret = crypto_shash_update(sdesc, ukey.data, *ukey.lenp);
+ if (ret) {
+ pr_err(PREFIX "shash update failed");
+ goto out;
+ }
+
+ ret = crypto_shash_update(sdesc, payload->salt, payload->saltlen);
+ if (ret) {
+ pr_err(PREFIX "shash update failed");
+ goto out;
+ }
+ } else {
+ /* next iterations */
+ ret = crypto_shash_update(sdesc, payload->data, payload->datalen);
+ if (ret) {
+ pr_err(PREFIX "shash update failed");
+ goto out;
+ }
+ }
+
+ ret = crypto_shash_final(sdesc, payload->data);
+ if (ret) {
+ pr_err(PREFIX "shash final failed");
+ goto out;
+ }
+
+ }
+
+out:
+ if (sdesc)
+ kzfree(sdesc);
+ if (!IS_ERR(sh))
+ crypto_free_shash(sh);
+ if (args_str)
+ kzfree(args_str);
+ return ret;
+}
+
+/* Reserve payload for derived key */
+static int reserve_derived_payload(struct key *key,
+ struct derived_key_payload *payload)
+{
+ return key_payload_reserve(key, sizeof(*payload)
+ + payload->datalen + payload->saltlen
+ + strlen(payload->alg_name) + strlen(payload->rng_name) + 2);
+}
+
+/* Derived key instantiate */
+int derived_instantiate(struct key *key, struct key_preparsed_payload *prep)
+{
+ int ret = -EINVAL;
+ struct derived_key_payload *payload = NULL;
+
+ if (prep->datalen <= 0 || prep->datalen > 32767 || !prep->data) {
+ pr_err(PREFIX "invalid input data");
+ return -EINVAL;
+ }
+
+ payload = kzalloc(sizeof(*payload), GFP_KERNEL);
+ if (!payload) {
+ pr_err(PREFIX "payload alloc failed");
+ return -ENOMEM;
+ }
+
+ /* fill payload */
+ ret = fill_payload(payload, prep);
+ if (!ret)
+ ret = reserve_derived_payload(key, payload);
+
+ /* assign key if succeed */
+ if (!ret)
+ rcu_assign_keypointer(key, payload);
+ else
+ kzfree(key->payload.data);
+
+ return ret;
+}
+EXPORT_SYMBOL_GPL(derived_instantiate);
+
+/* Derived key update */
+int derived_update(struct key *key, struct key_preparsed_payload *prep)
+{
+ int ret = -EINVAL;
+ struct derived_key_payload *payload =
+ (struct derived_key_payload *)key->payload.data;
+
+ /* free current payload */
+ free_payload_content(payload);
+ memset(payload, 0x00, sizeof(*payload));
+
+ ret = fill_payload(payload, prep);
+ if (!ret)
+ ret = reserve_derived_payload(key, payload);
+
+ return ret;
+}
+EXPORT_SYMBOL_GPL(derived_update);
+
+/* Derived key read */
+long derived_read(const struct key *key, char __user *buffer, size_t buflen)
+{
+ long len = -1;
+ struct derived_key_payload *payload = rcu_dereference_key(key);
+
+ if (!payload) {
+ pr_err(PREFIX "invalid key payload");
+ return -EINVAL;
+ }
+
+ len = payload->datalen;
+ if (buffer && buflen > 0) {
+ /* copy to buffer */
+ if (buflen < payload->datalen
+ || copy_to_user(buffer, payload->data, payload->datalen)) {
+ pr_err(PREFIX "read key data failed");
+ return -EFAULT;
+ }
+ } /* else return without copy */
+
+ return len;
+}
+EXPORT_SYMBOL_GPL(derived_read);
+
+/* Derived key revoke */
+void derived_revoke(struct key *key)
+{
+ struct derived_key_payload *payload =
+ (struct derived_key_payload *)key->payload.data;
+
+ /* clear the quota */
+ key_payload_reserve(key, 0);
+
+ if (payload) {
+ rcu_assign_keypointer(key, NULL);
+ kfree_rcu(payload, rcu);
+ }
+}
+EXPORT_SYMBOL(derived_revoke);
+
+/* Derived key destroy */
+void derived_destroy(struct key *key)
+{
+ struct derived_key_payload *payload =
+ (struct derived_key_payload *)key->payload.data;
+
+ if (!payload)
+ return;
+
+ free_payload_content(payload);
+
+ kzfree(payload);
+}
+EXPORT_SYMBOL_GPL(derived_destroy);
+
+struct key_type key_type_derived = {
+ .name = "derived",
+ .instantiate = derived_instantiate,
+ .update = derived_update,
+ .destroy = derived_destroy,
+ .revoke = derived_revoke,
+ .describe = user_describe,
+ .read = derived_read,
+};
+EXPORT_SYMBOL_GPL(key_type_derived);
+
+static int __init init_derived(void)
+{
+ return register_key_type(&key_type_derived);
+}
+
+static void __exit cleanup_derived(void)
+{
+ unregister_key_type(&key_type_derived);
+}
+
+late_initcall(init_derived);
+module_exit(cleanup_derived);
+
+MODULE_LICENSE("GPL");
--
1.9.1