diff options
author | Johan Hedberg <johan.hedberg@nokia.com> | 2011-01-17 07:41:05 -0500 |
---|---|---|
committer | Gustavo F. Padovan <padovan@profusion.mobi> | 2011-02-07 22:40:07 -0500 |
commit | 55ed8ca10f3530de8edbbf138acb50992bf5005b (patch) | |
tree | 3145b2b995758b2cb64493fc8ec29e63d0e9f0b4 | |
parent | 1aff6f09491f454d4cd9f405c783fa5e9d3168a0 (diff) |
Bluetooth: Implement link key handling for the management interface
This patch adds a management commands to feed the kernel with all stored
link keys as well as remove specific ones or all of them. Once the
load_keys command has been called the kernel takes over link key
replies. A new_key event is also added to inform userspace of newly
created link keys that should be stored permanently.
Signed-off-by: Johan Hedberg <johan.hedberg@nokia.com>
Signed-off-by: Gustavo F. Padovan <padovan@profusion.mobi>
-rw-r--r-- | include/net/bluetooth/hci.h | 2 | ||||
-rw-r--r-- | include/net/bluetooth/hci_core.h | 17 | ||||
-rw-r--r-- | include/net/bluetooth/mgmt.h | 29 | ||||
-rw-r--r-- | net/bluetooth/hci_core.c | 85 | ||||
-rw-r--r-- | net/bluetooth/hci_event.c | 51 | ||||
-rw-r--r-- | net/bluetooth/mgmt.c | 116 |
6 files changed, 300 insertions, 0 deletions
diff --git a/include/net/bluetooth/hci.h b/include/net/bluetooth/hci.h index 9ce46cd00ba2..08fbf1253b83 100644 --- a/include/net/bluetooth/hci.h +++ b/include/net/bluetooth/hci.h | |||
@@ -82,6 +82,8 @@ enum { | |||
82 | HCI_MGMT, | 82 | HCI_MGMT, |
83 | HCI_PAIRABLE, | 83 | HCI_PAIRABLE, |
84 | HCI_SERVICE_CACHE, | 84 | HCI_SERVICE_CACHE, |
85 | HCI_LINK_KEYS, | ||
86 | HCI_DEBUG_KEYS, | ||
85 | }; | 87 | }; |
86 | 88 | ||
87 | /* HCI ioctl defines */ | 89 | /* HCI ioctl defines */ |
diff --git a/include/net/bluetooth/hci_core.h b/include/net/bluetooth/hci_core.h index e62da084e01d..009fa63a9048 100644 --- a/include/net/bluetooth/hci_core.h +++ b/include/net/bluetooth/hci_core.h | |||
@@ -73,6 +73,14 @@ struct bt_uuid { | |||
73 | u8 svc_hint; | 73 | u8 svc_hint; |
74 | }; | 74 | }; |
75 | 75 | ||
76 | struct link_key { | ||
77 | struct list_head list; | ||
78 | bdaddr_t bdaddr; | ||
79 | u8 type; | ||
80 | u8 val[16]; | ||
81 | u8 pin_len; | ||
82 | }; | ||
83 | |||
76 | #define NUM_REASSEMBLY 4 | 84 | #define NUM_REASSEMBLY 4 |
77 | struct hci_dev { | 85 | struct hci_dev { |
78 | struct list_head list; | 86 | struct list_head list; |
@@ -153,6 +161,8 @@ struct hci_dev { | |||
153 | 161 | ||
154 | struct list_head uuids; | 162 | struct list_head uuids; |
155 | 163 | ||
164 | struct list_head link_keys; | ||
165 | |||
156 | struct hci_dev_stats stat; | 166 | struct hci_dev_stats stat; |
157 | 167 | ||
158 | struct sk_buff_head driver_init; | 168 | struct sk_buff_head driver_init; |
@@ -461,6 +471,12 @@ int hci_blacklist_clear(struct hci_dev *hdev); | |||
461 | 471 | ||
462 | int hci_uuids_clear(struct hci_dev *hdev); | 472 | int hci_uuids_clear(struct hci_dev *hdev); |
463 | 473 | ||
474 | int hci_link_keys_clear(struct hci_dev *hdev); | ||
475 | struct link_key *hci_find_link_key(struct hci_dev *hdev, bdaddr_t *bdaddr); | ||
476 | int hci_add_link_key(struct hci_dev *hdev, int new_key, bdaddr_t *bdaddr, | ||
477 | u8 *key, u8 type, u8 pin_len); | ||
478 | int hci_remove_link_key(struct hci_dev *hdev, bdaddr_t *bdaddr); | ||
479 | |||
464 | void hci_del_off_timer(struct hci_dev *hdev); | 480 | void hci_del_off_timer(struct hci_dev *hdev); |
465 | 481 | ||
466 | void hci_event_packet(struct hci_dev *hdev, struct sk_buff *skb); | 482 | void hci_event_packet(struct hci_dev *hdev, struct sk_buff *skb); |
@@ -697,6 +713,7 @@ int mgmt_index_removed(u16 index); | |||
697 | int mgmt_powered(u16 index, u8 powered); | 713 | int mgmt_powered(u16 index, u8 powered); |
698 | int mgmt_discoverable(u16 index, u8 discoverable); | 714 | int mgmt_discoverable(u16 index, u8 discoverable); |
699 | int mgmt_connectable(u16 index, u8 connectable); | 715 | int mgmt_connectable(u16 index, u8 connectable); |
716 | int mgmt_new_key(u16 index, struct link_key *key, u8 old_key_type); | ||
700 | 717 | ||
701 | /* HCI info for socket */ | 718 | /* HCI info for socket */ |
702 | #define hci_pi(sk) ((struct hci_pinfo *) sk) | 719 | #define hci_pi(sk) ((struct hci_pinfo *) sk) |
diff --git a/include/net/bluetooth/mgmt.h b/include/net/bluetooth/mgmt.h index b092c4c014eb..56b500a2f68c 100644 --- a/include/net/bluetooth/mgmt.h +++ b/include/net/bluetooth/mgmt.h | |||
@@ -98,6 +98,28 @@ struct mgmt_cp_set_service_cache { | |||
98 | __u8 enable; | 98 | __u8 enable; |
99 | } __packed; | 99 | } __packed; |
100 | 100 | ||
101 | struct mgmt_key_info { | ||
102 | bdaddr_t bdaddr; | ||
103 | u8 type; | ||
104 | u8 val[16]; | ||
105 | u8 pin_len; | ||
106 | } __packed; | ||
107 | |||
108 | #define MGMT_OP_LOAD_KEYS 0x000D | ||
109 | struct mgmt_cp_load_keys { | ||
110 | __le16 index; | ||
111 | __u8 debug_keys; | ||
112 | __le16 key_count; | ||
113 | struct mgmt_key_info keys[0]; | ||
114 | } __packed; | ||
115 | |||
116 | #define MGMT_OP_REMOVE_KEY 0x000E | ||
117 | struct mgmt_cp_remove_key { | ||
118 | __le16 index; | ||
119 | bdaddr_t bdaddr; | ||
120 | __u8 disconnect; | ||
121 | } __packed; | ||
122 | |||
101 | #define MGMT_EV_CMD_COMPLETE 0x0001 | 123 | #define MGMT_EV_CMD_COMPLETE 0x0001 |
102 | struct mgmt_ev_cmd_complete { | 124 | struct mgmt_ev_cmd_complete { |
103 | __le16 opcode; | 125 | __le16 opcode; |
@@ -133,3 +155,10 @@ struct mgmt_ev_index_removed { | |||
133 | #define MGMT_EV_CONNECTABLE 0x0008 | 155 | #define MGMT_EV_CONNECTABLE 0x0008 |
134 | 156 | ||
135 | #define MGMT_EV_PAIRABLE 0x0009 | 157 | #define MGMT_EV_PAIRABLE 0x0009 |
158 | |||
159 | #define MGMT_EV_NEW_KEY 0x000A | ||
160 | struct mgmt_ev_new_key { | ||
161 | __le16 index; | ||
162 | struct mgmt_key_info key; | ||
163 | __u8 old_key_type; | ||
164 | } __packed; | ||
diff --git a/net/bluetooth/hci_core.c b/net/bluetooth/hci_core.c index 748f5a65caf4..8ca8cf147058 100644 --- a/net/bluetooth/hci_core.c +++ b/net/bluetooth/hci_core.c | |||
@@ -970,6 +970,88 @@ int hci_uuids_clear(struct hci_dev *hdev) | |||
970 | return 0; | 970 | return 0; |
971 | } | 971 | } |
972 | 972 | ||
973 | int hci_link_keys_clear(struct hci_dev *hdev) | ||
974 | { | ||
975 | struct list_head *p, *n; | ||
976 | |||
977 | list_for_each_safe(p, n, &hdev->link_keys) { | ||
978 | struct link_key *key; | ||
979 | |||
980 | key = list_entry(p, struct link_key, list); | ||
981 | |||
982 | list_del(p); | ||
983 | kfree(key); | ||
984 | } | ||
985 | |||
986 | return 0; | ||
987 | } | ||
988 | |||
989 | struct link_key *hci_find_link_key(struct hci_dev *hdev, bdaddr_t *bdaddr) | ||
990 | { | ||
991 | struct list_head *p; | ||
992 | |||
993 | list_for_each(p, &hdev->link_keys) { | ||
994 | struct link_key *k; | ||
995 | |||
996 | k = list_entry(p, struct link_key, list); | ||
997 | |||
998 | if (bacmp(bdaddr, &k->bdaddr) == 0) | ||
999 | return k; | ||
1000 | } | ||
1001 | |||
1002 | return NULL; | ||
1003 | } | ||
1004 | |||
1005 | int hci_add_link_key(struct hci_dev *hdev, int new_key, bdaddr_t *bdaddr, | ||
1006 | u8 *val, u8 type, u8 pin_len) | ||
1007 | { | ||
1008 | struct link_key *key, *old_key; | ||
1009 | u8 old_key_type; | ||
1010 | |||
1011 | old_key = hci_find_link_key(hdev, bdaddr); | ||
1012 | if (old_key) { | ||
1013 | old_key_type = old_key->type; | ||
1014 | key = old_key; | ||
1015 | } else { | ||
1016 | old_key_type = 0xff; | ||
1017 | key = kzalloc(sizeof(*key), GFP_ATOMIC); | ||
1018 | if (!key) | ||
1019 | return -ENOMEM; | ||
1020 | list_add(&key->list, &hdev->link_keys); | ||
1021 | } | ||
1022 | |||
1023 | BT_DBG("%s key for %s type %u", hdev->name, batostr(bdaddr), type); | ||
1024 | |||
1025 | bacpy(&key->bdaddr, bdaddr); | ||
1026 | memcpy(key->val, val, 16); | ||
1027 | key->type = type; | ||
1028 | key->pin_len = pin_len; | ||
1029 | |||
1030 | if (new_key) | ||
1031 | mgmt_new_key(hdev->id, key, old_key_type); | ||
1032 | |||
1033 | if (type == 0x06) | ||
1034 | key->type = old_key_type; | ||
1035 | |||
1036 | return 0; | ||
1037 | } | ||
1038 | |||
1039 | int hci_remove_link_key(struct hci_dev *hdev, bdaddr_t *bdaddr) | ||
1040 | { | ||
1041 | struct link_key *key; | ||
1042 | |||
1043 | key = hci_find_link_key(hdev, bdaddr); | ||
1044 | if (!key) | ||
1045 | return -ENOENT; | ||
1046 | |||
1047 | BT_DBG("%s removing %s", hdev->name, batostr(bdaddr)); | ||
1048 | |||
1049 | list_del(&key->list); | ||
1050 | kfree(key); | ||
1051 | |||
1052 | return 0; | ||
1053 | } | ||
1054 | |||
973 | /* Register HCI device */ | 1055 | /* Register HCI device */ |
974 | int hci_register_dev(struct hci_dev *hdev) | 1056 | int hci_register_dev(struct hci_dev *hdev) |
975 | { | 1057 | { |
@@ -1029,6 +1111,8 @@ int hci_register_dev(struct hci_dev *hdev) | |||
1029 | 1111 | ||
1030 | INIT_LIST_HEAD(&hdev->uuids); | 1112 | INIT_LIST_HEAD(&hdev->uuids); |
1031 | 1113 | ||
1114 | INIT_LIST_HEAD(&hdev->link_keys); | ||
1115 | |||
1032 | INIT_WORK(&hdev->power_on, hci_power_on); | 1116 | INIT_WORK(&hdev->power_on, hci_power_on); |
1033 | INIT_WORK(&hdev->power_off, hci_power_off); | 1117 | INIT_WORK(&hdev->power_off, hci_power_off); |
1034 | setup_timer(&hdev->off_timer, hci_auto_off, (unsigned long) hdev); | 1118 | setup_timer(&hdev->off_timer, hci_auto_off, (unsigned long) hdev); |
@@ -1105,6 +1189,7 @@ int hci_unregister_dev(struct hci_dev *hdev) | |||
1105 | hci_dev_lock_bh(hdev); | 1189 | hci_dev_lock_bh(hdev); |
1106 | hci_blacklist_clear(hdev); | 1190 | hci_blacklist_clear(hdev); |
1107 | hci_uuids_clear(hdev); | 1191 | hci_uuids_clear(hdev); |
1192 | hci_link_keys_clear(hdev); | ||
1108 | hci_dev_unlock_bh(hdev); | 1193 | hci_dev_unlock_bh(hdev); |
1109 | 1194 | ||
1110 | __hci_dev_put(hdev); | 1195 | __hci_dev_put(hdev); |
diff --git a/net/bluetooth/hci_event.c b/net/bluetooth/hci_event.c index c69ee44d5bd7..80ffd3a901fc 100644 --- a/net/bluetooth/hci_event.c +++ b/net/bluetooth/hci_event.c | |||
@@ -1810,13 +1810,60 @@ static inline void hci_pin_code_request_evt(struct hci_dev *hdev, struct sk_buff | |||
1810 | 1810 | ||
1811 | static inline void hci_link_key_request_evt(struct hci_dev *hdev, struct sk_buff *skb) | 1811 | static inline void hci_link_key_request_evt(struct hci_dev *hdev, struct sk_buff *skb) |
1812 | { | 1812 | { |
1813 | struct hci_ev_link_key_req *ev = (void *) skb->data; | ||
1814 | struct hci_cp_link_key_reply cp; | ||
1815 | struct hci_conn *conn; | ||
1816 | struct link_key *key; | ||
1817 | |||
1813 | BT_DBG("%s", hdev->name); | 1818 | BT_DBG("%s", hdev->name); |
1819 | |||
1820 | if (!test_bit(HCI_LINK_KEYS, &hdev->flags)) | ||
1821 | return; | ||
1822 | |||
1823 | hci_dev_lock(hdev); | ||
1824 | |||
1825 | key = hci_find_link_key(hdev, &ev->bdaddr); | ||
1826 | if (!key) { | ||
1827 | BT_DBG("%s link key not found for %s", hdev->name, | ||
1828 | batostr(&ev->bdaddr)); | ||
1829 | goto not_found; | ||
1830 | } | ||
1831 | |||
1832 | BT_DBG("%s found key type %u for %s", hdev->name, key->type, | ||
1833 | batostr(&ev->bdaddr)); | ||
1834 | |||
1835 | if (!test_bit(HCI_DEBUG_KEYS, &hdev->flags) && key->type == 0x03) { | ||
1836 | BT_DBG("%s ignoring debug key", hdev->name); | ||
1837 | goto not_found; | ||
1838 | } | ||
1839 | |||
1840 | conn = hci_conn_hash_lookup_ba(hdev, ACL_LINK, &ev->bdaddr); | ||
1841 | |||
1842 | if (key->type == 0x04 && conn && conn->auth_type != 0xff && | ||
1843 | (conn->auth_type & 0x01)) { | ||
1844 | BT_DBG("%s ignoring unauthenticated key", hdev->name); | ||
1845 | goto not_found; | ||
1846 | } | ||
1847 | |||
1848 | bacpy(&cp.bdaddr, &ev->bdaddr); | ||
1849 | memcpy(cp.link_key, key->val, 16); | ||
1850 | |||
1851 | hci_send_cmd(hdev, HCI_OP_LINK_KEY_REPLY, sizeof(cp), &cp); | ||
1852 | |||
1853 | hci_dev_unlock(hdev); | ||
1854 | |||
1855 | return; | ||
1856 | |||
1857 | not_found: | ||
1858 | hci_send_cmd(hdev, HCI_OP_LINK_KEY_NEG_REPLY, 6, &ev->bdaddr); | ||
1859 | hci_dev_unlock(hdev); | ||
1814 | } | 1860 | } |
1815 | 1861 | ||
1816 | static inline void hci_link_key_notify_evt(struct hci_dev *hdev, struct sk_buff *skb) | 1862 | static inline void hci_link_key_notify_evt(struct hci_dev *hdev, struct sk_buff *skb) |
1817 | { | 1863 | { |
1818 | struct hci_ev_link_key_notify *ev = (void *) skb->data; | 1864 | struct hci_ev_link_key_notify *ev = (void *) skb->data; |
1819 | struct hci_conn *conn; | 1865 | struct hci_conn *conn; |
1866 | u8 pin_len = 0; | ||
1820 | 1867 | ||
1821 | BT_DBG("%s", hdev->name); | 1868 | BT_DBG("%s", hdev->name); |
1822 | 1869 | ||
@@ -1829,6 +1876,10 @@ static inline void hci_link_key_notify_evt(struct hci_dev *hdev, struct sk_buff | |||
1829 | hci_conn_put(conn); | 1876 | hci_conn_put(conn); |
1830 | } | 1877 | } |
1831 | 1878 | ||
1879 | if (test_bit(HCI_LINK_KEYS, &hdev->flags)) | ||
1880 | hci_add_link_key(hdev, 1, &ev->bdaddr, ev->link_key, | ||
1881 | ev->key_type, pin_len); | ||
1882 | |||
1832 | hci_dev_unlock(hdev); | 1883 | hci_dev_unlock(hdev); |
1833 | } | 1884 | } |
1834 | 1885 | ||
diff --git a/net/bluetooth/mgmt.c b/net/bluetooth/mgmt.c index a08f4ce03182..bdb0e85f182e 100644 --- a/net/bluetooth/mgmt.c +++ b/net/bluetooth/mgmt.c | |||
@@ -794,6 +794,99 @@ static int set_service_cache(struct sock *sk, unsigned char *data, u16 len) | |||
794 | return err; | 794 | return err; |
795 | } | 795 | } |
796 | 796 | ||
797 | static int load_keys(struct sock *sk, unsigned char *data, u16 len) | ||
798 | { | ||
799 | struct hci_dev *hdev; | ||
800 | struct mgmt_cp_load_keys *cp; | ||
801 | u16 dev_id, key_count, expected_len; | ||
802 | int i; | ||
803 | |||
804 | cp = (void *) data; | ||
805 | dev_id = get_unaligned_le16(&cp->index); | ||
806 | key_count = get_unaligned_le16(&cp->key_count); | ||
807 | |||
808 | expected_len = sizeof(*cp) + key_count * sizeof(struct mgmt_key_info); | ||
809 | if (expected_len != len) { | ||
810 | BT_ERR("load_keys: expected %u bytes, got %u bytes", | ||
811 | len, expected_len); | ||
812 | return -EINVAL; | ||
813 | } | ||
814 | |||
815 | hdev = hci_dev_get(dev_id); | ||
816 | if (!hdev) | ||
817 | return cmd_status(sk, MGMT_OP_LOAD_KEYS, ENODEV); | ||
818 | |||
819 | BT_DBG("hci%u debug_keys %u key_count %u", dev_id, cp->debug_keys, | ||
820 | key_count); | ||
821 | |||
822 | hci_dev_lock_bh(hdev); | ||
823 | |||
824 | hci_link_keys_clear(hdev); | ||
825 | |||
826 | set_bit(HCI_LINK_KEYS, &hdev->flags); | ||
827 | |||
828 | if (cp->debug_keys) | ||
829 | set_bit(HCI_DEBUG_KEYS, &hdev->flags); | ||
830 | else | ||
831 | clear_bit(HCI_DEBUG_KEYS, &hdev->flags); | ||
832 | |||
833 | for (i = 0; i < key_count; i++) { | ||
834 | struct mgmt_key_info *key = &cp->keys[i]; | ||
835 | |||
836 | hci_add_link_key(hdev, 0, &key->bdaddr, key->val, key->type, | ||
837 | key->pin_len); | ||
838 | } | ||
839 | |||
840 | hci_dev_unlock_bh(hdev); | ||
841 | hci_dev_put(hdev); | ||
842 | |||
843 | return 0; | ||
844 | } | ||
845 | |||
846 | static int remove_key(struct sock *sk, unsigned char *data, u16 len) | ||
847 | { | ||
848 | struct hci_dev *hdev; | ||
849 | struct mgmt_cp_remove_key *cp; | ||
850 | struct hci_conn *conn; | ||
851 | u16 dev_id; | ||
852 | int err; | ||
853 | |||
854 | cp = (void *) data; | ||
855 | dev_id = get_unaligned_le16(&cp->index); | ||
856 | |||
857 | hdev = hci_dev_get(dev_id); | ||
858 | if (!hdev) | ||
859 | return cmd_status(sk, MGMT_OP_REMOVE_KEY, ENODEV); | ||
860 | |||
861 | hci_dev_lock_bh(hdev); | ||
862 | |||
863 | err = hci_remove_link_key(hdev, &cp->bdaddr); | ||
864 | if (err < 0) { | ||
865 | err = cmd_status(sk, MGMT_OP_REMOVE_KEY, -err); | ||
866 | goto unlock; | ||
867 | } | ||
868 | |||
869 | err = 0; | ||
870 | |||
871 | if (!test_bit(HCI_UP, &hdev->flags) || !cp->disconnect) | ||
872 | goto unlock; | ||
873 | |||
874 | conn = hci_conn_hash_lookup_ba(hdev, ACL_LINK, &cp->bdaddr); | ||
875 | if (conn) { | ||
876 | struct hci_cp_disconnect dc; | ||
877 | |||
878 | put_unaligned_le16(conn->handle, &dc.handle); | ||
879 | dc.reason = 0x13; /* Remote User Terminated Connection */ | ||
880 | err = hci_send_cmd(hdev, HCI_OP_DISCONNECT, 0, NULL); | ||
881 | } | ||
882 | |||
883 | unlock: | ||
884 | hci_dev_unlock_bh(hdev); | ||
885 | hci_dev_put(hdev); | ||
886 | |||
887 | return err; | ||
888 | } | ||
889 | |||
797 | int mgmt_control(struct sock *sk, struct msghdr *msg, size_t msglen) | 890 | int mgmt_control(struct sock *sk, struct msghdr *msg, size_t msglen) |
798 | { | 891 | { |
799 | unsigned char *buf; | 892 | unsigned char *buf; |
@@ -858,6 +951,12 @@ int mgmt_control(struct sock *sk, struct msghdr *msg, size_t msglen) | |||
858 | case MGMT_OP_SET_SERVICE_CACHE: | 951 | case MGMT_OP_SET_SERVICE_CACHE: |
859 | err = set_service_cache(sk, buf + sizeof(*hdr), len); | 952 | err = set_service_cache(sk, buf + sizeof(*hdr), len); |
860 | break; | 953 | break; |
954 | case MGMT_OP_LOAD_KEYS: | ||
955 | err = load_keys(sk, buf + sizeof(*hdr), len); | ||
956 | break; | ||
957 | case MGMT_OP_REMOVE_KEY: | ||
958 | err = remove_key(sk, buf + sizeof(*hdr), len); | ||
959 | break; | ||
861 | default: | 960 | default: |
862 | BT_DBG("Unknown op %u", opcode); | 961 | BT_DBG("Unknown op %u", opcode); |
863 | err = cmd_status(sk, opcode, 0x01); | 962 | err = cmd_status(sk, opcode, 0x01); |
@@ -974,3 +1073,20 @@ int mgmt_connectable(u16 index, u8 connectable) | |||
974 | 1073 | ||
975 | return ret; | 1074 | return ret; |
976 | } | 1075 | } |
1076 | |||
1077 | int mgmt_new_key(u16 index, struct link_key *key, u8 old_key_type) | ||
1078 | { | ||
1079 | struct mgmt_ev_new_key ev; | ||
1080 | |||
1081 | memset(&ev, 0, sizeof(ev)); | ||
1082 | |||
1083 | put_unaligned_le16(index, &ev.index); | ||
1084 | |||
1085 | bacpy(&ev.key.bdaddr, &key->bdaddr); | ||
1086 | ev.key.type = key->type; | ||
1087 | memcpy(ev.key.val, key->val, 16); | ||
1088 | ev.key.pin_len = key->pin_len; | ||
1089 | ev.old_key_type = old_key_type; | ||
1090 | |||
1091 | return mgmt_event(MGMT_EV_NEW_KEY, &ev, sizeof(ev), NULL); | ||
1092 | } | ||