diff options
author | thayes%netscape.com <devnull@localhost> | 2000-06-10 19:00:45 +0000 |
---|---|---|
committer | thayes%netscape.com <devnull@localhost> | 2000-06-10 19:00:45 +0000 |
commit | 779b2555bb69fda1a21e3d494b4a1aac2e662f9d (patch) | |
tree | f2be9df2f1092d66345ebff4712687438e442b87 | |
parent | 09d7413f35b2d70553a53db04e9ee6ee5488f3c8 (diff) | |
parent | bd042e2db48225ac17f436ba5347b365a7a1c422 (diff) | |
download | nss-hg-779b2555bb69fda1a21e3d494b4a1aac2e662f9d.tar.gz |
Add permanent (token) key for supporting Secret Decoder Ring (SDR)
Bug 26085
-rw-r--r-- | security/nss/lib/pk11wrap/pk11func.h | 4 | ||||
-rw-r--r-- | security/nss/lib/pk11wrap/pk11sdr.c | 17 | ||||
-rw-r--r-- | security/nss/lib/pk11wrap/pk11skey.c | 106 | ||||
-rw-r--r-- | security/nss/lib/softoken/pkcs11.c | 328 |
4 files changed, 397 insertions, 58 deletions
diff --git a/security/nss/lib/pk11wrap/pk11func.h b/security/nss/lib/pk11wrap/pk11func.h index a6a6b5a0f..21003d1ce 100644 --- a/security/nss/lib/pk11wrap/pk11func.h +++ b/security/nss/lib/pk11wrap/pk11func.h @@ -233,6 +233,10 @@ CK_OBJECT_HANDLE PK11_ImportPublicKey(PK11SlotInfo *slot, SECKEYPublicKey *pubKey, PRBool isToken); PK11SymKey *PK11_KeyGen(PK11SlotInfo *slot,CK_MECHANISM_TYPE type, SECItem *param, int keySize,void *wincx); + +/* Key Generation specialized for SDR (fixed DES3 key) */ +PK11SymKey *PK11_GenDES3TokenKey(PK11SlotInfo *slot, SECItem *keyid, void *cx); + SECStatus PK11_PubWrapSymKey(CK_MECHANISM_TYPE type, SECKEYPublicKey *pubKey, PK11SymKey *symKey, SECItem *wrappedKey); SECStatus PK11_WrapSymKey(CK_MECHANISM_TYPE type, SECItem *params, diff --git a/security/nss/lib/pk11wrap/pk11sdr.c b/security/nss/lib/pk11wrap/pk11sdr.c index 460591835..07d9c1354 100644 --- a/security/nss/lib/pk11wrap/pk11sdr.c +++ b/security/nss/lib/pk11wrap/pk11sdr.c @@ -63,8 +63,8 @@ static SEC_ASN1Template template[] = { }; static unsigned char keyID[] = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }; static SECItem keyIDItem = { @@ -168,9 +168,18 @@ PK11SDR_Encrypt(SECItem *keyid, SECItem *data, SECItem *result, void *cx) /* Find the key to use */ pKeyID = keyid; - if (pKeyID->len == 0) pKeyID = &keyIDItem; /* Use default value */ + if (pKeyID->len == 0) { + pKeyID = &keyIDItem; /* Use default value */ + + /* Try to find the key */ + key = PK11_FindFixedKey(slot, type, pKeyID, cx); + + /* If the default key doesn't exist yet, try to create it */ + if (!key) key = PK11_GenDES3TokenKey(slot, pKeyID, cx); + } else { + key = PK11_FindFixedKey(slot, type, pKeyID, cx); + } - key = PK11_FindFixedKey(slot, type, pKeyID, cx); if (!key) { rv = SECFailure; goto loser; } params = PK11_GenerateNewParam(type, key); diff --git a/security/nss/lib/pk11wrap/pk11skey.c b/security/nss/lib/pk11wrap/pk11skey.c index 63c13527a..52f2ce30d 100644 --- a/security/nss/lib/pk11wrap/pk11skey.c +++ b/security/nss/lib/pk11wrap/pk11skey.c @@ -130,6 +130,7 @@ PK11_CreateNewObject(PK11SlotInfo *slot, CK_SESSION_HANDLE session, PORT_SetError( PK11_MapError(crv) ); rv = SECFailure; } + if (session == CK_INVALID_SESSION) { if (token) { PK11_RestoreROSession(slot, rwsession); @@ -137,6 +138,7 @@ PK11_CreateNewObject(PK11SlotInfo *slot, CK_SESSION_HANDLE session, PK11_ExitSlotMonitor(slot); } } + return rv; } @@ -1132,58 +1134,78 @@ pk11_ForceSlot(PK11SymKey *symKey,CK_MECHANISM_TYPE type, * from this interface! */ PK11SymKey * -PK11_KeyGen(PK11SlotInfo *slot, CK_MECHANISM_TYPE type, SECItem *param, - int keySize, void *wincx) +PK11_TokenKeyGen(PK11SlotInfo *slot, CK_MECHANISM_TYPE type, SECItem *param, + int keySize, SECItem *keyid, PRBool isToken, void *wincx) { - CK_ULONG key_size = 0; - /* we have to use these native types because when we call PKCS 11 modules - * we have to make sure that we are using the correct sizes for all the - * parameters. */ - CK_BBOOL ckfalse = CK_FALSE; - CK_BBOOL cktrue = CK_TRUE; - CK_ATTRIBUTE genTemplate[2]; + PK11SymKey *symKey; + CK_ATTRIBUTE genTemplate[4]; + CK_ATTRIBUTE *attrs = genTemplate; int count = sizeof(genTemplate)/sizeof(genTemplate[0]); + CK_SESSION_HANDLE session; CK_MECHANISM mechanism; - CK_MECHANISM_TYPE key_gen_mechanism; - PK11SymKey *symKey; CK_RV crv; - CK_ATTRIBUTE *attrs = genTemplate; PRBool weird = PR_FALSE; /* hack for fortezza */ + CK_BBOOL ckfalse = CK_FALSE; + CK_BBOOL cktrue = CK_TRUE; if ((keySize == -1) && (type == CKM_SKIPJACK_CBC64)) { weird = PR_TRUE; keySize = 0; } + /* TNH: Isn't this redundant, since "handleKey" will set defaults? */ PK11_SETATTRS(attrs, (!weird) ? CKA_ENCRYPT : CKA_DECRYPT, &cktrue, sizeof(CK_BBOOL)); attrs++; - key_size = keySize; - if (key_size != 0) { + + if (keySize != 0) { + CK_ULONG key_size = keySize; /* Convert to PK11 type */ + PK11_SETATTRS(attrs, CKA_VALUE_LEN, &key_size, sizeof(key_size)); attrs++; } + + /* Include key id value if provided */ + if (keyid) { + PK11_SETATTRS(attrs, CKA_ID, keyid->data, keyid->len); attrs++; + } + + if (isToken) { + PK11_SETATTRS(attrs, CKA_TOKEN, &cktrue, sizeof(cktrue)); attrs++; + } + count = attrs - genTemplate; PR_ASSERT(count <= sizeof(genTemplate)/sizeof(CK_ATTRIBUTE)); /* find a slot to generate the key into */ - if ((slot == NULL) || (!PK11_DoesMechanism(slot,type))) { - slot = PK11_GetBestSlot(type,wincx); - if (slot == NULL) { + /* Only do slot management if this is not a token key */ + if (!isToken && (slot == NULL || !PK11_DoesMechanism(slot,type))) { + PK11SlotInfo *bestSlot; + + bestSlot = PK11_GetBestSlot(type,wincx); /* TNH: references the slot? */ + if (bestSlot == NULL) { PORT_SetError( SEC_ERROR_NO_MODULE ); return NULL; } + + symKey = PK11_CreateSymKey(bestSlot,type,wincx); + + PK11_FreeSlot(bestSlot); } else { - PK11_ReferenceSlot(slot); + symKey = PK11_CreateSymKey(slot, type, wincx); } + if (symKey == NULL) return NULL; + + symKey->size = keySize; + symKey->origin = (!weird) ? PK11_OriginGenerated : PK11_OriginFortezzaHack; /* Initialize the Key Gen Mechanism */ - key_gen_mechanism = PK11_GetKeyGen(type); - if (key_gen_mechanism == CKM_FAKE_RANDOM) { - PK11_FreeSlot(slot); + mechanism.mechanism = PK11_GetKeyGen(type); + if (mechanism.mechanism == CKM_FAKE_RANDOM) { PORT_SetError( SEC_ERROR_NO_MODULE ); return NULL; } - mechanism.mechanism = key_gen_mechanism; + + /* Set the parameters for the key gen if provided */ mechanism.pParameter = NULL; mechanism.ulParameterLen = 0; if (param) { @@ -1191,27 +1213,47 @@ PK11_KeyGen(PK11SlotInfo *slot, CK_MECHANISM_TYPE type, SECItem *param, mechanism.ulParameterLen = param->len; } - /* get our key Structure */ - symKey = PK11_CreateSymKey(slot,type,wincx); - PK11_FreeSlot(slot); - if (symKey == NULL) { - return NULL; + /* Get session and perform locking */ + if (isToken) { + session = PK11_GetRWSession(symKey->slot); /* Should always be original slot */ + } else { + session = symKey->session; + pk11_EnterKeyMonitor(symKey); } - symKey->size = keySize; - symKey->origin = (!weird) ? PK11_OriginGenerated : PK11_OriginFortezzaHack; - pk11_EnterKeyMonitor(symKey); - crv = PK11_GETTAB(symKey->slot)->C_GenerateKey(symKey->session, + crv = PK11_GETTAB(symKey->slot)->C_GenerateKey(session, &mechanism, genTemplate, count, &symKey->objectID); - pk11_ExitKeyMonitor(symKey); + + /* Release lock and session */ + if (isToken) { + PK11_RestoreROSession(symKey->slot, session); + } else { + pk11_ExitKeyMonitor(symKey); + } + if (crv != CKR_OK) { PK11_FreeSymKey(symKey); PORT_SetError( PK11_MapError(crv) ); return NULL; } + return symKey; } +PK11SymKey * +PK11_KeyGen(PK11SlotInfo *slot, CK_MECHANISM_TYPE type, SECItem *param, + int keySize, void *wincx) +{ + return PK11_TokenKeyGen(slot, type, param, keySize, 0, PR_FALSE, wincx); +} + +/* --- */ +PK11SymKey * +PK11_GenDES3TokenKey(PK11SlotInfo *slot, SECItem *keyid, void *cx) +{ + return PK11_TokenKeyGen(slot, CKM_DES3_CBC, 0, 0, keyid, PR_TRUE, cx); +} + /* * PKCS #11 pairwise consistency check utilized to validate key pair. */ diff --git a/security/nss/lib/softoken/pkcs11.c b/security/nss/lib/softoken/pkcs11.c index fee744351..fc70b782a 100644 --- a/security/nss/lib/softoken/pkcs11.c +++ b/security/nss/lib/softoken/pkcs11.c @@ -966,19 +966,16 @@ pk11_handlePrivateKeyObject(PK11Object *object,CK_KEY_TYPE key_type) /* forward delcare the DES formating function for handleSecretKey */ void pk11_FormatDESKey(unsigned char *key, int length); +static SECKEYLowPrivateKey *pk11_mkSecretKeyRep(PK11Object *object); -/* - * check the consistancy and initialize a Secret Key Object - */ +/* Validate secret key data, and set defaults */ static CK_RV -pk11_handleSecretKeyObject(PK11Object *object,CK_KEY_TYPE key_type, - PRBool isFIPS) +validateSecretKey(PK11Object *object, CK_KEY_TYPE key_type, PRBool isFIPS) { CK_RV crv; CK_BBOOL cktrue = CK_TRUE; CK_BBOOL ckfalse = CK_FALSE; PK11Attribute *attribute = NULL; - crv = pk11_defaultAttribute(object,CKA_SENSITIVE, isFIPS?&cktrue:&ckfalse,sizeof(CK_BBOOL)); if (crv != CKR_OK) return crv; @@ -1045,6 +1042,53 @@ pk11_handleSecretKeyObject(PK11Object *object,CK_KEY_TYPE key_type, return crv; } +/* + * check the consistancy and initialize a Secret Key Object + */ +static CK_RV +pk11_handleSecretKeyObject(PK11Object *object,CK_KEY_TYPE key_type, + PRBool isFIPS) +{ + CK_RV crv; + CK_BBOOL cktrue = CK_TRUE; + CK_BBOOL ckfalse = CK_FALSE; + PK11Attribute *attribute = NULL; + SECKEYLowPrivateKey *privKey = NULL; + SECItem pubKey; + + pubKey.data = 0; + + /* First validate and set defaults */ + crv = validateSecretKey(object, key_type, isFIPS); + if (crv != CKR_OK) goto loser; + + /* If the object is a TOKEN object, store in the database */ + if (pk11_isTrue(object,CKA_TOKEN)) { + char *label; + SECStatus rv = SECSuccess; + + privKey=pk11_mkSecretKeyRep(object); + if (privKey == NULL) return CKR_HOST_MEMORY; + + label = object->label = pk11_getString(object,CKA_LABEL); + + crv = pk11_Attribute2SecItem(NULL,&pubKey,object,CKA_ID); /* Should this be ID? */ + if (crv != CKR_OK) goto loser; + + rv = SECKEY_StoreKeyByPublicKey(SECKEY_GetDefaultKeyDB(), + privKey, &pubKey, label, + (SECKEYGetPasswordKey) pk11_givePass, object->slot); + + object->inDB = PR_TRUE; + object->handle |= (PK11_TOKEN_MAGIC | PK11_TOKEN_TYPE_PRIV); + } + +loser: + if (privKey) SECKEY_LowDestroyPrivateKey(privKey); + if (pubKey.data) PORT_Free(pubKey.data); + + return crv; +} /* * check the consistancy and initialize a Key Object @@ -1058,18 +1102,6 @@ pk11_handleKeyObject(PK11Session *session, PK11Object *object) CK_BBOOL ckfalse = CK_FALSE; CK_RV crv; - /* Only private keys can be private or token */ - if ((object->objclass != CKO_PRIVATE_KEY) && (object->objclass != CKO_PUBLIC_KEY) - && - (pk11_isTrue(object,CKA_PRIVATE) || pk11_isTrue(object,CKA_TOKEN))) { - return CKR_ATTRIBUTE_VALUE_INVALID; - } - - /* Make sure that the private key is CKA_PRIVATE if it's a token */ - if (pk11_isTrue(object,CKA_TOKEN) && (object->objclass == CKO_SECRET_KEY)) { - return CKR_ATTRIBUTE_VALUE_INVALID; - } - /* verify the required fields */ if ( !pk11_hasAttribute(object,CKA_KEY_TYPE) ) { return CKR_TEMPLATE_INCOMPLETE; @@ -2067,16 +2099,174 @@ pk11_IsWeakKey(unsigned char *key,CK_KEY_TYPE key_type) } - +/* make a fake private key representing a symmetric key */ +static SECKEYLowPrivateKey * +pk11_mkSecretKeyRep(PK11Object *object) +{ + SECKEYLowPrivateKey *privKey; + PLArenaPool *arena = 0; + CK_RV crv; + SECStatus rv; + static unsigned char derZero[1] = { 0 }; + + arena = PORT_NewArena(DER_DEFAULT_CHUNKSIZE); + if (arena == NULL) { crv = CKR_HOST_MEMORY; goto loser; } + + privKey = (SECKEYLowPrivateKey *) + PORT_ArenaAlloc(arena,sizeof(SECKEYLowPrivateKey)); + if (privKey == NULL) { crv = CKR_HOST_MEMORY; goto loser; } + + privKey->arena = arena; + + /* Secret keys are represented in the database as "fake" RSA keys. The RSA key + * is marked as a secret key representation by setting the public exponent field + * to 0, which is an invalid RSA exponent. The other fields are set as follows: + * modulus - CKA_ID value for the secret key + * private exponent - CKA_VALUE (the key itself) + * coefficient - CKA_KEY_TYPE, which indicates what encryption algorithm + * is used for the key. + * all others - set to integer 0 + */ + privKey->keyType = rsaKey; + + /* The modulus is set to the key id of the symmetric key */ + crv=pk11_Attribute2SecItem(arena,&privKey->u.rsa.modulus,object,CKA_ID); + if (crv != CKR_OK) goto loser; + + /* The public exponent is set to 0 length to indicate a special key */ + privKey->u.rsa.publicExponent.len = sizeof derZero; + privKey->u.rsa.publicExponent.data = derZero; + + /* The private exponent is the actual key value */ + crv=pk11_Attribute2SecItem(arena,&privKey->u.rsa.privateExponent,object,CKA_VALUE); + if (crv != CKR_OK) goto loser; + + /* All other fields empty - needs testing */ + privKey->u.rsa.prime1.len = sizeof derZero; + privKey->u.rsa.prime1.data = derZero; + + privKey->u.rsa.prime2.len = sizeof derZero; + privKey->u.rsa.prime2.data = derZero; + + privKey->u.rsa.exponent1.len = sizeof derZero; + privKey->u.rsa.exponent1.data = derZero; + + privKey->u.rsa.exponent2.len = sizeof derZero; + privKey->u.rsa.exponent2.data = derZero; + + /* Coeficient set to KEY_TYPE */ + crv=pk11_Attribute2SecItem(arena,&privKey->u.rsa.coefficient,object,CKA_KEY_TYPE); + if (crv != CKR_OK) goto loser; + + /* Private key version field set normally for compatibility */ + rv = DER_SetUInteger(privKey->arena, &privKey->u.rsa.version,SEC_PRIVATE_KEY_VERSION); + if (rv != SECSuccess) { crv = CKR_HOST_MEMORY; goto loser; } + +loser: + if (crv != CKR_OK) { + PORT_FreeArena(arena,PR_FALSE); + privKey = 0; + } + + return privKey; +} + +static PRBool +isSecretKey(SECKEYLowPrivateKey *privKey) +{ + if (privKey->keyType == rsaKey && privKey->u.rsa.publicExponent.len == 1 && + privKey->u.rsa.publicExponent.data[0] == 0) + return PR_TRUE; + + return PR_FALSE; +} + +/* Import a Secret Key */ +static PRBool +importSecretKey(PK11Slot *slot, SECKEYLowPrivateKey *priv) +{ + PK11Object *object; + CK_OBJECT_CLASS secretClass = CKO_SECRET_KEY; + CK_BBOOL cktrue = CK_TRUE; + CK_BBOOL ckfalse = CK_FALSE; + CK_KEY_TYPE key_type; + + /* Check for secret key representation, return if it isn't one */ + if (!isSecretKey(priv)) + return PR_FALSE; + + /* + * now lets create an object to hang the attributes off of + */ + object = pk11_NewObject(slot); /* fill in the handle later */ + if (object == NULL) { + goto loser; + } + + /* Set the ID value */ + if (pk11_AddAttributeType(object, CKA_ID, + priv->u.rsa.modulus.data, priv->u.rsa.modulus.len)) { + pk11_FreeObject(object); + goto loser; + } + + /* initalize the object attributes */ + if (pk11_AddAttributeType(object, CKA_CLASS, &secretClass, + sizeof(secretClass)) != CKR_OK) { + pk11_FreeObject(object); + goto loser; + } + + if (pk11_AddAttributeType(object, CKA_TOKEN, &cktrue, + sizeof(cktrue)) != CKR_OK) { + pk11_FreeObject(object); + goto loser; + } + + if (pk11_AddAttributeType(object, CKA_PRIVATE, &ckfalse, + sizeof(CK_BBOOL)) != CKR_OK) { + pk11_FreeObject(object); + goto loser; + } + + if (pk11_AddAttributeType(object, CKA_VALUE, + pk11_item_expand(&priv->u.rsa.privateExponent)) != CKR_OK) { + pk11_FreeObject(object); + goto loser; + } + + if (pk11_AddAttributeType(object, CKA_KEY_TYPE, + pk11_item_expand(&priv->u.rsa.coefficient)) != CKR_OK) { + pk11_FreeObject(object); + goto loser; + } + key_type = *(CK_KEY_TYPE*)priv->u.rsa.coefficient.data; + + /* Validate and add default attributes */ + validateSecretKey(object, key_type, (PRBool)(slot->slotID == FIPS_SLOT_ID)); + + /* now just verify the required date fields */ + PK11_USE_THREADS(PR_Lock(slot->objectLock);) + object->handle = slot->tokenIDCount++; + object->handle |= (PK11_TOKEN_MAGIC | PK11_TOKEN_TYPE_PRIV); + PK11_USE_THREADS(PR_Unlock(slot->objectLock);) + + object->objclass = secretClass; + object->slot = slot; + object->inDB = PR_TRUE; + pk11_AddSlotObject(slot, object); + +loser: + return PR_TRUE; +} + /********************************************************************** * * Start of PKCS 11 functions * **********************************************************************/ - - - + /* return the function list */ CK_RV NSC_GetFunctionList(CK_FUNCTION_LIST_PTR *pFunctionList) @@ -3316,6 +3506,94 @@ decodeKeyDBGlobalSalt(DBT *saltData) } #endif +#if 0 +/* + * Create a (fixed) DES3 key [ testing ] + */ +static unsigned char keyValue[] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17 +}; + +static SECItem keyItem = { + 0, + keyValue, + sizeof keyValue +}; + +static unsigned char keyID[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +static SECItem keyIDItem = { + 0, + keyID, + sizeof keyID +}; +/* +AAA */ +static CK_RV +pk11_createFixedDES3Key(PK11Slot *slot) +{ + CK_RV rv = CKR_OK; + PK11Object *keyObject; + CK_BBOOL true = CK_TRUE; + CK_OBJECT_CLASS class = CKO_SECRET_KEY; + CK_KEY_TYPE keyType = CKK_DES3; + + /* + * Create the object + */ + keyObject = pk11_NewObject(slot); /* fill in the handle later */ + if (keyObject == NULL) { + return CKR_HOST_MEMORY; + } + + /* Add attributes to the object */ + rv = pk11_AddAttributeType(keyObject, CKA_ID, keyID, sizeof keyID); + if (rv != CKR_OK) { + pk11_FreeObject(keyObject); + return rv; + } + + rv = pk11_AddAttributeType(keyObject, CKA_VALUE, keyValue, sizeof keyValue); + if (rv != CKR_OK) { + pk11_FreeObject(keyObject); + return rv; + } + + rv = pk11_AddAttributeType(keyObject, CKA_TOKEN, &true, sizeof true); + if (rv != CKR_OK) { + pk11_FreeObject(keyObject); + return rv; + } + + rv = pk11_AddAttributeType(keyObject, CKA_CLASS, &class, sizeof class); + if (rv != CKR_OK) { + pk11_FreeObject(keyObject); + return rv; + } + + rv = pk11_AddAttributeType(keyObject, CKA_KEY_TYPE, &keyType, sizeof keyType); + if (rv != CKR_OK) { + pk11_FreeObject(keyObject); + return rv; + } + + pk11_handleSecretKeyObject(keyObject, keyType, PR_TRUE); + + PK11_USE_THREADS(PR_Lock(slot->objectLock);) + keyObject->handle = slot->tokenIDCount++; + PK11_USE_THREADS(PR_Unlock(slot->objectLock);) + keyObject->slot = slot; + keyObject->objclass = CKO_SECRET_KEY; + pk11_AddSlotObject(slot, keyObject); + + return rv; +} +#endif /* Fixed DES key */ + /* * load up our token database */ @@ -3360,6 +3638,11 @@ pk11_importKeyDB(PK11Slot *slot) * pkcs11 world */ for (node = keylist.head; node != NULL; node=node->next ) { + /* Check for "special" private key that wraps a symmetric key */ + if (isSecretKey(node->privKey)) { + importSecretKey(slot, node->privKey); + goto end_loop; + } /* create the private key object */ privateKeyObject = pk11_importPrivateKey(slot, node->privKey, @@ -3410,6 +3693,7 @@ end_loop: } PORT_FreeArena(keylist.arena, PR_FALSE); + return CKR_OK; } |