Tmp fixes

This commit is contained in:
Pascal Engélibert 2024-10-09 11:22:08 +02:00
parent 84b358c934
commit df1b2129c1
12 changed files with 41 additions and 130 deletions

View file

@ -1,2 +0,0 @@
ignore:
- "../*"

View file

@ -1,11 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "cargo" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"

View file

@ -1,15 +0,0 @@
name: Security audit
on:
push:
branches:
- "*"
schedule:
- cron: '0 0 * * *'
jobs:
security_audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/audit-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,60 +0,0 @@
name: Rust
on:
push
env:
CARGO_TERM_COLOR: always
jobs:
build-and-test-native:
strategy:
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
# `cargo check` command here will use installed `nightly`
# as it is set as an "override" for current directory
- name: Run cargo check
run: cargo check
- name: Run cargo test
run: cargo test
- name: Run cargo build
run: cargo build
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Actions-rs
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
- name: Run Test
uses: actions-rs/cargo@v1
with:
command: test
args: --all-features --no-fail-fast
env:
CARGO_INCREMENTAL: '0'
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests'
RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests'
- id: coverage
uses: actions-rs/grcov@v0.1
- name: Coveralls upload
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: ${{ steps.coverage.outputs.report }}

View file

@ -1,20 +1,20 @@
[package] [package]
name = "double-ratchet-rs" name = "double-ratchet-rs"
version = "0.4.6" version = "0.4.6"
authors = ["satvrn", "Hannes Furmans"] authors = ["satvrn", "Hannes Furmans", "Pascal Engélibert"]
edition = "2021" edition = "2021"
rust-version = "1.60" rust-version = "1.60"
description = "A pure Rust implementation of the Double Ratchet algorithm as described by Signal." description = "A pure Rust implementation of the Double Ratchet algorithm as described by Signal."
documentation = "https://docs.rs/double-ratchet-rs" documentation = "https://docs.rs/double-ratchet-rs"
readme = "README.md" readme = "README.md"
homepage = "https://github.com/notsatvrn/double-ratchet-rs" homepage = "https://git.txmn.tk/tuxmain/double-ratchet-rs"
repository = "https://github.com/notsatvrn/double-ratchet-rs" repository = "https://git.txmn.tk/tuxmain/double-ratchet-rs"
license = "MIT" license = "MIT"
keywords = ["double-ratchet", "signal"] keywords = ["double-ratchet", "signal"]
categories = ["algorithms", "cryptography", "no-std"] categories = ["algorithms", "cryptography", "no-std"]
[dependencies] [dependencies]
x25519-dalek = {version = "2.0.0-rc.3", default-features = false, features = ["serde", "static_secrets", "zeroize"]} x25519-dalek = {version = "2", default-features = false, features = ["serde", "static_secrets", "zeroize"]}
rand_core = "0.6" rand_core = "0.6"
hkdf = "0.12" hkdf = "0.12"
hmac = "0.12" hmac = "0.12"

View file

