Commit f9ed236c authored by David S. Miller's avatar David S. Miller

Merge branch 'for-upstream' of...

Merge branch 'for-upstream' of git://git.kernel.org/pub/scm/linux/kernel/git/bluetooth/bluetooth-next

Johan Hedberg says:

====================
pull request: bluetooth-next 2017-04-30

Here's one last batch of Bluetooth patches in the bluetooth-next tree
targeting the 4.12 kernel.

 - Remove custom ECDH implementation and use new KPP API instead
 - Add protocol checks to hci_ldisc
 - Add module license to HCI UART Nokia H4+ driver
 - Minor fix for 32bit user space - 64 bit kernel combination

Please let me know if there are any issues pulling. Thanks.
====================
Signed-off-by: default avatarDavid S. Miller <davem@davemloft.net>
parents d5066c46 71653eb6
...@@ -113,16 +113,21 @@ static inline struct sk_buff *hci_uart_dequeue(struct hci_uart *hu) ...@@ -113,16 +113,21 @@ static inline struct sk_buff *hci_uart_dequeue(struct hci_uart *hu)
{ {
struct sk_buff *skb = hu->tx_skb; struct sk_buff *skb = hu->tx_skb;
if (!skb) if (!skb) {
if (test_bit(HCI_UART_PROTO_READY, &hu->flags))
skb = hu->proto->dequeue(hu); skb = hu->proto->dequeue(hu);
else } else {
hu->tx_skb = NULL; hu->tx_skb = NULL;
}
return skb; return skb;
} }
int hci_uart_tx_wakeup(struct hci_uart *hu) int hci_uart_tx_wakeup(struct hci_uart *hu)
{ {
if (!test_bit(HCI_UART_PROTO_READY, &hu->flags))
return 0;
if (test_and_set_bit(HCI_UART_SENDING, &hu->tx_state)) { if (test_and_set_bit(HCI_UART_SENDING, &hu->tx_state)) {
set_bit(HCI_UART_TX_WAKEUP, &hu->tx_state); set_bit(HCI_UART_TX_WAKEUP, &hu->tx_state);
return 0; return 0;
...@@ -256,6 +261,9 @@ static int hci_uart_send_frame(struct hci_dev *hdev, struct sk_buff *skb) ...@@ -256,6 +261,9 @@ static int hci_uart_send_frame(struct hci_dev *hdev, struct sk_buff *skb)
BT_DBG("%s: type %d len %d", hdev->name, hci_skb_pkt_type(skb), BT_DBG("%s: type %d len %d", hdev->name, hci_skb_pkt_type(skb),
skb->len); skb->len);
if (!test_bit(HCI_UART_PROTO_READY, &hu->flags))
return -EUNATCH;
hu->proto->enqueue(hu, skb); hu->proto->enqueue(hu, skb);
hci_uart_tx_wakeup(hu); hci_uart_tx_wakeup(hu);
......
...@@ -36,6 +36,8 @@ ...@@ -36,6 +36,8 @@
#include "hci_uart.h" #include "hci_uart.h"
#include "btbcm.h" #include "btbcm.h"
#define VERSION "0.1"
#define NOKIA_ID_BCM2048 0x04 #define NOKIA_ID_BCM2048 0x04
#define NOKIA_ID_TI1271 0x31 #define NOKIA_ID_TI1271 0x31
...@@ -818,3 +820,8 @@ static struct serdev_device_driver nokia_bluetooth_serdev_driver = { ...@@ -818,3 +820,8 @@ static struct serdev_device_driver nokia_bluetooth_serdev_driver = {
}; };
module_serdev_device_driver(nokia_bluetooth_serdev_driver); module_serdev_device_driver(nokia_bluetooth_serdev_driver);
MODULE_AUTHOR("Sebastian Reichel <sre@kernel.org>");
MODULE_DESCRIPTION("Bluetooth HCI UART Nokia H4+ driver ver " VERSION);
MODULE_VERSION(VERSION);
MODULE_LICENSE("GPL");
...@@ -13,6 +13,7 @@ menuconfig BT ...@@ -13,6 +13,7 @@ menuconfig BT
select CRYPTO_CMAC select CRYPTO_CMAC
select CRYPTO_ECB select CRYPTO_ECB
select CRYPTO_SHA256 select CRYPTO_SHA256
select CRYPTO_ECDH
help help
Bluetooth is low-cost, low-power, short-range wireless technology. Bluetooth is low-cost, low-power, short-range wireless technology.
It was designed as a replacement for cables and other short-range It was designed as a replacement for cables and other short-range
......
...@@ -13,7 +13,7 @@ bluetooth_6lowpan-y := 6lowpan.o ...@@ -13,7 +13,7 @@ bluetooth_6lowpan-y := 6lowpan.o
bluetooth-y := af_bluetooth.o hci_core.o hci_conn.o hci_event.o mgmt.o \ bluetooth-y := af_bluetooth.o hci_core.o hci_conn.o hci_event.o mgmt.o \
hci_sock.o hci_sysfs.o l2cap_core.o l2cap_sock.o smp.o lib.o \ hci_sock.o hci_sysfs.o l2cap_core.o l2cap_sock.o smp.o lib.o \
ecc.o hci_request.o mgmt_util.o ecdh_helper.o hci_request.o mgmt_util.o
bluetooth-$(CONFIG_BT_BREDR) += sco.o bluetooth-$(CONFIG_BT_BREDR) += sco.o
bluetooth-$(CONFIG_BT_HS) += a2mp.o amp.o bluetooth-$(CONFIG_BT_HS) += a2mp.o amp.o
......
This diff is collapsed.
/*
* Copyright (c) 2013, Kenneth MacKay
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/* Create a public/private key pair.
* Outputs:
* public_key - Will be filled in with the public key.
* private_key - Will be filled in with the private key.
*
* Returns true if the key pair was generated successfully, false
* if an error occurred. The keys are with the LSB first.
*/
bool ecc_make_key(u8 public_key[64], u8 private_key[32]);
/* Compute a shared secret given your secret key and someone else's
* public key.
* Note: It is recommended that you hash the result of ecdh_shared_secret
* before using it for symmetric encryption or HMAC.
*
* Inputs:
* public_key - The public key of the remote party
* private_key - Your private key.
*
* Outputs:
* secret - Will be filled in with the shared secret value.
*
* Returns true if the shared secret was generated successfully, false
* if an error occurred. Both input and output parameters are with the
* LSB first.
*/
bool ecdh_shared_secret(const u8 public_key[64], const u8 private_key[32],
u8 secret[32]);
/*
* ECDH helper functions - KPP wrappings
*
* Copyright (C) 2017 Intel Corporation
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation;
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER(S) AND AUTHOR(S) BE LIABLE FOR ANY
* CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* ALL LIABILITY, INCLUDING LIABILITY FOR INFRINGEMENT OF ANY PATENTS,
* COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS, RELATING TO USE OF THIS
* SOFTWARE IS DISCLAIMED.
*/
#include "ecdh_helper.h"
#include <linux/scatterlist.h>
#include <crypto/kpp.h>
#include <crypto/ecdh.h>
struct ecdh_completion {
struct completion completion;
int err;
};
static void ecdh_complete(struct crypto_async_request *req, int err)
{
struct ecdh_completion *res = req->data;
if (err == -EINPROGRESS)
return;
res->err = err;
complete(&res->completion);
}
static inline void swap_digits(u64 *in, u64 *out, unsigned int ndigits)
{
int i;
for (i = 0; i < ndigits; i++)
out[i] = __swab64(in[ndigits - 1 - i]);
}
bool compute_ecdh_secret(const u8 public_key[64], const u8 private_key[32],
u8 secret[32])
{
struct crypto_kpp *tfm;
struct kpp_request *req;
struct ecdh p;
struct ecdh_completion result;
struct scatterlist src, dst;
u8 *tmp, *buf;
unsigned int buf_len;
int err = -ENOMEM;
tmp = kmalloc(64, GFP_KERNEL);
if (!tmp)
return false;
tfm = crypto_alloc_kpp("ecdh", CRYPTO_ALG_INTERNAL, 0);
if (IS_ERR(tfm)) {
pr_err("alg: kpp: Failed to load tfm for kpp: %ld\n",
PTR_ERR(tfm));
goto free_tmp;
}
req = kpp_request_alloc(tfm, GFP_KERNEL);
if (!req)
goto free_kpp;
init_completion(&result.completion);
/* Security Manager Protocol holds digits in litte-endian order
* while ECC API expect big-endian data
*/
swap_digits((u64 *)private_key, (u64 *)tmp, 4);
p.key = (char *)tmp;
p.key_size = 32;
/* Set curve_id */
p.curve_id = ECC_CURVE_NIST_P256;
buf_len = crypto_ecdh_key_len(&p);
buf = kmalloc(buf_len, GFP_KERNEL);
if (!buf) {
pr_err("alg: kpp: Failed to allocate %d bytes for buf\n",
buf_len);
goto free_req;
}
crypto_ecdh_encode_key(buf, buf_len, &p);
/* Set A private Key */
err = crypto_kpp_set_secret(tfm, (void *)buf, buf_len);
if (err)
goto free_all;
swap_digits((u64 *)public_key, (u64 *)tmp, 4); /* x */
swap_digits((u64 *)&public_key[32], (u64 *)&tmp[32], 4); /* y */
sg_init_one(&src, tmp, 64);
sg_init_one(&dst, secret, 32);
kpp_request_set_input(req, &src, 64);
kpp_request_set_output(req, &dst, 32);
kpp_request_set_callback(req, CRYPTO_TFM_REQ_MAY_BACKLOG,
ecdh_complete, &result);
err = crypto_kpp_compute_shared_secret(req);
if (err == -EINPROGRESS) {
wait_for_completion(&result.completion);
err = result.err;
}
if (err < 0) {
pr_err("alg: ecdh: compute shared secret failed. err %d\n",
err);
goto free_all;
}
swap_digits((u64 *)secret, (u64 *)tmp, 4);
memcpy(secret, tmp, 32);
free_all:
kzfree(buf);
free_req:
kpp_request_free(req);
free_kpp:
crypto_free_kpp(tfm);
free_tmp:
kfree(tmp);
return (err == 0);
}
bool generate_ecdh_keys(u8 public_key[64], u8 private_key[32])
{
struct crypto_kpp *tfm;
struct kpp_request *req;
struct ecdh p;
struct ecdh_completion result;
struct scatterlist dst;
u8 *tmp, *buf;
unsigned int buf_len;
int err = -ENOMEM;
const unsigned short max_tries = 16;
unsigned short tries = 0;
tmp = kmalloc(64, GFP_KERNEL);
if (!tmp)
return false;
tfm = crypto_alloc_kpp("ecdh", CRYPTO_ALG_INTERNAL, 0);
if (IS_ERR(tfm)) {
pr_err("alg: kpp: Failed to load tfm for kpp: %ld\n",
PTR_ERR(tfm));
goto free_tmp;
}
req = kpp_request_alloc(tfm, GFP_KERNEL);
if (!req)
goto free_kpp;
init_completion(&result.completion);
/* Set curve_id */
p.curve_id = ECC_CURVE_NIST_P256;
p.key_size = 32;
buf_len = crypto_ecdh_key_len(&p);
buf = kmalloc(buf_len, GFP_KERNEL);
if (!buf) {
pr_err("alg: kpp: Failed to allocate %d bytes for buf\n",
buf_len);
goto free_req;
}
do {
if (tries++ >= max_tries)
goto free_all;
/* Set private Key */
p.key = (char *)private_key;
crypto_ecdh_encode_key(buf, buf_len, &p);
err = crypto_kpp_set_secret(tfm, buf, buf_len);
if (err)
goto free_all;
sg_init_one(&dst, tmp, 64);
kpp_request_set_input(req, NULL, 0);
kpp_request_set_output(req, &dst, 64);
kpp_request_set_callback(req, CRYPTO_TFM_REQ_MAY_BACKLOG,
ecdh_complete, &result);
err = crypto_kpp_generate_public_key(req);
if (err == -EINPROGRESS) {
wait_for_completion(&result.completion);
err = result.err;
}
/* Private key is not valid. Regenerate */
if (err == -EINVAL)
continue;
if (err < 0)
goto free_all;
else
break;
} while (true);
/* Keys are handed back in little endian as expected by Security
* Manager Protocol
*/
swap_digits((u64 *)tmp, (u64 *)public_key, 4); /* x */
swap_digits((u64 *)&tmp[32], (u64 *)&public_key[32], 4); /* y */
swap_digits((u64 *)private_key, (u64 *)tmp, 4);
memcpy(private_key, tmp, 32);
free_all:
kzfree(buf);
free_req:
kpp_request_free(req);
free_kpp:
crypto_free_kpp(tfm);
free_tmp:
kfree(tmp);
return (err == 0);
}
/*
* ECDH helper functions - KPP wrappings
*
* Copyright (C) 2017 Intel Corporation
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation;
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER(S) AND AUTHOR(S) BE LIABLE FOR ANY
* CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* ALL LIABILITY, INCLUDING LIABILITY FOR INFRINGEMENT OF ANY PATENTS,
* COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS, RELATING TO USE OF THIS
* SOFTWARE IS DISCLAIMED.
*/
#include <linux/types.h>
bool compute_ecdh_secret(const u8 pub_a[64], const u8 priv_b[32],
u8 secret[32]);
bool generate_ecdh_keys(u8 public_key[64], u8 private_key[32]);
...@@ -1680,7 +1680,8 @@ static int hci_sock_sendmsg(struct socket *sock, struct msghdr *msg, ...@@ -1680,7 +1680,8 @@ static int hci_sock_sendmsg(struct socket *sock, struct msghdr *msg,
if (msg->msg_flags & MSG_OOB) if (msg->msg_flags & MSG_OOB)
return -EOPNOTSUPP; return -EOPNOTSUPP;
if (msg->msg_flags & ~(MSG_DONTWAIT|MSG_NOSIGNAL|MSG_ERRQUEUE)) if (msg->msg_flags & ~(MSG_DONTWAIT|MSG_NOSIGNAL|MSG_ERRQUEUE|
MSG_CMSG_COMPAT))
return -EINVAL; return -EINVAL;
if (len < 4 || len > HCI_MAX_FRAME_SIZE) if (len < 4 || len > HCI_MAX_FRAME_SIZE)
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
#include <net/bluetooth/bluetooth.h> #include <net/bluetooth/bluetooth.h>
#include <net/bluetooth/hci_core.h> #include <net/bluetooth/hci_core.h>
#include "ecc.h" #include "ecdh_helper.h"
#include "smp.h" #include "smp.h"
#include "selftest.h" #include "selftest.h"
...@@ -142,18 +142,30 @@ static int __init test_ecdh_sample(const u8 priv_a[32], const u8 priv_b[32], ...@@ -142,18 +142,30 @@ static int __init test_ecdh_sample(const u8 priv_a[32], const u8 priv_b[32],
const u8 pub_a[64], const u8 pub_b[64], const u8 pub_a[64], const u8 pub_b[64],
const u8 dhkey[32]) const u8 dhkey[32])
{ {
u8 dhkey_a[32], dhkey_b[32]; u8 *tmp, *dhkey_a, *dhkey_b;
int ret = 0;
ecdh_shared_secret(pub_b, priv_a, dhkey_a); tmp = kmalloc(64, GFP_KERNEL);
ecdh_shared_secret(pub_a, priv_b, dhkey_b); if (!tmp)
if (memcmp(dhkey_a, dhkey, 32))
return -EINVAL; return -EINVAL;
dhkey_a = &tmp[0];
dhkey_b = &tmp[32];
compute_ecdh_secret(pub_b, priv_a, dhkey_a);
compute_ecdh_secret(pub_a, priv_b, dhkey_b);
if (memcmp(dhkey_a, dhkey, 32)) {
ret = -EINVAL;
goto out;
}
if (memcmp(dhkey_b, dhkey, 32)) if (memcmp(dhkey_b, dhkey, 32))
return -EINVAL; ret = -EINVAL;
return 0; out:
kfree(dhkey_a);
return ret;
} }
static char test_ecdh_buffer[32]; static char test_ecdh_buffer[32];
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
#include <net/bluetooth/l2cap.h> #include <net/bluetooth/l2cap.h>
#include <net/bluetooth/mgmt.h> #include <net/bluetooth/mgmt.h>
#include "ecc.h" #include "ecdh_helper.h"
#include "smp.h" #include "smp.h"
#define SMP_DEV(hdev) \ #define SMP_DEV(hdev) \
...@@ -569,8 +569,11 @@ int smp_generate_oob(struct hci_dev *hdev, u8 hash[16], u8 rand[16]) ...@@ -569,8 +569,11 @@ int smp_generate_oob(struct hci_dev *hdev, u8 hash[16], u8 rand[16])
smp->debug_key = true; smp->debug_key = true;
} else { } else {
while (true) { while (true) {
/* Seed private key with random number */
get_random_bytes(smp->local_sk, 32);
/* Generate local key pair for Secure Connections */ /* Generate local key pair for Secure Connections */
if (!ecc_make_key(smp->local_pk, smp->local_sk)) if (!generate_ecdh_keys(smp->local_pk, smp->local_sk))
return -EIO; return -EIO;
/* This is unlikely, but we need to check that /* This is unlikely, but we need to check that
...@@ -1895,8 +1898,11 @@ static u8 sc_send_public_key(struct smp_chan *smp) ...@@ -1895,8 +1898,11 @@ static u8 sc_send_public_key(struct smp_chan *smp)
set_bit(SMP_FLAG_DEBUG_KEY, &smp->flags); set_bit(SMP_FLAG_DEBUG_KEY, &smp->flags);
} else { } else {
while (true) { while (true) {
/* Seed private key with random number */
get_random_bytes(smp->local_sk, 32);
/* Generate local key pair for Secure Connections */ /* Generate local key pair for Secure Connections */
if (!ecc_make_key(smp->local_pk, smp->local_sk)) if (!generate_ecdh_keys(smp->local_pk, smp->local_sk))
return SMP_UNSPECIFIED; return SMP_UNSPECIFIED;
/* This is unlikely, but we need to check that /* This is unlikely, but we need to check that
...@@ -2670,7 +2676,7 @@ static int smp_cmd_public_key(struct l2cap_conn *conn, struct sk_buff *skb) ...@@ -2670,7 +2676,7 @@ static int smp_cmd_public_key(struct l2cap_conn *conn, struct sk_buff *skb)
SMP_DBG("Remote Public Key X: %32phN", smp->remote_pk); SMP_DBG("Remote Public Key X: %32phN", smp->remote_pk);
SMP_DBG("Remote Public Key Y: %32phN", smp->remote_pk + 32); SMP_DBG("Remote Public Key Y: %32phN", smp->remote_pk + 32);
if (!ecdh_shared_secret(smp->remote_pk, smp->local_sk, smp->dhkey)) if (!compute_ecdh_secret(smp->remote_pk, smp->local_sk, smp->dhkey))
return SMP_UNSPECIFIED; return SMP_UNSPECIFIED;
SMP_DBG("DHKey %32phN", smp->dhkey); SMP_DBG("DHKey %32phN", smp->dhkey);
...@@ -3483,6 +3489,32 @@ void smp_unregister(struct hci_dev *hdev) ...@@ -3483,6 +3489,32 @@ void smp_unregister(struct hci_dev *hdev)
#if IS_ENABLED(CONFIG_BT_SELFTEST_SMP) #if IS_ENABLED(CONFIG_BT_SELFTEST_SMP)
static inline void swap_digits(u64 *in, u64 *out, unsigned int ndigits)
{
int i;
for (i = 0; i < ndigits; i++)
out[i] = __swab64(in[ndigits - 1 - i]);
}
static int __init test_debug_key(void)
{
u8 pk[64], sk[32];
swap_digits((u64 *)debug_sk, (u64 *)sk, 4);
if (!generate_ecdh_keys(pk, sk))
return -EINVAL;
if (memcmp(sk, debug_sk, 32))
return -EINVAL;
if (memcmp(pk, debug_pk, 64))
return -EINVAL;
return 0;
}
static int __init test_ah(struct crypto_cipher *tfm_aes) static int __init test_ah(struct crypto_cipher *tfm_aes)
{ {
const u8 irk[16] = { const u8 irk[16] = {
...@@ -3738,6 +3770,12 @@ static int __init run_selftests(struct crypto_cipher *tfm_aes, ...@@ -3738,6 +3770,12 @@ static int __init run_selftests(struct crypto_cipher *tfm_aes,
calltime = ktime_get(); calltime = ktime_get();
err = test_debug_key();
if (err) {
BT_ERR("debug_key test failed");
goto done;
}
err = test_ah(tfm_aes); err = test_ah(tfm_aes);
if (err) { if (err) {
BT_ERR("smp_ah test failed"); BT_ERR("smp_ah test failed");
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment