Skip to main content

rustfs_crypto/encdec/
stream_io.rs

1// Copyright 2024 RustFS Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! sio-go compatible stream encryption for IAM config.
16//! Header: salt(32) + alg_id(1) + nonce_prefix(8) = 41 bytes.
17//! Body: DARE-style fragmented AEAD (bufSize=16384, per-fragment nonce).
18
19#![allow(deprecated)] // AeadInPlace deprecated in favor of AeadInOut; keep for aead 0.6 compatibility
20
21use crate::encdec::id::ID;
22use crate::error::Error;
23use aes_gcm::{
24    Aes256Gcm,
25    aead::{AeadCore, AeadInPlace, KeyInit as _, array::Array},
26};
27use chacha20poly1305::ChaCha20Poly1305;
28
29const STREAM_IO_HEADER_LEN: usize = 41;
30const SIO_BUF_SIZE: usize = 16384;
31const SIO_NONCE_PREFIX_LEN: usize = 8;
32const AES_GCM_OVERHEAD: usize = 16;
33const CHACHA_OVERHEAD: usize = 16;
34
35/// Decrypt data in stream_io (sio-go) format.
36pub fn decrypt_stream_io(password: &[u8], data: &[u8]) -> Result<Vec<u8>, Error> {
37    if data.len() < STREAM_IO_HEADER_LEN {
38        return Err(Error::ErrUnexpectedHeader);
39    }
40    let salt = &data[0..32];
41    let id = ID::try_from(data[32])?;
42    let nonce_prefix = &data[33..41];
43    let body = &data[STREAM_IO_HEADER_LEN..];
44
45    let key = id.get_key(password, salt)?;
46
47    match id {
48        ID::Argon2idChaCHa20Poly1305 => decrypt_stream(
49            ChaCha20Poly1305::new_from_slice(&key).map_err(|e| Error::ErrInvalidInput(e.to_string()))?,
50            nonce_prefix,
51            body,
52            CHACHA_OVERHEAD,
53        ),
54        _ => decrypt_stream(
55            Aes256Gcm::new_from_slice(&key).map_err(|e| Error::ErrInvalidInput(e.to_string()))?,
56            nonce_prefix,
57            body,
58            AES_GCM_OVERHEAD,
59        ),
60    }
61}
62
63fn decrypt_stream<A>(aead: A, nonce_prefix: &[u8], body: &[u8], overhead: usize) -> Result<Vec<u8>, Error>
64where
65    A: AeadInPlace,
66{
67    let ciphertext_len = SIO_BUF_SIZE + overhead;
68    let ad = build_associated_data(&aead, nonce_prefix)?;
69    let mut plain = Vec::with_capacity(body.len());
70
71    let mut seq_num: u32 = 1;
72    let mut pos = 0;
73
74    while pos < body.len() {
75        let remaining = body.len() - pos;
76        let frag_len = remaining.min(ciphertext_len);
77        let is_last = (pos + frag_len) == body.len();
78
79        if frag_len < overhead {
80            return Err(Error::ErrDecryptFailed(aes_gcm::aead::Error));
81        }
82
83        let mut nonce = [0u8; 12];
84        nonce[0..SIO_NONCE_PREFIX_LEN].copy_from_slice(nonce_prefix);
85        nonce[8..12].copy_from_slice(&seq_num.to_le_bytes());
86
87        let mut ad_mut = ad.clone();
88        ad_mut[0] = if is_last { 0x80 } else { 0x00 };
89
90        let fragment = &body[pos..pos + frag_len];
91        let tag_len = overhead;
92        let (ct, tag) = fragment.split_at(frag_len - tag_len);
93
94        let mut buffer = ct.to_vec();
95        let nonce_arr = Array::<u8, <A as AeadCore>::NonceSize>::try_from(&nonce[..])
96            .map_err(|_| Error::ErrDecryptFailed(aes_gcm::aead::Error))?;
97        let tag_arr =
98            Array::<u8, <A as AeadCore>::TagSize>::try_from(tag).map_err(|_| Error::ErrDecryptFailed(aes_gcm::aead::Error))?;
99        aead.decrypt_in_place_detached(&nonce_arr, &ad_mut, &mut buffer, &tag_arr)
100            .map_err(|_| Error::ErrDecryptFailed(aes_gcm::aead::Error))?;
101        plain.extend_from_slice(&buffer);
102
103        pos += frag_len;
104        seq_num += 1;
105
106        if is_last {
107            break;
108        }
109    }
110
111    Ok(plain)
112}
113
114fn build_associated_data<A>(aead: &A, nonce_prefix: &[u8]) -> Result<Vec<u8>, Error>
115where
116    A: AeadInPlace,
117{
118    let mut nonce = [0u8; 12];
119    nonce[0..SIO_NONCE_PREFIX_LEN].copy_from_slice(nonce_prefix);
120    nonce[8..12].copy_from_slice(&0u32.to_le_bytes());
121
122    let nonce_arr = Array::<u8, <A as AeadCore>::NonceSize>::try_from(&nonce[..])
123        .map_err(|_| Error::ErrEncryptFailed(aes_gcm::aead::Error))?;
124    let mut empty: [u8; 0] = [];
125    let tag = aead
126        .encrypt_in_place_detached(&nonce_arr, &[] as &[u8], &mut empty)
127        .map_err(Error::ErrEncryptFailed)?;
128
129    let mut ad = vec![0u8; 1 + tag.len()];
130    ad[0] = 0x00;
131    ad[1..].copy_from_slice(tag.as_slice());
132    Ok(ad)
133}
134
135/// Encrypt data in stream_io (sio-go) format.
136pub fn encrypt_stream_io(password: &[u8], data: &[u8]) -> Result<Vec<u8>, Error> {
137    let salt: [u8; 32] = rand::random();
138
139    #[cfg(feature = "fips")]
140    let id = ID::Pbkdf2AESGCM;
141
142    #[cfg(not(feature = "fips"))]
143    let id = if crate::encdec::encrypt::native_aes() {
144        ID::Argon2idAESGCM
145    } else {
146        ID::Argon2idChaCHa20Poly1305
147    };
148
149    let key = id.get_key(password, &salt)?;
150    let nonce_prefix: [u8; SIO_NONCE_PREFIX_LEN] = rand::random();
151
152    let mut out = Vec::with_capacity(STREAM_IO_HEADER_LEN + data.len() + 32);
153    out.extend_from_slice(&salt);
154    out.push(id as u8);
155    out.extend_from_slice(&nonce_prefix);
156
157    match id {
158        ID::Argon2idChaCHa20Poly1305 => encrypt_stream(
159            ChaCha20Poly1305::new_from_slice(&key).map_err(|e| Error::ErrInvalidInput(e.to_string()))?,
160            &nonce_prefix,
161            data,
162            &mut out,
163            CHACHA_OVERHEAD,
164        )?,
165        _ => encrypt_stream(
166            Aes256Gcm::new_from_slice(&key).map_err(|e| Error::ErrInvalidInput(e.to_string()))?,
167            &nonce_prefix,
168            data,
169            &mut out,
170            AES_GCM_OVERHEAD,
171        )?,
172    }
173
174    Ok(out)
175}
176
177fn encrypt_stream<A>(
178    aead: A,
179    nonce_prefix: &[u8; SIO_NONCE_PREFIX_LEN],
180    data: &[u8],
181    out: &mut Vec<u8>,
182    _overhead: usize,
183) -> Result<(), Error>
184where
185    A: AeadInPlace,
186{
187    let ad = build_associated_data(&aead, nonce_prefix)?;
188    let mut seq_num: u32 = 1;
189    let mut pos = 0;
190
191    while pos < data.len() {
192        let remaining = data.len() - pos;
193        let is_last = remaining <= SIO_BUF_SIZE;
194
195        let chunk_len = if is_last { remaining } else { SIO_BUF_SIZE };
196        let chunk = &data[pos..pos + chunk_len];
197
198        let mut nonce = [0u8; 12];
199        nonce[0..SIO_NONCE_PREFIX_LEN].copy_from_slice(nonce_prefix);
200        nonce[8..12].copy_from_slice(&seq_num.to_le_bytes());
201
202        let mut ad_mut = ad.clone();
203        ad_mut[0] = if is_last { 0x80 } else { 0x00 };
204
205        let mut buffer = chunk.to_vec();
206        let nonce_arr = Array::<u8, <A as AeadCore>::NonceSize>::try_from(&nonce[..])
207            .map_err(|_| Error::ErrEncryptFailed(aes_gcm::aead::Error))?;
208        let tag = aead
209            .encrypt_in_place_detached(&nonce_arr, &ad_mut, &mut buffer)
210            .map_err(Error::ErrEncryptFailed)?;
211        out.extend_from_slice(&buffer);
212        out.extend_from_slice(tag.as_slice());
213
214        pos += chunk_len;
215        seq_num += 1;
216    }
217
218    Ok(())
219}