최초 작성: 2023.06.12
프로젝트에 메세지를 암호화하는 작업이 필요해서 CryptoKit을 공부하게 되었다.
📌 CryptoKit 시리즈
1. 암호화 작업을 위한 Apple CryptoKit 알아보기 (현재 글)
2. Apple CryptoKit으로 채팅 메세지를 암호화할 수 있을까?
Apple CryptoKit?
암호화 작업을 안전하고 효율적으로 수행할 수 있도록 도와주는 애플 프레임워크이다.
그래서 Apple CryptoKit을 사용하면 암호화 작업을 간편하게 수행할 수 있다.
- 암호학적 해싱(Cryptographic hashing) 제공
- 공개 키 암호화: 디지털 서명(Digital Signature) 작업, 키 교환
- 대칭 키 암호화: 메시지 인증 및 데이터 암호화 작업에 사용
CryptoKit는 HMAC, AES, ChaChaPoly, P256, P384, P512 Curve25519 등의 암호화 방법을 제공한다.
- Cryptographic Hash
- SHA215
- SHA384
- SHA512
- Message Authentication Code
- HMAC
- Cipher
- AES
- ChaChaPoly
- Public Key
- P256
- P384
- P512
- Curve25519
본격적으로 CryptoKit이 제공하는 암호화 방법들을 간단히 알아보도록 하자.
크게 [Hash / Symmetric Key / Public Key] 로 크게 분류하였다.
CryptoKit이 제공하는 암호화 방식만 보면 '암호화' 자체를 이해하기 어렵다. 그래서 암호학에 관련된 내용도 슬쩍 포함해서 정리했다.
그럼 레츄고.
1. Hashing
데이터를 숫자로 표현하는 방법
자료구조를 배운 적이 있다면 해싱이 낯설지는 않을 것이다. (딕셔너리 자료형이나, key-value 구조)
해싱은 해시 알고리즘으로 임의의 값을 고정 길이(혹은 가변 길이)의 값으로 만드는 것이다. 간단하게 데이터를 숫자로 표현한다고 봐도 좋다!
해시 알고리즘은 여러 가지가 존재하고, '해시 함수에 값을 넣는다'고도 표현한다.
해싱을 key-value 구조라고 하는 것도 임의의 값으로 나온 결과 값이 서로 매핑되기 때문이다. 결과 값은 해시 값(hash value), hash code, digest, hashes 라고 부른다.
어떤 값이 들어가느냐에 따라 해시 값이 달라진다. 물론 어떤 해시 알고리즘을 쓰느냐에 따라 결과 값이 같을 수도, 다를 수도 있는데 이 내용은 너무 길어지므로 생략하도록 하겠다. 궁금하다면 자료 구조를 공부하면 더 많은 내용을 알 수 있다.
해싱, 해시 함수, 해시 알고리즘, 해시 값, digest는 나중에도 많이 보게 될 용어이니 헷갈리지 않도록 하자.
1-1. Normal Hashing
아래의 암호학적 해싱과 구분하기 위해 노말 해싱이라고 적었지만, 사실 위에서 설명한 해시의 내용과 같다.
Swift에서도 hashing을 제공하며, CryptoKit에서 제공하는 hashing 방법과 다른 메서드를 사용한다.
Swift는 Hashable 프로토콜을 제공하고, Set이나 Dictionary 타입의 데이터를 Hashing하고 싶다면 Hashable 프로토콜을 따라야 한다. String이나 Int는 Hashable 프로토콜을 따르고 있어서 문자열, 숫자 데이터는 바로 hashing이 가능하다.
/// 리터럴 값을 해싱하면 hash value이 나온다.
/// haser로 만든 hash value는 계산할 때마다 다른 값이 나온다.
func hashItem(item: String) -> Int {
var hasher = Hasher()
item.hash(into: &hasher)
return hasher.finalize()
}
let hashvalue = hashItem(item: "brown fox")
1-2. Cyptrograhpic Hashing (SHA)
암호학적 hashing
CryptoKit이 제공하는 Cryptographic hashing은 SHA-2(Secure Hash Algorithm 2) 방식을 지원한다.. digest의 크기에 따라 3가지로 나뉜다. 예를 들어 SHA256의 digest는 256 비트를 가진다.
- SHA256
- SHA384
- SHA512
digest로 손상 여부를 확인할 수 있다. 즉, 데이터 무결성을 확인할 수 있다.
import CryptoKit
/// 이미지 데이터를 받아온다.
func getData(for item: String, of type: String) -> Data {
let filePath = Bundle.main.path(forResource: item, ofType: type)!
return FileManager.default.contents(atPath: filePath)!
}
let data = getData(for: "Baby", of: "png")
UIImage(data: data)
/// 256-bit의 digest를 만든다.
let digest = SHA256.hash(data: data)
/// 송신자는 'data'와 'digest'를 수신자에게 보낸다.
/// 수신자는 'data'를 cryptographic hashing한 값과 'digest'를 비교한다. 같다면 데이터는 무결한 상태이다.
let receivedDataDigest = SHA256.hash(data: data)
if digest == receivedDataDigest {
print("보낸 data == 받은 data")
}
- 데이터 송신자는 'data'와 'Cryptographic hashing의 결과 값 digest'와 같이 실어 보낸다.
- 데이터 수신자는 'data'를 'Cryptographic hashing한 값과 전달받은 digest'를 비교한다. 값이 똑같으면 데이터에 손상 없고, 그렇지 않으면 손상이 있는 것이다.
2. Symmetric Key Cryptography
송, 수신자가 서로 동일한 비밀키를 공유하고, 이 키로 데이터를 암호화/복호화하는 방법
Symmetric Key는 수신자, 송신자가 둘 다 알고 있는 비밀키이다.
이 키로 데이터를 암호화하고 복호화할 수 있다. 아래와 같은 장단점을 가진다.
- 단점
- 송-수신자 모두가 비밀 키에 접근할 수 있다. (공개 키 암호화에 비해 단점)
- 장점
- 적은 저장 공간 사용
- 빠른 전송
대칭키 암호화 알고리즘은
- AES
- Salsa20
- ChaCha20
- Blowfish
- CAST5
- RC4
- DES
- 3DES
등등이 있다. 아주 많으니 따로 찾아봐도 좋다.
이 중에서 AES, ChaCha20 알고리즘이 CryptoKit에서 제공된다.
2-1. Hash-based Message Authenticate Code (HMAC)
MAC에 cryptographic hashing과 비밀키가 포함된 방식
HMAC을 사용하면 데이터 무결성과 권한을 확인할 수 있다.
- cryptographic hashing으로 digest를 만든다. 이 digest를 대칭키로 암호화하여 사용하여 송신자에게서 온 것임을 알 수 있도록 한다.
- 혹은 서버가 파일을 업로드할 권한이 있는지 확인할 수 있다.
- 데이터 무결성은 데이터를 해싱한 값과 digest를 비교하면 된다.
hasing data는 데이터를 암호화한 것이 아니다. 그래서 중간에 데이터를 탈취하여 데이터를 변경해서 보내는 등의 악의적인 변경을 막을 수 없다.
예를 들어 송신자가 수신자에게 [digest+data]를 보냈는데, 중간에 해커가 데이터를 탈취하였다고 가정하자.
해커는 데이터를 임의로 변경하고, digest도 변경된 데이터의 digest로 바꿔서 수신자에게 보낸다. 수신자는 이 데이터가 변경된 것인지 알 방법이 없다.
그래서 송신하는 데이터에 '서명'을 남기는 방법이 생겨났다. 대칭키를 사용하여 데이터에 서명(sign)을 하고, 데이터의 출처를 표기할 수 있다.
대칭키를 사용하는 HMAC 방법은 이러한 악의적인 변경에서 데이터를 보호할 수 있다.
HMAC은 digest에 서명을 남기는 방식이다.
CryptoKit에서 대칭키와 HMAC 알고리즘을 지원한다. 코드를 살펴보도록 하자!
import CryptoKit
let data = getData(for: "Baby", of: "png")
UIImage(data: data)
/// HMAC의 일반적인 공격은 무차별 입력(brute force)이므로, bits 수가 클수록 방어력이 올라간다.
/// 대칭키가 256비트라면, 2^256의 가짓수를 다 봐야한다.
let key256 = SymmetricKey(size: .bits256)
/// Baby 이미지에 대한 512 비트의 HMAC 서명을 만들었다.
/// digest는 인증코드(Message Authentication Code) 혹은 서명(Signature)으로 불릴 수 있다.
let sha512MAC = HMAC<SHA512>.authenticationCode(
for: data,
using: key256
)
/// 네트워크로 HMAC 서명을 보내기 위해서는 Data 타입으로 바꿔줘야 한다.
let authenticationData = Data(sha512MAC)
/// 송신자가 'data'와 'signature'를 수신자에게 보냈다고 가정하자!
/// 수신자는 'signature'를 아래의 `isValidAuthenticationCode`메서드로 확인할 수 있다.
if HMAC<SHA512>.isValidAuthenticationCode(
authenticationData,
authenticating: data,
using: key256
){
print("The message authentication code is validating the data: \(data))")
UIImage(data: data)
} else {
print("not valid")
}
HMAC의 동작 방법은 아래와 같다.
HMAC은 내부적으로 2번의 해싱을 한다. 이때 사용되는 키는 각각 내부키(inner key)와 외부키(outer key)이다.
- 대칭키로 내부키, 외부키를 도출한다.
- [데이터 + 내부키]로 내부 해시(internal hash)를 만들어낸다.
- [내부해시 + 외부키]로 서명(signature)을 만들어낸다.
📌 정리
HMAC은 데이터 무결성과 수신자 신원을 보장한다.
HMAC은 Key가 있는 사용자만 MAC을 생성할 수 있다. 그래서 송신 중간에 변경된 데이터의 해쉬값이 와도 수신자는 이 데이터의 무결함을 key 값으로 확인할 수 있다. 데이터가 변경되었음을 확인할 수 있다.
그러나 데이터를 암호화한 것은 아니라는 것을 명심하자.
Hasing은 암호화가 아니라 무결성을 확인하는 기술이다.
2-2. Encrypting and Authenticating Data (AEAS: AES, ChaChaPoly)
SealedBox 암호화
네트워크로 데이터를 안전하게 보낸다 하더라도, 암호화된 파일을 사용자에게 보내야 하는 경우가 있다.
예를 들면, 인앱 결제를 예시로 들 수 있다. 앱을 결제한 사용자에게 암호화된 파일과 복호화 키를 같이 보내줄 것이다.
그래서 데이터 무결성뿐만 아니라 데이터의 암호화/복호화도 필요해지게 되었다.
AEAD 암호화 알고리즘은 Authenticated Encryption with Associated Data의 약자로 CryptoKit이 제공한다.
- (1)데이터의 암호화/복호화와 (2)데이터 무결성도 보장한다.
- 비밀키와 MAC을 사용한다.
- AEAD에서 사용하는 MAC은 일반 텍스트가 아니라 암호화된 텍스트를 hashing 한다.
CryptoKit이 제공하는 AEAD 방식은 2가지이다.
- AES.GCM (AES-GCM)
- ChaChaPoly (ChaCha20-Poly1305)
AEAD 방식은 4가지의 입력값을 가진다.
- 암호화될 일반 텍스트
- 비밀키
- 고유한 초기값 (initial value: IV, nonce)
- [옵셔널] 인증되었지만 암호화되지 않은 데이터 (AD에 해당)
암호화하는 과정(seal)은 다음의 순서와 같다.
- 비밀키와 nonce를 사용하여 보조 키를 생성합니다.
- 비밀키와 nonce를 사용하여 데이터를 암호화합니다.
- 보조 키를 사용하여 추가 데이터, 암호화된 데이터 및 각각의 길이의 키 digest를 생성합니다.
- 비밀키와 nonce를 사용하여 키 digest를 암호화한 다음, 암호화된 키 digest를 암호화된 데이터에 추가합니다.
코드를 보면서 직접 사용해보자!
import CryptoKit
func getData(for item: String, of type: String) -> Data {
let filePath = Bundle.main.path(forResource: item, ofType: type)!
return FileManager.default.contents(atPath: filePath)!
}
let data = getData(for: "Baby", of: "png")
UIImage(data: data)
/// 대칭키
let key256 = SymmetricKey(size: .bits256)
/// 'data'를 봉인(seal) 한다. === 'data'를 암호화 한다.
/// 이때 대칭키를 사용한다.
let sealedBoxData = try! ChaChaPoly.seal(data, using: key256).combined
let sealedBoxData2 = try! AES.GCM.seal(data, using: key256).combined
/// 봉인한(암호화한) 데이터를 네트워크로 보내기 위해서는 Data 타입이어야 한다.
/// SealedBox를 타입으로 변환한다.
/// 택배 상자로 봉인한 것과 같다!
let sealedBox = try! ChaChaPoly.SealedBox(combined: sealedBoxData)
let sealedBox2 = try! AES.GCM.SealedBox(combined: sealedBoxData2!) // 옵셔녈 값 풀어주기
/// 암호화에 사용했던 대칭키로 복호화를 할 수 있다.
/// 'sealedBox'를 개봉(open) 한다. == 암호화된 데이터를 복호화 한다.
let decryptedData = try! ChaChaPoly.open(sealedBox, using: key256)
/// 박스 안에 있는 내용물에서 nonce, tag를 확인할 수 있다.
/// initalizzation value, IV or nonce
sealedBox.nonce // 12 bytes
/// secondary key
sealedBox.tag // 16 bytes
/// 암호화한 데이터는 보이지 않는다. nil이 반환된다.
let encryptedData = sealedBox.ciphertext
UIImage(data: encryptedData)
/// 대칭키로 복호화한 데이터가 잘 출력되는 모습을 볼 수 있다.
UIImage(data: decryptedData)
AES와 ChaCha20은 암호 알고리즘이고, GCM과 Poly1305는 MAC 알고리즘이다.
Q. 어라? 왜 2가지 알고리즘을 합쳐서 사용하나요?
- AE: 인증된 암호화로, 데이터 암호화를 의미한다.
- AD: 관련 데이터로, 데이터 무결성을 포함한다.
그러므로 데이터 암호화와 데이터 무결성의 기능을 한번에 가지고 있는 알고리즘이 AEAD라고 할 수 있다.
따라서 [AES-CGM], [ChaCha20-Poly2305]로 불리는 것이다.
AES, ChaChaPoly를 SealedBox 암호화라고 부른다.
3. Public Key Cryptography (Curve25512)
Public Key와 Private Key 2가지를 만든다.
Public Key는 공개되는 Key이며, Private Key는 자신만 알고 있는 key이다.
수신자, 송신자 모두 각각의 Public, Private Key를 가진다.
HMAC와 Sealed Box 암호화는 송신자와 수신자가 모두 알고 있는 대칭 키를 사용한다.
안전하게 대칭키를 보낼 수 없다면, 공개 키 암호화를 사용하자.
공개 키 암호화는 개인 키와 공개 키 두 개를 만들어 사용한다. 공개 키로 데이터에 서명하는 방법이다.
- 개인 키는 비밀로 유지하고, 공개 키는 게시한다.
- 개인 키로 데이터나 digest에 서명(sign)하여 송신한다.
- 수신자는 공개 키 데이터에서 공개 키를 만든 다음, 이를 사용하여 서명된 데이터나 digest를 확인한다.
예를 들어, 앱(client)이 서버로 요청을 보낼 때, 서버에서 인증을 요구할 수도 있다. (로그인이나 신원 확인이 필요할 때 혹은 그 외)
개인 키를 기기의 KeyChain 또는 SecureEnclave에 저장하고 공개 키를 서버에 등록한다.
사용자가 관련된 작업을 하면 앱은 사용자의 개인 키로 작업의 세부 사항에 서명하고, 서명된 세부 사항을 서버로 전송하여 사용자의 공개 키로 확인한다.
공개 키 암호화로 대칭 키를 만들어 암호화된 데이터를 보낼 수도 있다.
- 송신자와 수신자 둘 다 각각 한 쌍의 키(Public + Private)를 만들고 공개 키를 게시한다.
- 둘 다 개인 키를 다른 사람의 공개 키와 결합하여 shared secret을 만든다.
- 송신자 Public + 수신자 Private
- 수신자 Public + 송신자 Private
- 둘 다 이 shared secret을 사용하여 동일한 대칭 키를 도출한다.
- 대칭 키 챕터에서 말한 AEAD 방식을 사용한다.
공개 키 암호화는 트랩도어 알고리즘을 사용한다. 그래서 공개 키에서 개인 키를 계산하는 것은 매우 어렵다.
아주 아주, 매우 매우 큰 숫자를 두 소수의 곱으로 표현하기 어렵다는 점을 생각하면 된다.
이미 알고 있는 아주 큰 두 소수의 곱으로 아주 큰 수를 구할 수는 있어도, 그의 역은 굉장히 힘들다.
이 논리가 공개키 암호화에서 사용된다.
Public Key 알고리즘은 RSA, ECC 등이 있다.
- RSA
- ECC
CryptoKit은 ECC 알고리즘을 독점적으로 사용한다. ECC 알고리즘의 원리를 살펴보면 좌표 평면에 그려지는 '곡선'을 활용한다.
어떤 곡선을 쓰느냐에 따라 다르게 쓰인다. 아래는 곡선의 종류이다.
- P256 (OpenSSL에서 기본으로 사용되는 곡선)
- P384
- P521
- Curve25519
3-1. Digital Signing
Public, Private Key로 Data의 출처를 밝힐 수 있다. 이를 Digital Signature라고 한다. (디지털 서명)
대칭 키 챕터에서 디지털 서명의 방식으로 HMAC를 소개했다. 그리고 공개 키 암호화로도 디지털 서명을 만들 수 있다.
송신자는 개인 키를 사용하여 서명하고, 수신자는 송신자의 공개 키를 사용하여 서명을 확인한다. 서로 같은 키(대칭키)를 사용하지 않아도 디지털 서명을 만들 수 있다!
다음은 공개 키 암호화의 디지털 서명 알고리즘들이다.
- ECDSA: P256, P385, P521 곡선 사용
- EdDSA: Curve25519 곡선 사용
CryptoKit는 앞서 설명한 4개의 곡선을 모두 제공하고 있다.
CryptoKit의 디지털 서명 알고리즘은 곡선명.Signing.- 으로 사용할 수 있다.
import CryptoKit
func getData(for item: String, of type: String) -> Data {
let filePath = Bundle.main.path(forResource: item, ofType: type)!
return FileManager.default.contents(atPath: filePath)!
}
let data = getData(for: "Baby", of: "png")
UIImage(data: data)
/// 송신자가 수신자에게 사진 이미지를 보내려고 한다.
/// 송신자의 Public, Private Key 쌍을 만든다.
let senderSigningPrivateKey = Curve25519.Signing.PrivateKey()
let senderSigningPublicKeyData = senderSigningPrivateKey.publicKey.rawRepresentation
/// 송신자는 자신의 Public Key, 즉 `senderSigningPublicKeyData`를 게시한다.
/// 송신자는 자신의 Private Key로 'data'에 서명한다.
let signatureForData = try! senderSigningPrivateKey.signature(for: data)
// Signing a digest of the data is faster:
/// 데이터를 hasing한 'digest'에 서명하는 방법도 있다.
let digest512 = SHA512.hash(data: data)
let signatureForDigest = try! senderSigningPrivateKey.signature(for: Data(digest512))
/// 송신자는 'senderSigningPublicKeyData', 'data', 'digest512', 'signatureForData'를 수신자에게 보낸다.
/// 'signatureForData'대신 'signatureForDigest'을 보낼 수도 있다.
/// 수신자는 상대방의 공개 키로 서명이 올바른지 확인한다. (데이터 무결성 확인)
let publickey = try! Curve25519.Signing.PublicKey(rawRepresentation: senderSigningPublicKeyData)
if publickey.isValidSignature(signatureForData, for: data) {
print("Sender sent this data.")
UIImage(data: data)
}
if publickey.isValidSignature(signatureForDigest, for: Data(digest512)) {
print("보낸 데이터 == 받은 데이터")
UIImage(data: data)
}
3-2. Shared secret
Public, Private key로 shared secret을 만들어 symmetric key를 안전하게 공유할 수 있다.
이렇게 만든 symmetric key로 데이터를 암호화/복호화 할 수 있다.
공개 키 암호화를 사용하여 디지털 서명뿐만 아니라 데이터 암호화도 가능하다.
공개 키와 개인 키로 송/수신자들만 알 수 있는 shared secret을 만들어서 대칭 키를 만든다. 이 대칭 키로 데이터 암호화/복호화를 하는 것이다!! 아주 쉽다.
- [본인 개인키 + 상대방 공개키 + salt value + hasing]으로 shared secret을 만든다.
- shared secret으로 대칭키를 만들고, 데이터를 암호화한다.
- 상대방에게 [암호화한 데이터 + salt value]를 보내준다. (공개키는 이미 공개되어 서로 알고 있다)
- 수신자는 암호화한 데이터를 풀기 위해 [본인 개인키 + 상대방 공개키 + salt value + hasing]으로 shared secret을 만들고, 이로써 대칭키까지 알아낼 수 있다.
- 알아낸 대칭키로 데이터를 복호화한다. 끗.
CryptoKit에서 Shared secret을 공유하기 위해서는 곡선.KeyAgreement.- 을 사용하면 된다.
(서로 키에 대한 합의가 이루어졌다고 해서 KeyAgreement 이라고 한다. 자세한 것은 여기로)
import CryptoKit
func getData(for item: String, of type: String) -> Data {
let filePath = Bundle.main.path(forResource: item, ofType: type)!
return FileManager.default.contents(atPath: filePath)!
}
let data = getData(for: "Baby", of: "png")
UIImage(data: data)
/// 송신자와 수신자가 서로 키 합의(KeyAgreemen)로 공개 키, 개인 키를 만든다.
/// 서로의 공개 키는 공개한다. (게시한다)
let senderPrivateKey = Curve25519.KeyAgreement.PrivateKey()
let senderPublicKeyData = senderPrivateKey.publicKey.rawRepresentation
let receiverPrivateKey = Curve25519.KeyAgreement.PrivateKey()
let receiverPublicKeyData = receiverPrivateKey.publicKey.rawRepresentation
/// 대칭 키를 만들 때 필요한 무작위 값(salt value)을 준비한다.
/// 이 값은 나와 상대방 둘 다 똑같이 알고 있어야 한다.
/// salt value를 탈취 당해도 개인 키가 없으면 대칭 키를 알 수 없다.
let protocolSalt = "일급 비밀 정보입니다.".data(using: .utf8)!
/// 송신자는 자신의 개인 키와 수신자의 공개 키로 'sharedSecret'과 'symmetricKey'를 생성한다.
let receiverPublicKey = try! Curve25519.KeyAgreement.PublicKey(rawRepresentation: receiverPublicKeyData)
let SEsharedSecret = try! senderPrivateKey.sharedSecretFromKeyAgreement(with: receiverPublicKey)
let SEsymmetricKey = SEsharedSecret.hkdfDerivedSymmetricKey(
using: SHA256.self,
salt: protocolSalt,
sharedInfo: Data(),
outputByteCount: 32
)
/// 수신자는 자신의 개인 키와 송신자의 공개 키로 'sharedSecret'과 'symmetricKey'를 생성한다.
let senderPublicKey = try! Curve25519.KeyAgreement.PublicKey(rawRepresentation: senderPublicKeyData)
let REsharedSecret = try! senderPrivateKey.sharedSecretFromKeyAgreement(with: senderPublicKey)
let REsymmetricKey = REsharedSecret.hkdfDerivedSymmetricKey(
using: SHA256.self,
salt: protocolSalt,
sharedInfo: Data(),
outputByteCount: 32
)
/// 송신자의 대칭 키와 수신자의 대칭 키가 동일한지 확인해보자.
if SEsymmetricKey == REsymmetricKey {
print("송신자와 수신자의 대칭키가 서로 일치합니다.")
}
짠!! 이제 CryptoKit을 이해하기 한결 수월해졌다.
메세지나 중요한 정보를 저장해야하거나, 암호화된 인증이 필요할 때 CryptoKit의 도움을 받아보도록 하자!
reference
Apple Developer Documentation
Wikipedia
- Hash function
- Cryptographic hash function
- SHA-2
- Symmetric-key algorithms
- HMAC
- Authenticated encryption
- Public-key cryptography
- EdDSA
Article
'공부 > iOS' 카테고리의 다른 글
Apple CryptoKit으로 채팅 메세지를 암호화할 수 있을까? (0) | 2023.06.14 |
---|---|
[SwiftUI] Firebase의 GitHub 로그인으로 Auto Login 구현하기 (0) | 2023.06.12 |
[SwiftUI] 네? iOS15에는 sheet의 크기를 조절할 수 없다구요? 그럼 직접 만들지 뭐... (2) | 2023.05.07 |
[SwiftUI] SwiftUI에서 무한스크롤(Infinite Scroll) 구현하기 (0) | 2023.05.07 |