-- RPC: Authentication and security facilities --RPCSecurity.mesa -- Andrew Birrell September 7, 1982 1:47 pm DIRECTORY BufferDefs USING[ PupBuffer ], DESFace, Heap USING[ FreeNode, MakeNode, systemZone ], Inline USING[ LongCOPY ], NameInfoDefs USING[ AuthenticateKey, CheckStamp ], PSB USING[ PsbIndex, PsbNull ], PupDefs USING[ GetLocalPupAddress, PupRouterSendThis, ReturnFreePupBuffer], PupTypes USING[ PupAddress ], MesaRPC USING[ AuthenticateFailed, Conversation, ConversationLevel, EncryptionKey, maxPrincipalLength, Principal, SecurityLevel, unencrypted ], RPCInternal USING[ ConversationObject ], RPCLupine USING[ DataLength, Dispatcher, GetStubPkt, Header, pktOverhead, RPCPkt, StubPkt ], RPCPkt USING[ CallCount, ConnectionID, ConversationID, DispatcherDetails, Header, Machine, PktConversationID, PktExchange, PktID, pktLengthOverhead, SetupResponse ], RPCPrivate USING[ rpcSocket, ReturnBuffer ], Runtime USING[ IsBound ], System USING[ GetClockPulses, GetGreenwichMeanTime, SecondsSinceEpoch ]; RPCSecurity: MONITOR IMPORTS DESFace, Heap, Inline, NameInfoDefs, PupDefs, MesaRPC, RPCLupine, RPCPkt, RPCPrivate, Runtime, System EXPORTS MesaRPC--encryption stuff--, RPCInternal--GetAuthenticator--, RPCLupine SHARES RPCLupine = BEGIN Header: PUBLIC TYPE = RPCPkt.Header; ConcreteHeader: PROC[abstract: LONG POINTER TO RPCLupine.Header] RETURNS[LONG POINTER TO Header] = INLINE { RETURN[ abstract ] }; GiveBackBuffer: PROC[b: BufferDefs.PupBuffer] = -- NOTE: calls of this must be made outside our monitor, because -- RPCPrivate.ReturnBuffer acquires the EthernetDriver monitor, -- and the EthernetDriver may call EnqueueRecvd which acquires -- our monitor! IF Runtime.IsBound[RPCPrivate.ReturnBuffer] THEN RPCPrivate.ReturnBuffer ELSE PupDefs.ReturnFreePupBuffer; CopyPrincipal: PROC[from: MesaRPC.Principal, to: LONG POINTER] = { Inline.LongCOPY[from: from, to: to, nwords: SIZE[StringBody[from.length]]] }; AllocPrincipal: PROC[from: MesaRPC.Principal] RETURNS[to: MesaRPC.Principal] = BEGIN IF from = NIL THEN RETURN[NIL]; to _ Heap.MakeNode[n: SIZE[StringBody[from.length]]]; CopyPrincipal[from, to]; END; CopyAuth: PROC[from: Authenticator, to: LONG POINTER] = { Inline.LongCOPY[from: BASE[from], to: to, nwords: LENGTH[from]] }; AllocAuth: PROC[from: Authenticator] RETURNS[to: Authenticator] = BEGIN IF BASE[from] = NIL THEN RETURN[DESCRIPTOR[NIL,0]]; to _ DESCRIPTOR[Heap.MakeNode[n: LENGTH[from]], LENGTH[from]]; CopyAuth[from, BASE[to]]; END; -- ******** Conversation management ******** -- ConversationID: PUBLIC TYPE = RPCPkt.ConversationID; ConversationObject: PUBLIC TYPE = RPCInternal.ConversationObject; Conversation: TYPE = LONG POINTER TO ConversationObject; lastConversation: ConversationID; firstConversation: PUBLIC RPCPkt.PktConversationID; GenerateConversation: PUBLIC PROC RETURNS[conversation: Conversation] = { RETURN[EntryGenerate[none, , NIL, NIL, DESFace.nullKey, DESCRIPTOR[NIL,0]]] }; EntryGenerate: ENTRY PROC[ level: MesaRPC.SecurityLevel, iv: DESFace.IV, originator, responder: MesaRPC.Principal, convKey: DESFace.Key, auth: Authenticator] RETURNS[ conversation: Conversation ] = BEGIN lastConversation.count.ls _ lastConversation.count.ls+1; IF lastConversation.count.ls = 0 THEN lastConversation.count.ms _ lastConversation.count.ms+1; conversation _ AddConversation[lastConversation, level, iv, originator, responder, convKey, auth]; END; Authenticator: TYPE = LONG DESCRIPTOR FOR ARRAY OF WORD; -- for A talking to B using key CK, contains: -- {KY}KB, spare, { {CK}KB, spare, time, A }KY . -- The keys are single cipher blocks. -- The rest is encrypted with CBC-check using a zero IV -- AuthenticatorLayout: TYPE = MACHINE DEPENDENT RECORD[ ky(0): DESFace.Key, kySpare(4): DESFace.Key, -- space for larger keys! -- ck(8): DESFace.Key, ckSpare(12): DESFace.Key, -- space for larger keys! -- time(16): LONG CARDINAL, a(18): StringBody]; BadConversationLevel: ERROR = CODE; StartConversation: PUBLIC PROC[caller: MesaRPC.Principal, key: MesaRPC.EncryptionKey, callee: MesaRPC.Principal, level: MesaRPC.ConversationLevel] RETURNS[conversation: Conversation] = BEGIN authenticator: Authenticator; convKey: DESFace.Key; iv: DESFace.IV; IF level NOT IN MesaRPC.ConversationLevel THEN ERROR BadConversationLevel[]; [authenticator, convKey, iv] _ Authenticate[caller, key, callee]; RETURN[ EntryGenerate[level, iv, caller, callee, convKey, authenticator] ] END; Authenticate: PROC[caller: MesaRPC.Principal, key: MesaRPC.EncryptionKey, callee: MesaRPC.Principal] RETURNS[ authenticator: Authenticator, convKey: DESFace.Key, iv: DESFace.IV] = BEGIN -- TEMP: This entire procedure should use a secure R-Server protocol! -- nBlks: CARDINAL = ( SIZE[AuthenticatorLayout] - SIZE[StringBody[0]] + SIZE[StringBody[caller.length]] + SIZE[DESFace.Block]-1 --rounding-- ) / SIZE[DESFace.Block]; authLength: CARDINAL = nBlks * SIZE[DESFace.Block]; -- TEMP: copy into MDS for GrapevineUser's benefit -- buff: STRING = [MesaRPC.maxPrincipalLength]; IF caller.length > buff.maxlength THEN ERROR MesaRPC.AuthenticateFailed[badCaller]; IF callee.length > buff.maxlength THEN ERROR MesaRPC.AuthenticateFailed[badCallee]; Inline.LongCOPY[from:caller, to: buff, nwords: SIZE[StringBody[caller.length]]]; SELECT NameInfoDefs.AuthenticateKey[buff, key] FROM group, notFound => ERROR MesaRPC.AuthenticateFailed[badCaller]; allDown => ERROR MesaRPC.AuthenticateFailed[communications]; badPwd => ERROR MesaRPC.AuthenticateFailed[badKey]; ENDCASE => NULL; Inline.LongCOPY[from:callee, to: buff, nwords: SIZE[StringBody[callee.length]]]; SELECT NameInfoDefs.CheckStamp[buff] FROM group, notFound => ERROR MesaRPC.AuthenticateFailed[badCallee]; allDown => ERROR MesaRPC.AuthenticateFailed[communications]; ENDCASE => NULL; iv _ DESFace.GetRandomIV[]; authenticator _ DESCRIPTOR[Heap.MakeNode[n:authLength], authLength]; BEGIN -- TEMP: construct authenticator in clear ourselves! -- kb: DESFace.Key _ DESFace.nullKey -- !!! --; authRec: LONG POINTER TO AuthenticatorLayout = LOOPHOLE[BASE[authenticator]]; authRec.ky _ DESFace.GetRandomKey[]; authRec.ck _ convKey _ DESFace.GetRandomKey[]; authRec.time _ 0; Inline.LongCOPY[from:caller, to: @(authRec.a), nwords: SIZE[StringBody[caller.length]]]; DESFace.CorrectParity[@kb]; DESFace.EncryptBlock[key: kb, from: LOOPHOLE[@authRec.ck], to: LOOPHOLE[@authRec.ck] ]; DESFace.CBCCheckEncrypt[key: authRec.ky, nBlks: nBlks-2, from: LOOPHOLE[@authRec.ck], to: LOOPHOLE[@authRec.ck], seed: [0,0,0,0] ]; DESFace.EncryptBlock[key: kb, from: LOOPHOLE[@authRec.ky], to: LOOPHOLE[@authRec.ky] ]; END; END; ConvHashKey: TYPE = [0..127]; conversations: LONG POINTER TO ARRAY ConvHashKey OF Conversation = Heap.systemZone.NEW[ARRAY ConvHashKey OF Conversation _ ALL[NIL]]; InconsistentHashTable: ERROR = CODE; EntryAddConversation: ENTRY PROC[ id: ConversationID, level: MesaRPC.SecurityLevel, iv: DESFace.IV, originator, responder: MesaRPC.Principal, convKey: DESFace.Key, auth: Authenticator] RETURNS[ conversation: Conversation ] = INLINE { RETURN[AddConversation[id, level, iv, originator, responder, convKey, auth]] }; AddConversation: INTERNAL PROC[ id: ConversationID, level: MesaRPC.SecurityLevel, iv: DESFace.IV, originator, responder: MesaRPC.Principal, convKey: DESFace.Key, auth: Authenticator] RETURNS[ conversation: Conversation ] = BEGIN dataPtr: LONG POINTER TO Conversation _ @conversations[id.count.ls MOD SUCC[LAST[ConvHashKey]]]; DO SELECT TRUE FROM dataPtr^ = NIL => BEGIN conversation _ Heap.systemZone.NEW[ConversationObject_ [NIL, id, level, convKey, iv, AllocPrincipal[originator], AllocPrincipal[responder], auth] ]; dataPtr^ _ conversation; EXIT; END; dataPtr^.id = id => { conversation _ dataPtr^; EXIT -- already there --}; ENDCASE => dataPtr _ @(dataPtr^.next); ENDLOOP; END; UnknownConversation: ERROR = CODE; EndConversation: PUBLIC ENTRY PROC[conversation: Conversation] = BEGIN dataPtr: LONG POINTER TO Conversation _ @conversations[conversation.id.count.ls MOD SUCC[LAST[ConvHashKey]]]; IF conversation.id.originator # myHost THEN RETURN WITH ERROR UnknownConversation[]; DO SELECT TRUE FROM dataPtr^ = NIL => RETURN WITH ERROR UnknownConversation[]; dataPtr^ = conversation => BEGIN dataPtr^ _ dataPtr^.next; IF conversation.originator # NIL THEN Heap.FreeNode[p: conversation.originator]; IF conversation.responder # NIL THEN Heap.FreeNode[p: conversation.responder]; IF BASE[conversation.authenticator] # NIL THEN Heap.FreeNode[p: BASE[conversation.authenticator]]; Heap.systemZone.FREE[@conversation]; EXIT; END; ENDCASE => dataPtr _ @(dataPtr^.next); ENDLOOP; END; InvalidateConversations: INTERNAL PROC = BEGIN -- Invalidate conversations for which we are the originator, such that client -- will get an error if he uses them. FOR hash: ConvHashKey IN ConvHashKey DO FOR conversation: Conversation _ conversations[hash], conversation.next UNTIL conversation = NIL DO IF conversation.id.originator = myHost THEN conversation.id.originator _ [[0],[0]] ENDLOOP; ENDLOOP; END; GetConversationID: PUBLIC ENTRY PROC[conversation: Conversation] RETURNS[id: ConversationID] = { RETURN[ IF conversation = NIL THEN [[[0],[0]],[0,0]] ELSE conversation.id ] }; GetCaller: PUBLIC ENTRY PROC[conversation: Conversation] RETURNS[ MesaRPC.Principal ] = { RETURN[ IF conversation = NIL THEN NIL ELSE conversation.originator ] }; GetLevel: PUBLIC ENTRY PROC[conversation: Conversation] RETURNS[ level: MesaRPC.SecurityLevel ] = { RETURN[ IF conversation = NIL THEN none ELSE conversation.level ] }; -- ******** Packet encryption and decryption ******** -- encryptedHeaderLength: CARDINAL = SIZE[RPCPkt.PktID] + SIZE[RPCPkt.DispatcherDetails]; Check: TYPE = LONG CARDINAL; -- cipher checksum at end of packet -- -- bottom 2 bits indicate block rounding; remaining 30 bits should be 0 -- EncryptPkt: PUBLIC PROC[pkt: RPCLupine.RPCPkt, l: RPCLupine.DataLength] RETURNS[CARDINAL] = BEGIN header: LONG POINTER TO RPCPkt.Header = @pkt.header; conv: Conversation = pkt.convHandle; IF conv # NIL AND conv.level # none AND conv.level # authOnly THEN BEGIN words: CARDINAL = l -- stub data words -- + encryptedHeaderLength + SIZE[Check]; nBlks: CARDINAL = (words + SIZE[DESFace.Block]-1 -- round up --) / SIZE[DESFace.Block]; blocks: DESFace.Blocks = LOOPHOLE[@header.pktID]; checkPtr: LONG POINTER TO Check = LOOPHOLE[@blocks[nBlks] - SIZE[Check]]; checkPtr^ _ nBlks * SIZE[DESFace.Block] - words; SELECT conv.level FROM ECB => DESFace.ECBEncrypt[conv.key, nBlks, blocks, blocks]; CBC => DESFace.CBCEncrypt[conv.key, nBlks, blocks, blocks, conv.iv]; CBCCheck => DESFace.CBCCheckEncrypt[conv.key, nBlks, blocks, blocks, conv.iv]; ENDCASE => ERROR; RETURN[nBlks * SIZE[DESFace.Block] - encryptedHeaderLength + RPCPkt.pktLengthOverhead] END ELSE RETURN[l + RPCPkt.pktLengthOverhead] END; DecryptPkt: PUBLIC PROC[header: LONG POINTER TO RPCPkt.Header, convHandle: Conversation] RETURNS[ok: BOOLEAN, l: RPCLupine.DataLength] = BEGIN ok _ TRUE; IF convHandle # NIL AND convHandle.level # none AND convHandle.level # authOnly THEN BEGIN nBlks: CARDINAL = (header.length - RPCPkt.pktLengthOverhead + encryptedHeaderLength) / SIZE[DESFace.Block]; blocks: DESFace.Blocks = LOOPHOLE[@header.pktID]; checkPtr: LONG POINTER TO Check = LOOPHOLE[@blocks[nBlks] - SIZE[Check]]; SELECT convHandle.level FROM ECB => DESFace.ECBDecrypt[convHandle.key, nBlks, blocks, blocks]; CBC => DESFace.CBCDecrypt[convHandle.key, nBlks, blocks, blocks, convHandle.iv]; CBCCheck => DESFace.CBCCheckDecrypt[convHandle.key, nBlks, blocks, blocks, convHandle.iv]; ENDCASE => ERROR; IF checkPtr^ IN [0..SIZE[DESFace.Block]) THEN l _ nBlks * SIZE[DESFace.Block] - checkPtr^ - SIZE[Check] - encryptedHeaderLength ELSE ok _ FALSE; END ELSE l _ header.length - RPCPkt.pktLengthOverhead; END; -- ******** Requests-For-Authenticator's (RFA's) ******** -- RFARequest: TYPE = MACHINE DEPENDENT RECORD[ -- conversationID, {pktID}CK, nonceID callerConv(0): RPCPkt.PktConversationID, callerPktID(2): RPCPkt.PktID -- still encrypted --, nonceID(6): LONG CARDINAL -- nominated by requester --]; RFAResponse: TYPE = MACHINE DEPENDENT RECORD[ -- {iv, connectionID, callCount, nonceID+1}CK, -- null, authScheme, authLength, authenticator, callee. -- The initial part is encrypted with CBC using a zero IV. iv(0): DESFace.IV, connection(4): RPCPkt.ConnectionID, call(8): RPCPkt.CallCount, nonceID(10): LONG CARDINAL, -- ends here if callee originated the conversation -- level(12): MesaRPC.SecurityLevel, -- ends here if level = none -- authLength(13): CARDINAL, -- length of authenticator (words) -- authenticator(14): AuthenticatorLayout -- followed by Callee principal name -- ]; responseCKBlocks: CARDINAL = 12 / SIZE[DESFace.Block]; -- Size of {iv, connectionID, callCount, nonceID+1} rfaDataLength: RPCLupine.DataLength = SIZE[RFAResponse] - SIZE[StringBody[0]] + 2 * SIZE[StringBody[MesaRPC.maxPrincipalLength]]; myHost: RPCPkt.Machine; GetConnectionState: PUBLIC PROC[ decrypted: BOOLEAN, callPkt: RPCLupine.RPCPkt] RETURNS[ ok: BOOLEAN, id: RPCPkt.ConnectionID, call: RPCPkt.CallCount, conv: Conversation, l: RPCLupine.DataLength] = BEGIN -- The RFA packet looks a lot like the first packet of a new call. -- The response, a data packet, will look a lot like the result of -- this call. callHeader: LONG POINTER TO Header = @callPkt.header; rfaPktSpace: ARRAY [1..rfaDataLength + RPCLupine.pktOverhead] OF WORD; rfaPkt: RPCLupine.StubPkt = RPCLupine.GetStubPkt[@rfaPktSpace]; myNonceID: LONG CARDINAL = System.GetClockPulses[]; request: LONG POINTER TO RFARequest = LOOPHOLE[LONG[@(rfaPkt[0])]]; response: LONG POINTER TO RFAResponse = LOOPHOLE[LONG[@(rfaPkt[0])]]; rfaHeader: LONG POINTER TO Header = @rfaPkt.header; request.callerConv _ callHeader.conv; request.callerPktID _ callHeader.pktID; --still encrypted-- request.nonceID _ myNonceID; rfaPkt.convHandle _ MesaRPC.unencrypted; rfaHeader^ _ ConcreteHeader[@callPkt.header]^; RPCPkt.SetupResponse[rfaHeader]; rfaHeader.conv _ firstConversation; rfaHeader.pktID.activity _ 0; -- rfaHeader.pktID.callSeq will be set by PktExchange -- rfaHeader.pktID.pktSeq _ 0; BEGIN responseLength: RPCLupine.DataLength _ RPCPkt.PktExchange[rfaPkt, SIZE[RFARequest], rfaDataLength, authReq].newLength; IF responseLength < SIZE[RFAResponse] - SIZE[AuthenticatorLayout] THEN RETURN[FALSE,,,,]; END; IF response.level = none THEN { l _ callHeader.length - RPCPkt.pktLengthOverhead; conv _ NIL } ELSE BEGIN -- response = {IV,cond,call,nonce}CK,,{KY}KB,{{CK}KB,time,A}KY --TEMP-- kb: DESFace.Key _ DESFace.nullKey -- !!! --; DESFace.CorrectParity[@kb]; DESFace.DecryptBlock[key: kb, from: LOOPHOLE[@response.authenticator.ky], to: LOOPHOLE[@response.authenticator.ky] ]; DESFace.CBCCheckDecrypt[key: response.authenticator.ky, nBlks: response.authLength/SIZE[DESFace.Block] - 2, from: LOOPHOLE[@response.authenticator.ck], to: LOOPHOLE[@response.authenticator.ck], seed: [0,0,0,0] ]; DESFace.DecryptBlock[key: kb, from: LOOPHOLE[@response.authenticator.ck], to: LOOPHOLE[@response.authenticator.ck] ]; DESFace.CBCCheckDecrypt[key: response.authenticator.ck, nBlks: responseCKBlocks, from: LOOPHOLE[response], to: LOOPHOLE[response], seed: [0,0,0,0] ]; conv _ EntryAddConversation[ -- Construct a ConversationID from a PktConversationID -- [IF response.connection.conv.originator = caller THEN response.connection.caller ELSE myHost, [response.connection.conv.ls, response.connection.conv.ms]], response.level, response.iv, @response.authenticator.a, NIL, response.authenticator.ck, DESCRIPTOR[NIL,0] ]; callPkt.convHandle _ conv; IF NOT decrypted THEN BEGIN ok: BOOLEAN; [ok, l] _ DecryptPkt[callHeader, conv]; IF NOT ok THEN RETURN[FALSE,,,,]; END; END; -- correctness of nonceID means he knows CK. IF response.nonceID # myNonceID+1 THEN RETURN[FALSE,,,,]; IF response.connection # [ callHeader.conv, callHeader.srceHost, callHeader.pktID.activity] THEN RETURN[FALSE,,,,]; IF response.call # callHeader.pktID.callSeq THEN RETURN[FALSE,,,,]; RETURN[TRUE, response.connection, response.call, conv, l]; END; ReplyToRFA: PUBLIC PROC[b: BufferDefs.PupBuffer, callHeader: LONG POINTER TO RPCPkt.Header, -- encrypted -- callPktID: RPCPkt.PktID -- clear --, convHandle: Conversation] RETURNS[BOOLEAN] = BEGIN -- Remember BufferDefs.PupBuffer's don't look like RPCLupine.RPCPkt's! -- recvdHeader: LONG POINTER TO RPCPkt.Header = LOOPHOLE[@b.pupLength]; -- copy request out of buffer to avoid overwriting our args! -- request: RFARequest = LOOPHOLE[recvdHeader+SIZE[RPCPkt.Header], LONG POINTER TO RFARequest]^; response: LONG POINTER TO RFAResponse = LOOPHOLE[recvdHeader+SIZE[RPCPkt.Header]]; used: CARDINAL _ SIZE[RFAResponse] - SIZE[AuthenticatorLayout]; IF request.callerConv # callHeader.conv OR request.callerPktID # callHeader.pktID THEN { GiveBackBuffer[b]; RETURN[FALSE] }; response.connection _ [callHeader.conv, callHeader.srceHost, callPktID.activity]; response.call _ callPktID.callSeq; response.nonceID _ request.nonceID+1; IF callHeader.conv = firstConversation THEN response.level _ none ELSE BEGIN IF callHeader.conv.originator = callee THEN --TEMP--ERROR; response.level _ convHandle.level; response.iv _ convHandle.iv; DESFace.CBCCheckEncrypt[key: convHandle.key, nBlks: responseCKBlocks, from: LOOPHOLE[response], to: LOOPHOLE[response], seed: [0,0,0,0] ]; response.authLength _ LENGTH[convHandle.authenticator]; CopyAuth[from: convHandle.authenticator, to: @response.authenticator]; used _ used + LENGTH[convHandle.authenticator]; CopyPrincipal[from: convHandle.originator, to: @response.authenticator+LENGTH[convHandle.authenticator]]; used _ used + SIZE[StringBody[convHandle.originator.length]]; END; BEGIN header: Header _ recvdHeader^; RPCPkt.SetupResponse[@header]; header.length _ RPCPkt.pktLengthOverhead + used; header.oddByte _ no; header.type _ [0,rpc,end,dontAck,data]; header.pktID.pktSeq _ header.pktID.pktSeq+1; header.srceHost _ myHost; header.srceSoc _ RPCPrivate.rpcSocket; header.srcePSB _ PSB.PsbNull; recvdHeader^ _ header; END; PupDefs.PupRouterSendThis[b]; RETURN[TRUE] END; -- Initialization -- Initialize: ENTRY PROC = BEGIN myAddr: PupTypes.PupAddress _ PupDefs.GetLocalPupAddress[RPCPrivate.rpcSocket,NIL]; now: MACHINE DEPENDENT RECORD[ls: CARDINAL, spare: [0..1], ms: [0..77777B]] = LOOPHOLE[System.SecondsSinceEpoch[System.GetGreenwichMeanTime[]]]; firstHandle: Conversation; myHost _ [myAddr.net, myAddr.host]; lastConversation _ [originator: myHost, count:[ls: now.ls, ms: now.ms] ]; -- Use first available conversationID for unencrypted conversations. firstHandle _ AddConversation[lastConversation, none, , NIL, NIL, DESFace.nullKey, DESCRIPTOR[NIL,0]]; firstConversation _ [ls: firstHandle.id.count.ls, originator: caller, ms: firstHandle.id.count.ms]; END; Restart: ENTRY PROC = BEGIN InvalidateConversations[]; END; DO Initialize[]; STOP; Restart[]; ENDLOOP; END. Ê X– "Mesa" style˜Iprocš Ïc/œœ-œÏk œ žœ žœ-žœžœ!žœžœ!žœIžœžœ žœ#žœ_žœ¶žœ'žœžœKžœžœožœœ œ žœžœ žœžœÏnœžœ žœžœžœ žœžœžœžœ žœžœŸœžœAœ@œ?œœžœ,žœžœŸ œžœžœžœ4žœŸœžœžœžœžœžœžœžœžœžœ9žœŸœžœžœžœžœžœ Ÿ œžœžœžœžœžœ žœžœžœž œžœ ž œžœ žœžœ žœ/œžœžœ/žœžœ8žœžœžœžœOžœŸœžœžœžœ#žœžœžœž œžœ Ÿ œžœžœGžœºžœ#žœ>žœ!žœãžœžœžœž œžœžœžœžœ.œ1œ&œ:œžœžœž œžœBœAœžœžœ9žœžœŸœžœžœéžœ!žœGžœžœžœžœžœžœ_žœGžœŸ œžœtžœ|žœžœHœ žœžœ:žœ4žœ@žœ œžœžœ žœ5œ žœ#žœ žœžœ*žœ žœžœYžœ žœ)žœžœ7žœ;žœ'žœžœ3žœ žœžœžœ7žœ/žœžœ2ž œ-žœ7œ' œžœžœžœžœžœçžœ€žœ.žœžœ2žœžœ.žœžœžœžœžœžœžœžœ žœ"žœžœ žœžœžœžœžœŸœžœžœ€žœÎžœ!žœžœKŸœžœžœ€žœÎžœ#žœ žœžœžœ9žœžœžœžœžœžœžœžœ žœ)žœ#žœçžœ žœAžœœžœ"žœžœžœžœŸœžœžœžœ!žœ žœžœžœEžœžœžœžœ'žœžœžœžœžœžœžœžœžœžœžœžœCžœ-žœžœ žœ5žœžœ žœ4žœžœžœ žœžœ8žœžœ žœžœ"žœžœŸœžœžœžœuœžœžœžœžœJžœžœžœžœ%žœ(žœžœžœŸœžœžœžœ:žœžœžœžœ žœžœŸ œžœžœžœ-žœžœžœžœžœžœžœŸœžœžœžœ-žœ'žœžœžœžœžœ8œžœžœžœ$žœžœžœ&œJœŸ œžœžœAžœžœžœ žœžœžœGžœžœžœžœžœžœžœœMžœžœ žœœžœ1žœ"žœžœžœžœžœ%žœ žœ žœ žœ€žœÛžœžœ žœ žœgžœžœžœ!žœŸ œžœžœ žœžœžœRžœžœžœžœžœžœžœžœžœžœžœ{žœ1žœ"žœžœžœžœžœžœžœ žœ†žœóžœžœ žœ žœžœžœ žœ>žœ7žœžœ žœžœ1žœ<œžœžœž œžœ&œOœžœžœœžœžœž œžœ/œ8œ;œžœhžœžœ5œ/œžœ%œ,'œžœžœ4œ'žœžœ'žœFŸœžœžœ žœTžœžœàžœCœCœœžœžœžœ*žœ,žœžœQžœžœ(žœžœžœžœžœžœžœžœžœžœžœžœžœlœç8œ!žœOžœ5žœžœžœžœžœžœ žœžœžœ<žœžœžœ?œœ# œlžœ>žœ›žœ;žœDžœ™žœ@žœ¿žœ2žœs9œžœ?žœ-žœÏžœ=ž œžœ0žœžœžœžœžœBžœžœžœžœžœžœ žœ-œžœ žœžœžœ žœlžœžœžœ žœ*žœžœžœ žœžœ2žœŸ œžœžœ>žœžœžœœ1 œDžœžœžœIœžœžœžœžœ?œžœ žœ3žœžœžœžœžœžœžœ žœžœžœžœžœ&žœ*žœžœžœ¨žœ'žœžœžœžœ%žœžœäžœ2žœ\žœ€žœžœ2žœ3žœžœÐžœ'žœ$žœžœžœœŸ œžœžœžœ\žœ žœž œžœžœ/žœÌEœ:žœžœ3ž œžœ…žœŸœžœžœžœ žœžœžœ žœžœ˜Í¶—…—[Pe®