$NetBSD$ --- /dev/null Mon Sep 27 01:09:07 1999 +++ netbsd-1.4/ppp_mppe.c Mon Sep 27 02:11:53 1999 @@ -0,0 +1,785 @@ +/* NetBSD + * + * taken from: FILEVERSION 9906180 + * + * ppp_mppe.c - MPPE "compressor/decompressor" module. + * + * Copyright (c) 1994 Árpád Magosányi + * All rights reserved. + * Copyright (c) 1999 Tim Hockin, Cobalt Networks Inc. + * Copyright (c) 1999 Darrin B. Jewell + * + * Permission to use, copy, modify, and distribute this software and its + * documentation is hereby granted, provided that the above copyright + * notice appears in all copies. This software is provided without any + * warranty, express or implied. The Australian National University + * makes no representations about the suitability of this software for + * any purpose. + * + * IN NO EVENT SHALL THE AUSTRALIAN NATIONAL UNIVERSITY BE LIABLE TO ANY + * PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES + * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF + * THE AUSTRALIAN NATIONAL UNIVERSITY HAS BEEN ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * THE AUSTRALIAN NATIONAL UNIVERSITY SPECIFICALLY DISCLAIMS ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS + * ON AN "AS IS" BASIS, AND THE AUSTRALIAN NATIONAL UNIVERSITY HAS NO + * OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, + * OR MODIFICATIONS. + * + * From: deflate.c,v 1.1 1996/01/18 03:17:48 paulus Exp + */ + +#include +#include +#include +#include +#include + +#include + +#define PACKETPTR struct mbuf * +#include + +#include "rc4.h" +#include "rc4_enc.c" +#include "sha1dgst.c" +#include "mppe.h" + +extern void +GetNewKeyFromSHA(unsigned char *StartKey, + unsigned char *SessionKey, + unsigned long SessionKeyLength, + unsigned char *InterimKey); + +#if defined(LKM) || defined(_LKM) +int mppe_in_use = 0; +#define MOD_DEC_USE_COUNT (mppe_in_use--) +#define MOD_INC_USE_COUNT (mppe_in_use++) +#else +#define MOD_DEC_USE_COUNT while (0) +#define MOD_INC_USE_COUNT while (0) +#endif + +#ifndef CI_MPPE +#define CI_MPPE 18 /* config. option for MPPE */ +#define CILEN_MPPE 6 /* length of config. option */ +#endif + +#ifdef MPPE_DEBUG +#define DPRINTF(x) printf x +#else +#define DPRINTF(x) +#endif + +/* + * State for a mppe "(de)compressor". + */ +struct ppp_mppe_state { + unsigned int ccount; /*coherency count */ + RC4_KEY RC4_send_key; /* chap-ms-v2 dictates 2 keys */ + RC4_KEY RC4_recv_key; + unsigned char session_send_key[16]; + unsigned char session_recv_key[16]; + unsigned char master_send_key[16]; + unsigned char master_recv_key[16]; + int keylen; + int stateless; + int decomp_error; + unsigned int bits; + int unit; + int debug; + int mru; + struct compstat stats; +}; + +#define MPPE_CCOUNT_FROM_PACKET(ibuf) ((((ibuf)[4] & 0x0f) << 8) + (ibuf)[5]) +#define MPPE_BITS(ibuf) ((ibuf)[4] & 0xf0 ) +#define MPPE_CTRLHI(state) ((((state)->ccount & 0xf00)>>8)|((state)->bits)) +#define MPPE_CTRLLO(state) ((state)->ccount & 0xff) + +#define MPPE_OVHD 4 + +void mppe_synchronize_key(struct ppp_mppe_state *); +void mppe_initialize_key(struct ppp_mppe_state *); +void mppe_change_key(struct ppp_mppe_state *); +void mppe_update_count(struct ppp_mppe_state *); + +/* Procedures from the MPPE draft */ +void +mppe_synchronize_key(struct ppp_mppe_state *state) +{ + /* get new keys and flag our state as such */ + RC4_set_key(&(state->RC4_send_key),state->keylen,state->session_send_key); + RC4_set_key(&(state->RC4_recv_key),state->keylen,state->session_recv_key); + + state->bits=MPPE_BIT_FLUSHED|MPPE_BIT_ENCRYPTED; +} + + +void +mppe_initialize_key(struct ppp_mppe_state *state) +{ + /* generate new session keys */ + GetNewKeyFromSHA(state->master_send_key, state->master_send_key, + state->keylen, state->session_send_key); + GetNewKeyFromSHA(state->master_recv_key, state->master_recv_key, + state->keylen, state->session_recv_key); + + if(state->keylen == 8) { + /* cripple them from 64bit->40bit */ + state->session_send_key[0]=state->session_recv_key[0] = MPPE_40_SALT0; + state->session_send_key[1]=state->session_recv_key[1] = MPPE_40_SALT1; + state->session_send_key[2]=state->session_recv_key[2] = MPPE_40_SALT2; + } + + mppe_synchronize_key(state); +} + + +void +mppe_change_key(struct ppp_mppe_state *state) +{ + unsigned char InterimSendKey[16]; + unsigned char InterimRecvKey[16]; + + /* get temp keys */ + GetNewKeyFromSHA(state->master_send_key, state->session_send_key, + state->keylen, InterimSendKey); + GetNewKeyFromSHA(state->master_recv_key, state->session_recv_key, + state->keylen, InterimRecvKey); + + /* build RC4 keys from the temp keys */ + RC4_set_key(&(state->RC4_send_key), state->keylen, InterimSendKey); + RC4_set_key(&(state->RC4_recv_key), state->keylen, InterimRecvKey); + + /* make new session keys */ + RC4(&(state->RC4_send_key), state->keylen, InterimSendKey, + state->session_send_key); + RC4(&(state->RC4_recv_key), state->keylen, InterimRecvKey, + state->session_recv_key); + + if(state->keylen == 8) + { + /* cripple them from 64->40 bits*/ + state->session_send_key[0]=state->session_recv_key[0] = MPPE_40_SALT0; + state->session_send_key[1]=state->session_recv_key[1] = MPPE_40_SALT1; + state->session_send_key[2]=state->session_recv_key[2] = MPPE_40_SALT2; + } + + /* make the final rc4 keys */ + RC4_set_key(&(state->RC4_send_key), state->keylen, state->session_send_key); + RC4_set_key(&(state->RC4_recv_key), state->keylen, state->session_recv_key); + + state->bits=MPPE_BIT_ENCRYPTED; +} + + +#if defined(MPPE_DEBUG) && (MPEE_DEBUG > 1) +typedef u_int8_t __u8; +/* Utility procedures to print a buffer in hex/ascii */ +static void +ppp_print_hex (register __u8 *out, const __u8 *in, int count) +{ + register __u8 next_ch; + static char hex[] = "0123456789ABCDEF"; + + while (count-- > 0) { + next_ch = *in++; + *out++ = hex[(next_ch >> 4) & 0x0F]; + *out++ = hex[next_ch & 0x0F]; + ++out; + } +} + + +static void +ppp_print_char (register __u8 *out, const __u8 *in, int count) +{ + register __u8 next_ch; + + while (count-- > 0) { + next_ch = *in++; + + if (next_ch < 0x20 || next_ch > 0x7e) + *out++ = '.'; + else { + *out++ = next_ch; + if (next_ch == '%') /* printk/syslogd has a bug !! */ + *out++ = '%'; + } + } + *out = '\0'; +} + +static void +ppp_print_buffer (const __u8 *name, const __u8 *buf, int count) +{ + __u8 line[44]; + + if (name != (__u8 *) NULL) + printf("ppp: %s, count = %d\n", name, count); + + while (count > 8) { + memset (line, 32, 44); + ppp_print_hex (line, buf, 8); + ppp_print_char (&line[8 * 3], buf, 8); + printf("%s\n", line); + count -= 8; + buf += 8; + } + + if (count > 0) { + memset (line, 32, 44); + ppp_print_hex (line, buf, count); + ppp_print_char (&line[8 * 3], buf, count); + printf("%s\n", line); + } +} +#endif + +/* our 'compressor' proper */ +void *mppe_comp_alloc __P((unsigned char *, int)); +void mppe_comp_free __P((void *)); +int mppe_comp_init __P((void *, unsigned char *, + int, int, int, int)); +int mppe_decomp_init __P((void *, unsigned char *, + int, int, int, int, int)); +int mppe_compress __P((void *, struct mbuf **, + struct mbuf *,int, int)); +void mppe_incomp __P((void *, struct mbuf *)); +int mppe_decompress __P((void *, struct mbuf *, struct mbuf **)); +void mppe_comp_reset __P((void *)); +void mppe_comp_stats __P((void *, struct compstat *)); + + +/* cleanup the compressor */ +void +mppe_comp_free(void *arg) +{ + struct ppp_mppe_state *state = (struct ppp_mppe_state *) arg; + + if (state) { + free(state,M_DEVBUF); + MOD_DEC_USE_COUNT; + } +} + + +/* allocate space for a compressor. */ +void * +mppe_comp_alloc(unsigned char *options, int opt_len) +{ + struct ppp_mppe_state *state; + + if (((2*8)+3 != opt_len && (2*16)+3 != opt_len) /* 2 keys + 3 */ + || options[0] != CI_MPPE || options[1] != CILEN_MPPE) { + DPRINTF(("ppp/mppe: compress rejected: opt_len=%u,o[0]=%x,o[1]=%x\n", + opt_len,options[0],options[1])); + if (opt_len == 32) { + DPRINTF(("ppp/mppe: try increasing CCP_MAX_OPTION_LENGTH in ppp-comp.h\n")); + } + return NULL; + } + + state = (struct ppp_mppe_state *)malloc(sizeof(*state), M_DEVBUF, M_NOWAIT); + if (state == NULL) + return NULL; + + MOD_INC_USE_COUNT; + + memset (state, 0, sizeof (struct ppp_mppe_state)); + + /* write the data in options to the right places */ + memcpy(&state->stateless,options+2,1); + + state->keylen = (opt_len-3)/2; + memcpy(state->master_send_key,options+3,state->keylen); + memcpy(state->master_recv_key,options+3+state->keylen,state->keylen); + + mppe_initialize_key(state); + + return (void *) state; +} + + +int +mppe_comp_init(void *arg, unsigned char *options, int opt_len, int unit, + int hdrlen, int debug) +{ + struct ppp_mppe_state *state = (struct ppp_mppe_state *)arg; + + if (options[0] != CI_MPPE || options[1] != CILEN_MPPE) { + DPRINTF(("ppp%d/mppe: compress rejected: opt_len=%u,o[0]=%x,o[1]=%x\n", + state->unit,opt_len,options[0],options[1])); + return 0; + } + + state->ccount = 0; + state->unit = unit; + state->debug = debug; + + /* 19 is the min (2*keylen) + 3 */ + if(opt_len >= 19) { + memcpy(&state->stateless,options+2,1); + + state->keylen = (opt_len-3)/2; + memcpy(state->master_send_key,options+3,state->keylen); + memcpy(state->master_recv_key,options+3+state->keylen,state->keylen); + + mppe_initialize_key(state); + } + + return 1; +} + + +int +mppe_decomp_init(void *arg, unsigned char *options, int opt_len, int unit, + int hdrlen, int mru, int debug) +{ + struct ppp_mppe_state *state = (struct ppp_mppe_state *)arg; + + if (options[0] != CI_MPPE || options[1] != CILEN_MPPE) { + DPRINTF(("ppp%d/MPPE: options are bad: %x %x\n", + state->unit,options[0],options[1])); + return 0; + } + + state->ccount = 0; + state->unit = unit; + state->debug = debug; + state->mru = mru; + + /* 19 is the min (2*keylen)+3 */ + if(opt_len >= 19) { + memcpy(&state->stateless,options+2,1); + + state->keylen = (opt_len-3)/2; + memcpy(state->master_send_key,options+3,state->keylen); + memcpy(state->master_recv_key,options+3+state->keylen,state->keylen); + + mppe_initialize_key(state); + } + + return 1; +} + + +void +mppe_comp_reset(void *arg) +{ + struct ppp_mppe_state *state = (struct ppp_mppe_state *)arg; + + DPRINTF(("mppe_comp_reset\n")); + + (state->stats).ratio = 0; + + mppe_synchronize_key(state); +} + + +void +mppe_update_count(struct ppp_mppe_state *state) +{ + if(!state->stateless) + { + if ( 0xff == (state->ccount&0xff)){ + /* time to change keys */ + if ( 0xfff == (state->ccount&0xfff)){ + state->ccount = 0; + } else { + (state->ccount)++; + } + mppe_change_key(state); + } else { + state->ccount++; + } + } else { + if ( 0xFFF == (state->ccount & 0xFFF)) { + state->ccount = 0; + } else { + (state->ccount)++; + } + mppe_change_key(state); + } +} + + +/* the big nasty */ +int +mppe_compress(void *arg, struct mbuf **mret, struct mbuf *mp, + int isize, int osize) +{ + unsigned char *rptr; + struct ppp_mppe_state *state = (struct ppp_mppe_state *) arg; + int proto, olen; + unsigned char *wptr; + + *mret = NULL; + +#ifdef DIAGNOSTIC + /* XXX: dbj -- we cheat, and allow ourselves to go up to MPPE_OVHD + * past the osize handed to us. This implies we may send a packet + * greater than the mtu. We don't have much other choice. + */ + if (osize+MPPE_OVHD < isize+MPPE_OVHD) { + /* XXX: what should we do here? If we return NULL, the data + * will go out unencrypted! + */ + panic("ppp%d/mppe: Not enough space to encrypt packet: %d<%d+%d!\n", + state->unit,isize, osize, MPPE_OVHD); + } + + /* Sanity check that at least the header is contigous, + * if this ever fails, we should just copy the header out by hand + * rather than actually do a pullup. This can't happen + * with the current if_ppp implementation + */ + if (mp->m_len < PPP_HDRLEN) { + panic("ppp%d/mppe: m_pullup required to get ppp header!", + state->unit); + } +#endif + +#if defined(MPPE_DEBUG) && (MPEE_DEBUG > 1) + ppp_print_buffer("mppe_encrypt",rptr,isize); +#endif + + rptr = mtod(mp, unsigned char *); + + /* Check that the protocol is in the range we handle. */ + proto = PPP_PROTOCOL(rptr); + if (proto < 0x0021 || proto > 0x00FA ) + return 0; + +#ifdef DIAGNOSTIC + if (!(mp->m_flags & M_PKTHDR)) { + panic("ppp%d/mppe: expecting pkthdr in mbuf",state->unit); + } +#endif + + /* Allocate an mbuf chain to hold the encrypted packet */ + { + struct mbuf *mfirst = NULL; + struct mbuf *mprev; + struct mbuf *m = NULL; + int bleft = isize+MPPE_OVHD; + do { + mprev = m; + MGET(m,M_DONTWAIT, MT_DATA); + if (m == NULL) { + m_freem(mfirst); + /* XXX: what should we do here? If we return NULL, the data + * will go out unencrypted. We can't use M_WAITOK, since this + * will get get called from splsoftnet() + */ + panic("ppp%d/mppe: unable to allocate mbuf to encrypt packet", + state->unit); + } + m->m_len = 0; + if (mfirst == NULL) { + mfirst = m; + M_COPY_PKTHDR(m,mp); + if (bleft > MHLEN) { + MCLGET(m, M_DONTWAIT); + } + } else { + mprev->m_next = m; + if (bleft > MLEN) { + MCLGET(m, M_DONTWAIT); + } + } + bleft -= M_TRAILINGSPACE(m); + } while (bleft > 0); + *mret = mfirst; + } + + wptr = mtod(*mret, unsigned char *); + + /* Copy over the PPP header and store the 2-byte sequence number. */ + wptr[0] = PPP_ADDRESS(rptr); + wptr[1] = PPP_CONTROL(rptr); + wptr[2] = PPP_MPPE >>8; + wptr[3] = PPP_MPPE; + wptr += PPP_HDRLEN; + wptr[0] = MPPE_CTRLHI(state); + wptr[1] = MPPE_CTRLLO(state); + wptr += 2; + (*mret)->m_len = PPP_HDRLEN+2; + + state->bits=MPPE_BIT_ENCRYPTED; + mppe_update_count(state); + + /* March down input and output mbuf chains, encoding with RC4 */ + { + struct mbuf *mi = mp; /* mbuf in */ + struct mbuf *mo = *mret; /* mbuf out */ + int maxi, maxo; + maxi = mi->m_len-2; + maxo = M_TRAILINGSPACE(mo); + while (mi) { + if (maxi < maxo) { + RC4(&(state->RC4_send_key),maxi, + mtod(mi,unsigned char *)+mi->m_len-maxi, + mtod(mo,unsigned char *)+mo->m_len); + mo->m_len += maxi; + maxo -= maxi; + mi = mi->m_next; + if (mi) { + maxi = mi->m_len; + } + } else if (maxi > maxo) { + RC4(&(state->RC4_send_key),maxo, + mtod(mi,unsigned char *)+mi->m_len-maxi, + mtod(mo,unsigned char *)+mo->m_len); + mo->m_len += maxo; + maxi -= maxo; + mo = mo->m_next; + if (mo) { + maxo = M_TRAILINGSPACE(mo); + } + } else { + RC4(&(state->RC4_send_key),maxi, + mtod(mi,unsigned char *)+mi->m_len-maxi, + mtod(mo,unsigned char *)+mo->m_len); + mo->m_len += maxi; + mi = mi->m_next; + mo = mo->m_next; + if (mi) { + maxi = mi->m_len; + maxo = M_TRAILINGSPACE(mo); + } + } + } + } + olen=isize+MPPE_OVHD; + + (state->stats).comp_bytes += isize; + (state->stats).comp_packets++; + +#if defined(MPPE_DEBUG) && (MPEE_DEBUG > 1) + ppp_print_buffer("mppe_encrypt out",obuf,olen); +#endif + + return olen; +} + + +void +mppe_comp_stats(void *arg, struct compstat *stats) +{ + struct ppp_mppe_state *state = (struct ppp_mppe_state *)arg; + + /* this _SHOULD_ always be 1 */ + (state->stats).ratio = (state->stats).unc_bytes/(state->stats).comp_bytes; + + *stats = state->stats; + +} + + +/* the other big nasty */ +int +mppe_decompress(void *arg, struct mbuf *mp, struct mbuf **mret) +{ + struct ppp_mppe_state *state = (struct ppp_mppe_state *)arg; + int seq; + unsigned char *ibuf; + int isize; + unsigned char *obuf; + + if (!mp) { + DPRINTF(("ppp%d/mppe: null input packet\n",state->unit)); + return DECOMP_ERROR; + } + + /* Sanity check that at least the header is contigous, + * and the packet is long enough. + */ + if (mp->m_len <= PPP_HDRLEN + MPPE_OVHD) { + if (state->debug) { + DPRINTF(("ppp%d/mppe: short start mbuf (len=%d)\n", + state->unit, mp->m_len)); + } + + return DECOMP_ERROR; + } + + ibuf = mtod(mp,unsigned char *); + + /* Check the sequence number. */ + seq = MPPE_CCOUNT_FROM_PACKET(ibuf); + + if(!state->stateless && (MPPE_BITS(ibuf) & MPPE_BIT_FLUSHED)) { + state->decomp_error = 0; + state->ccount = seq; + } + + if(state->decomp_error) { + return DECOMP_ERROR; + } + + if (seq != state->ccount) { + if (state->debug) { + DPRINTF(("ppp%d/mppe: decompress: bad seq # %d, expected %d\n", + state->unit, seq, state->ccount)); + } + + while(state->ccount != seq) { + mppe_update_count(state); + } + + mppe_update_count(state); + + return DECOMP_ERROR; + } + + if(!(MPPE_BITS(ibuf) & MPPE_BIT_ENCRYPTED)) { + DPRINTF(("ppp%d/mppe: ERROR: not an encrypted packet\n",state->unit)); + mppe_synchronize_key(state); + return DECOMP_ERROR; + } + + /* Allocate an mbuf chain to hold the decrypted packet */ + { + struct mbuf *mfirst = 0; + struct mbuf *mprev; + struct mbuf *m = 0; + int bleft; + isize = 0; + for (m=mp; m; m= m->m_next) isize += m->m_len; + bleft = isize-MPPE_OVHD; + do { + mprev = m; + MGET(m,M_DONTWAIT, MT_DATA); + if (m == NULL) { + m_freem(mfirst); + DPRINTF(("ppp%d/mppe: unable to allocate mbuf to decrypt packet\n", + state->unit)); + return DECOMP_ERROR; + } + m->m_len = 0; + if (mfirst == NULL) { + mfirst=m; + M_COPY_PKTHDR(m,mp); + if (bleft > MHLEN) { + MCLGET(m, M_DONTWAIT); + } + } else { + mprev->m_next = m; + if (bleft > MLEN) { + MCLGET(m, M_DONTWAIT); + } + } + bleft -= M_TRAILINGSPACE(m); + } while (bleft > 0); + *mret = mfirst; + } + + obuf = mtod(*mret,unsigned char *); + + /* + * Fill in the first part of the PPP header. The protocol field + * comes from the decompressed data. + */ + obuf[0] = PPP_ADDRESS(ibuf); + obuf[1] = PPP_CONTROL(ibuf); + obuf += 2; + (*mret)->m_len += 2; + + { + if(!state->stateless && (MPPE_BITS(ibuf) & MPPE_BIT_FLUSHED)) + mppe_synchronize_key(state); + mppe_update_count(state); + + /* March down input and output mbuf chains, decoding with RC4 */ + { + struct mbuf *mi = mp; /* mbuf in */ + struct mbuf *mo = *mret; /* mbuf out */ + int maxi, maxo; + maxi = mi->m_len-6; /* adjust for PPP_HDRLEN and MPPE_OVERHEAD */ + maxo = M_TRAILINGSPACE(mo); + while (mi) { + if (maxi < maxo) { + RC4(&(state->RC4_recv_key),maxi, + mtod(mi,unsigned char *)+mi->m_len-maxi, + mtod(mo,unsigned char *)+mo->m_len); + mo->m_len += maxi; + maxo -= maxi; + mi = mi->m_next; + if (mi) { + maxi = mi->m_len; + } + } else if (maxi > maxo) { + RC4(&(state->RC4_recv_key),maxo, + mtod(mi,unsigned char *)+mi->m_len-maxi, + mtod(mo,unsigned char *)+mo->m_len); + mo->m_len += maxo; + maxi -= maxo; + mo = mo->m_next; + if (mo) { + maxo = M_TRAILINGSPACE(mo); + } + } else { + RC4(&(state->RC4_recv_key),maxi, + mtod(mi,unsigned char *)+mi->m_len-maxi, + mtod(mo,unsigned char *)+mo->m_len); + mo->m_len += maxi; + mi = mi->m_next; + mo = mo->m_next; + if (mi) { + maxi = mi->m_len; + maxo = M_TRAILINGSPACE(mo); + } + } + } + } + + (state->stats).unc_bytes += (isize-MPPE_OVHD); + (state->stats).unc_packets ++; + + return DECOMP_OK; + } +} + + +/* Incompressible data has arrived - add it to the history. */ +void +mppe_incomp(void *arg, struct mbuf *mp) +{ + struct ppp_mppe_state *state = (struct ppp_mppe_state *)arg; + struct mbuf *m; + for (m=mp;m;m = m->m_next) { + (state->stats).inc_bytes += m->m_len; + } + (state->stats).inc_packets++; +} + + +/************************************************************* + * Module interface table + *************************************************************/ + +/* + * Procedures exported to if_ppp.c. + */ +struct compressor ppp_mppe = { + CI_MPPE, /* compress_proto */ + mppe_comp_alloc, /* comp_alloc */ + mppe_comp_free, /* comp_free */ + mppe_comp_init, /* comp_init */ + mppe_comp_reset, /* comp_reset */ + mppe_compress, /* compress */ + mppe_comp_stats, /* comp_stat */ + mppe_comp_alloc, /* decomp_alloc */ + mppe_comp_free, /* decomp_free */ + mppe_decomp_init, /* decomp_init */ + mppe_comp_reset, /* decomp_reset */ + mppe_decompress, /* decompress */ + mppe_incomp, /* incomp */ + mppe_comp_stats, /* decomp_stat */ +}; +