@ -2,6 +2,7 @@ MIT License
Copyright (c) 2023 satvrn Copyright (c) 2023 satvrn
Copyright (c) 2021 Hannes Furmans Copyright (c) 2021 Hannes Furmans
Copyright (c) 2024 Pascal Engélibert
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -1,8 +1,3 @@
[![Crate](https://img.shields.io/crates/v/double-ratchet-rs)](https://crates.io/crates/double-ratchet-rs)
[![License](https://img.shields.io/github/license/notsatvrn/double-ratchet-rs)](https://github.com/notsatvrn/double-ratchet-rs/blob/main/LICENSE)
[![Coverage Status](https://coveralls.io/repos/github/notsatvrn/double-ratchet-rs/badge.svg?branch=main)](https://coveralls.io/github/notsatvrn/double-ratchet-rs?branch=main)
[![Workflow Status](https://github.com/notsatvrn/double-ratchet-rs/actions/workflows/rust.yml/badge.svg)](https://github.com/notsatvrn/double-ratchet-rs/actions/workflows/rust.yml)
# double-ratchet-rs # double-ratchet-rs
A pure Rust implementation of the Double Ratchet algorithm as described by [Signal][1]. A pure Rust implementation of the Double Ratchet algorithm as described by [Signal][1].
@ -10,7 +5,7 @@ A pure Rust implementation of the Double Ratchet algorithm as described by [Sign
This implementation follows the cryptographic recommendations provided by [Signal][2]. This implementation follows the cryptographic recommendations provided by [Signal][2].
The AEAD algorithm uses a constant Nonce. This might be changed in the future. The AEAD algorithm uses a constant Nonce. This might be changed in the future.
Fork of [double-ratchet-2](https://github.com/Dione-Software/double-ratchet-2). Temporary fork of [double-ratchet-rs](https://github.com/notsatvrn/double-ratchet-rs), which is published on crates.io. Use this one instead because my fork is published for a proof of concept only, not meant to stay forever.
## Examples ## Examples
@ -174,7 +169,7 @@ The current MSRV is 1.60.0 without `hashbrown` and 1.64.0 with `hashbrown`.
## License ## License
This project is licensed under the [MIT license](https://github.com/notsatvrn/double-ratchet-rs/blob/main/LICENSE). This project is licensed under the [MIT license](https://git.txmn.tk/tuxmain/double-ratchet-rs/blob/main/LICENSE).
[1]: https://signal.org/docs/specifications/doubleratchet/ [1]: https://signal.org/docs/specifications/doubleratchet/
[2]: https://signal.org/docs/specifications/doubleratchet/#recommended-cryptographic-algorithms [2]: https://signal.org/docs/specifications/doubleratchet/#recommended-cryptographic-algorithms

View file

@ -35,9 +35,9 @@ fn ratchet_encrypt_decrypt_four() {
let (mut bob_ratchet, public_key) = Ratchet::init_bob(sk); let (mut bob_ratchet, public_key) = Ratchet::init_bob(sk);
let mut alice_ratchet = Ratchet::init_alice(sk, public_key); let mut alice_ratchet = Ratchet::init_alice(sk, public_key);
let (header1, encrypted1, nonce1) = alice_ratchet.encrypt(&data, b""); let (header1, encrypted1, nonce1) = alice_ratchet.encrypt(&data, b"");
let _decrypted1 = bob_ratchet.decrypt(&header1, &encrypted1, &nonce1, b""); let _decrypted1 = bob_ratchet.decrypt(&header1, &encrypted1, &nonce1, b"").unwrap();
let (header2, encrypted2, nonce2) = bob_ratchet.encrypt(&data, b""); let (header2, encrypted2, nonce2) = bob_ratchet.encrypt(&data, b"");
let _decrypted2 = alice_ratchet.decrypt(&header2, &encrypted2, &nonce2, b""); let _decrypted2 = alice_ratchet.decrypt(&header2, &encrypted2, &nonce2, b"").unwrap();
} }
fn criterion_benchmark_3(c: &mut Criterion) { fn criterion_benchmark_3(c: &mut Criterion) {
@ -54,7 +54,7 @@ fn ratchet_ench_enc_single() {
let mut alice_ratchet = RatchetEncHeader::init_alice(sk, public_key, shared_hka, shared_nhkb); let mut alice_ratchet = RatchetEncHeader::init_alice(sk, public_key, shared_hka, shared_nhkb);
let data = include_bytes!("../src/header.rs").to_vec(); let data = include_bytes!("../src/header.rs").to_vec();
let (header, encrypted, nonce) = alice_ratchet.encrypt(&data, b""); let (header, encrypted, nonce) = alice_ratchet.encrypt(&data, b"");
let decrypted = bob_ratchet.decrypt(&header, &encrypted, &nonce, b""); let decrypted = bob_ratchet.decrypt(&header, &encrypted, &nonce, b"").unwrap();
assert_eq!(data, decrypted) assert_eq!(data, decrypted)
} }
@ -91,9 +91,9 @@ fn ratchet_ench_decrypt_four() {
let mut alice_ratchet = RatchetEncHeader::init_alice(sk, public_key, shared_hka, shared_nhkb); let mut alice_ratchet = RatchetEncHeader::init_alice(sk, public_key, shared_hka, shared_nhkb);
let data = include_bytes!("../src/dh.rs").to_vec(); let data = include_bytes!("../src/dh.rs").to_vec();
let (header1, encrypted1, nonce1) = alice_ratchet.encrypt(&data, b""); let (header1, encrypted1, nonce1) = alice_ratchet.encrypt(&data, b"");
let _decrypted1 = bob_ratchet.decrypt(&header1, &encrypted1, &nonce1, b""); let _decrypted1 = bob_ratchet.decrypt(&header1, &encrypted1, &nonce1, b"").unwrap();
let (header2, encrypted2, nonce2) = bob_ratchet.encrypt(&data, b""); let (header2, encrypted2, nonce2) = bob_ratchet.encrypt(&data, b"");
let _decrypted2 = alice_ratchet.decrypt(&header2, &encrypted2, &nonce2, b""); let _decrypted2 = alice_ratchet.decrypt(&header2, &encrypted2, &nonce2, b"").unwrap();
} }
fn criterion_benchmark_6(c: &mut Criterion) { fn criterion_benchmark_6(c: &mut Criterion) {

View file

@ -22,13 +22,16 @@ pub fn encrypt(mk: &[u8; 32], data: &[u8], associated_data: &[u8]) -> (Vec<u8>,
(buffer, nonce_data) (buffer, nonce_data)
} }
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InvalidAd;
pub fn decrypt( pub fn decrypt(
mk: &[u8; 32], mk: &[u8; 32],
enc_data: &[u8], enc_data: &[u8],
associated_data: &[u8], associated_data: &[u8],
nonce: &[u8; 12], nonce: &[u8; 12],
) -> Vec<u8> { ) -> Result<Vec<u8>, InvalidAd> {
let cipher = Aes256GcmSiv::new_from_slice(mk).expect("Decryption failure {}"); let cipher = Aes256GcmSiv::new_from_slice(mk).expect("unreachable");
let nonce = Nonce::from_slice(nonce); let nonce = Nonce::from_slice(nonce);
@ -37,9 +40,9 @@ pub fn decrypt(
cipher cipher
.decrypt_in_place(nonce, associated_data, &mut buffer) .decrypt_in_place(nonce, associated_data, &mut buffer)
.expect("Decryption failure {}"); .map_err(|_| InvalidAd)?;
buffer Ok(buffer)
} }
#[cfg(test)] #[cfg(test)]
@ -53,7 +56,7 @@ mod tests {
let associated_data = include_bytes!("lib.rs"); let associated_data = include_bytes!("lib.rs");
let mk = gen_mk(); let mk = gen_mk();
let (enc_data, nonce) = encrypt(&mk, test_data, associated_data); let (enc_data, nonce) = encrypt(&mk, test_data, associated_data);
let data = decrypt(&mk, &enc_data, associated_data, &nonce); let data = decrypt(&mk, &enc_data, associated_data, &nonce).unwrap();
assert_eq!(test_data, data.as_slice()) assert_eq!(test_data, data.as_slice())
} }
} }

View file

@ -14,7 +14,7 @@ use alloc::vec::Vec;
#[derive(Serialize, Deserialize, Debug, Zeroize, Clone, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, Zeroize, Clone, PartialEq, Eq)]
#[zeroize(drop)] #[zeroize(drop)]
pub struct Header { pub struct Header {
ad: Vec<u8>, pub ad: Vec<u8>,
pub public_key: PublicKey, pub public_key: PublicKey,
pub pn: usize, pub pn: usize,
pub n: usize, pub n: usize,
@ -122,7 +122,7 @@ mod tests {
let header_data = header.concat(b""); let header_data = header.concat(b"");
let data = include_bytes!("aead.rs"); let data = include_bytes!("aead.rs");
let (encrypted, nonce) = encrypt(&mk, data, &header_data); let (encrypted, nonce) = encrypt(&mk, data, &header_data);
let decrypted = decrypt(&mk, &encrypted, &header_data, &nonce); let decrypted = decrypt(&mk, &encrypted, &header_data, &nonce).unwrap();
assert_eq!(decrypted, data.to_vec()) assert_eq!(decrypted, data.to_vec())
} }

View file

@ -1,6 +1,6 @@
//! Ratchet providing encryption and decryption. //! Ratchet providing encryption and decryption.
use crate::aead::{decrypt, encrypt}; use crate::aead::{decrypt, encrypt, InvalidAd};
use crate::dh::DhKeyPair; use crate::dh::DhKeyPair;
use crate::header::{EncryptedHeader, Header}; use crate::header::{EncryptedHeader, Header};
use crate::kdf_chain::kdf_ck; use crate::kdf_chain::kdf_ck;
@ -108,7 +108,7 @@ impl Ratchet {
enc_data: &[u8], enc_data: &[u8],
nonce: &[u8; 12], nonce: &[u8; 12],
associated_data: &[u8], associated_data: &[u8],
) -> Option<Vec<u8>> { ) -> Option<Result<Vec<u8>, InvalidAd>> {
if self if self
.mkskipped .mkskipped
.contains_key(&(header.public_key.to_bytes(), header.n)) .contains_key(&(header.public_key.to_bytes(), header.n))
@ -160,7 +160,7 @@ impl Ratchet {
enc_data: &[u8], enc_data: &[u8],
nonce: &[u8; 12], nonce: &[u8; 12],
associated_data: &[u8], associated_data: &[u8],
) -> Vec<u8> { ) -> Result<Vec<u8>, InvalidAd> {
let data = self.try_skipped_message_keys(header, enc_data, nonce, associated_data); let data = self.try_skipped_message_keys(header, enc_data, nonce, associated_data);
match data { match data {
Some(d) => d, Some(d) => d,
@ -328,7 +328,7 @@ impl RatchetEncHeader {
enc_data: &[u8], enc_data: &[u8],
nonce: &[u8; 12], nonce: &[u8; 12],
associated_data: &[u8], associated_data: &[u8],
) -> (Option<Vec<u8>>, Option<Header>) { ) -> (Option<Result<Vec<u8>, InvalidAd>>, Option<Header>) {
let ret_data = self.mkskipped.clone().into_iter().find(|e| { let ret_data = self.mkskipped.clone().into_iter().find(|e| {
let header = enc_header.decrypt(&e.0 .0); let header = enc_header.decrypt(&e.0 .0);
match header { match header {
@ -412,7 +412,7 @@ impl RatchetEncHeader {
enc_data: &[u8], enc_data: &[u8],
nonce: &[u8; 12], nonce: &[u8; 12],
associated_data: &[u8], associated_data: &[u8],
) -> Vec<u8> { ) -> Result<Vec<u8>, InvalidAd> {
let (data, _) = self.try_skipped_message_keys(enc_header, enc_data, nonce, associated_data); let (data, _) = self.try_skipped_message_keys(enc_header, enc_data, nonce, associated_data);
if let Some(d) = data { if let Some(d) = data {
return d; return d;
@ -438,7 +438,7 @@ impl RatchetEncHeader {
enc_data: &[u8], enc_data: &[u8],
nonce: &[u8; 12], nonce: &[u8; 12],
associated_data: &[u8], associated_data: &[u8],
) -> (Vec<u8>, Header) { ) -> (Result<Vec<u8>, InvalidAd>, Header) {
let (data, header) = let (data, header) =
self.try_skipped_message_keys(enc_header, enc_data, nonce, associated_data); self.try_skipped_message_keys(enc_header, enc_data, nonce, associated_data);
if let Some(d) = data { if let Some(d) = data {

View file

@ -20,7 +20,7 @@ fn enc_single() {
let mut alice_ratchet = Ratchet::init_alice(sk, public_key); let mut alice_ratchet = Ratchet::init_alice(sk, public_key);
let data = include_bytes!("../src/header.rs").to_vec(); let data = include_bytes!("../src/header.rs").to_vec();
let (header, encrypted, nonce) = alice_ratchet.encrypt(&data, b""); let (header, encrypted, nonce) = alice_ratchet.encrypt(&data, b"");
let decrypted = bob_ratchet.decrypt(&header, &encrypted, &nonce, b""); let decrypted = bob_ratchet.decrypt(&header, &encrypted, &nonce, b"").unwrap();
assert_eq!(data, decrypted) assert_eq!(data, decrypted)
} }
@ -33,9 +33,9 @@ fn enc_skip() {
let (header1, encrypted1, nonce1) = alice_ratchet.encrypt(&data, b""); let (header1, encrypted1, nonce1) = alice_ratchet.encrypt(&data, b"");
let (header2, encrypted2, nonce2) = alice_ratchet.encrypt(&data, b""); let (header2, encrypted2, nonce2) = alice_ratchet.encrypt(&data, b"");
let (header3, encrypted3, nonce3) = alice_ratchet.encrypt(&data, b""); let (header3, encrypted3, nonce3) = alice_ratchet.encrypt(&data, b"");
let decrypted3 = bob_ratchet.decrypt(&header3, &encrypted3, &nonce3, b""); let decrypted3 = bob_ratchet.decrypt(&header3, &encrypted3, &nonce3, b"").unwrap();
let decrypted2 = bob_ratchet.decrypt(&header2, &encrypted2, &nonce2, b""); let decrypted2 = bob_ratchet.decrypt(&header2, &encrypted2, &nonce2, b"").unwrap();
let decrypted1 = bob_ratchet.decrypt(&header1, &encrypted1, &nonce1, b""); let decrypted1 = bob_ratchet.decrypt(&header1, &encrypted1, &nonce1, b"").unwrap();
let comp_res = decrypted1 == data && decrypted2 == data && decrypted3 == data; let comp_res = decrypted1 == data && decrypted2 == data && decrypted3 == data;
assert!(comp_res) assert!(comp_res)
} }
@ -56,9 +56,9 @@ fn encryt_decrypt_four() {
let (mut bob_ratchet, public_key) = Ratchet::init_bob(sk); let (mut bob_ratchet, public_key) = Ratchet::init_bob(sk);
let mut alice_ratchet = Ratchet::init_alice(sk, public_key); let mut alice_ratchet = Ratchet::init_alice(sk, public_key);
let (header1, encrypted1, nonce1) = alice_ratchet.encrypt(&data, b""); let (header1, encrypted1, nonce1) = alice_ratchet.encrypt(&data, b"");
let decrypted1 = bob_ratchet.decrypt(&header1, &encrypted1, &nonce1, b""); let decrypted1 = bob_ratchet.decrypt(&header1, &encrypted1, &nonce1, b"").unwrap();
let (header2, encrypted2, nonce2) = bob_ratchet.encrypt(&data, b""); let (header2, encrypted2, nonce2) = bob_ratchet.encrypt(&data, b"");
let decrypted2 = alice_ratchet.decrypt(&header2, &encrypted2, &nonce2, b""); let decrypted2 = alice_ratchet.decrypt(&header2, &encrypted2, &nonce2, b"").unwrap();
let comp_res = decrypted1 == data && decrypted2 == data; let comp_res = decrypted1 == data && decrypted2 == data;
assert!(comp_res) assert!(comp_res)
} }
@ -81,7 +81,7 @@ fn ench_enc_single() {
let mut alice_ratchet = RatchetEncHeader::init_alice(sk, public_key, shared_hka, shared_nhkb); let mut alice_ratchet = RatchetEncHeader::init_alice(sk, public_key, shared_hka, shared_nhkb);
let data = include_bytes!("../src/header.rs").to_vec(); let data = include_bytes!("../src/header.rs").to_vec();
let (header, encrypted, nonce) = alice_ratchet.encrypt(&data, b""); let (header, encrypted, nonce) = alice_ratchet.encrypt(&data, b"");
let decrypted = bob_ratchet.decrypt(&header, &encrypted, &nonce, b""); let decrypted = bob_ratchet.decrypt(&header, &encrypted, &nonce, b"").unwrap();
assert_eq!(data, decrypted) assert_eq!(data, decrypted)
} }
@ -96,9 +96,9 @@ fn ench_enc_skip() {
let (header1, encrypted1, nonce1) = alice_ratchet.encrypt(&data, b""); let (header1, encrypted1, nonce1) = alice_ratchet.encrypt(&data, b"");
let (header2, encrypted2, nonce2) = alice_ratchet.encrypt(&data, b""); let (header2, encrypted2, nonce2) = alice_ratchet.encrypt(&data, b"");
let (header3, encrypted3, nonce3) = alice_ratchet.encrypt(&data, b""); let (header3, encrypted3, nonce3) = alice_ratchet.encrypt(&data, b"");
let decrypted3 = bob_ratchet.decrypt(&header3, &encrypted3, &nonce3, b""); let decrypted3 = bob_ratchet.decrypt(&header3, &encrypted3, &nonce3, b"").unwrap();
let decrypted2 = bob_ratchet.decrypt(&header2, &encrypted2, &nonce2, b""); let decrypted2 = bob_ratchet.decrypt(&header2, &encrypted2, &nonce2, b"").unwrap();
let decrypted1 = bob_ratchet.decrypt(&header1, &encrypted1, &nonce1, b""); let decrypted1 = bob_ratchet.decrypt(&header1, &encrypted1, &nonce1, b"").unwrap();
let comp_res = decrypted1 == data && decrypted2 == data && decrypted3 == data; let comp_res = decrypted1 == data && decrypted2 == data && decrypted3 == data;
assert!(comp_res) assert!(comp_res)
} }
@ -123,9 +123,9 @@ fn ench_decrypt_four() {
let mut alice_ratchet = RatchetEncHeader::init_alice(sk, public_key, shared_hka, shared_nhkb); let mut alice_ratchet = RatchetEncHeader::init_alice(sk, public_key, shared_hka, shared_nhkb);
let data = include_bytes!("../src/dh.rs").to_vec(); let data = include_bytes!("../src/dh.rs").to_vec();
let (header1, encrypted1, nonce1) = alice_ratchet.encrypt(&data, b""); let (header1, encrypted1, nonce1) = alice_ratchet.encrypt(&data, b"");
let decrypted1 = bob_ratchet.decrypt(&header1, &encrypted1, &nonce1, b""); let decrypted1 = bob_ratchet.decrypt(&header1, &encrypted1, &nonce1, b"").unwrap();
let (header2, encrypted2, nonce2) = bob_ratchet.encrypt(&data, b""); let (header2, encrypted2, nonce2) = bob_ratchet.encrypt(&data, b"");
let decrypted2 = alice_ratchet.decrypt(&header2, &encrypted2, &nonce2, b""); let decrypted2 = alice_ratchet.decrypt(&header2, &encrypted2, &nonce2, b"").unwrap();
let comp_res = decrypted1 == data && decrypted2 == data; let comp_res = decrypted1 == data && decrypted2 == data;
assert!(comp_res) assert!(comp_res)
} }
@ -156,7 +156,7 @@ fn ench_enc_skip_panic() {
let header = headers.get(idx).unwrap(); let header = headers.get(idx).unwrap();
let encrypted = encrypteds.get(idx).unwrap(); let encrypted = encrypteds.get(idx).unwrap();
let nonce = nonces.get(idx).unwrap(); let nonce = nonces.get(idx).unwrap();
let decrypted = bob_ratchet.decrypt(header, encrypted, nonce, b""); let decrypted = bob_ratchet.decrypt(header, encrypted, nonce, b"").unwrap();
decrypteds.push(decrypted); decrypteds.push(decrypted);
} }
} }