mz_catalog/durable/upgrade/
v77_to_v78.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use crate::durable::upgrade::MigrationAction;
11use crate::durable::upgrade::objects_v77 as v77;
12use base64::prelude::*;
13use mz_ore::now::SYSTEM_TIME;
14
15// Old: SCRAM-SHA-256$<iters>:<salt>$<client_key>:<server_key>
16// New: SCRAM-SHA-256$<iters>:<salt>$<stored_key>:<server_key>, where stored_key = SHA256(client_key)
17fn migrate_scram_client_to_stored(hash_old: &str) -> Option<String> {
18    let parts: Vec<&str> = hash_old.split('$').collect();
19    if parts.len() != 3 || parts[0] != "SCRAM-SHA-256" {
20        return None;
21    }
22    let iters_salt = parts[1];
23    let key_parts: Vec<&str> = parts[2].split(':').collect();
24    if key_parts.len() != 2 {
25        return None;
26    }
27    let first_key_b64 = key_parts[0];
28    let server_key_b64 = key_parts[1];
29
30    let Ok(first_key) = BASE64_STANDARD.decode(first_key_b64) else {
31        tracing::warn!("failed to decode base64 client key during scram migration");
32        return None;
33    };
34    if first_key.len() != 32 {
35        tracing::warn!("unexpected client key length during scram migration");
36        return None;
37    }
38
39    let stored_key = openssl::sha::sha256(&first_key);
40    let stored_key_b64 = BASE64_STANDARD.encode(stored_key);
41
42    Some(format!(
43        "SCRAM-SHA-256${}${}:{}",
44        iters_salt, stored_key_b64, server_key_b64
45    ))
46}
47
48pub fn upgrade(
49    snapshot: Vec<v77::StateUpdateKind>,
50) -> Vec<MigrationAction<v77::StateUpdateKind, v77::StateUpdateKind>> {
51    let mut migrations = Vec::new();
52    for update in snapshot {
53        match update.kind {
54            Some(v77::state_update_kind::Kind::RoleAuth(old_role_auth)) => {
55                let Some(ref value) = old_role_auth.value else {
56                    continue;
57                };
58                let Some(ref hashed_password) = value.password_hash else {
59                    continue;
60                };
61                let Some(new_hash) = migrate_scram_client_to_stored(hashed_password) else {
62                    continue;
63                };
64
65                let new_role_auth = v77::state_update_kind::RoleAuth {
66                    key: old_role_auth.key.clone(),
67                    value: Some(v77::RoleAuthValue {
68                        password_hash: Some(new_hash),
69                        updated_at: Some(v77::EpochMillis {
70                            millis: SYSTEM_TIME(),
71                        }),
72                    }),
73                };
74                let old_role_auth = v77::StateUpdateKind {
75                    kind: Some(v77::state_update_kind::Kind::RoleAuth(old_role_auth)),
76                };
77                let new_role_auth = v77::StateUpdateKind {
78                    kind: Some(v77::state_update_kind::Kind::RoleAuth(new_role_auth)),
79                };
80                let migration = MigrationAction::Update(old_role_auth, new_role_auth);
81                migrations.push(migration);
82            }
83            _ => {}
84        }
85    }
86    migrations
87}