From 4352fffeec7915e45559b46dce18640a25f46801 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Fri, 9 Jan 2026 12:21:10 +0000 Subject: [PATCH 1/2] Fix web-vault version check and update web-vault (#6686) --- docker/DockerSettings.yaml | 4 +- docker/Dockerfile.alpine | 12 ++-- docker/Dockerfile.debian | 12 ++-- docker/Dockerfile.j2 | 6 +- src/api/admin.rs | 78 ++++++++++++++++------ src/config.rs | 4 +- src/main.rs | 2 +- src/static/scripts/admin_diagnostics.js | 12 ++-- src/static/templates/admin/diagnostics.hbs | 4 +- src/util.rs | 2 +- 10 files changed, 87 insertions(+), 49 deletions(-) diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index e74f979c..dd87a9e3 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -1,6 +1,6 @@ --- -vault_version: "v2025.12.1.1" -vault_image_digest: "sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457" +vault_version: "v2025.12.1+build.3" +vault_image_digest: "sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42" # Cross Compile Docker Helper Scripts v1.9.0 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 6453ba1f..2a6cf9f2 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.1.1 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1.1 -# [docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457] +# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.1_build.3 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1_build.3 +# [docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457 -# [docker.io/vaultwarden/web-vault:v2025.12.1.1] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 +# [docker.io/vaultwarden/web-vault:v2025.12.1_build.3] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 AS vault ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 and linux/arm64 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 25545f32..03c0faba 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.1.1 -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1.1 -# [docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457] +# $ docker pull docker.io/vaultwarden/web-vault:v2025.12.1_build.3 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2025.12.1_build.3 +# [docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457 -# [docker.io/vaultwarden/web-vault:v2025.12.1.1] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 +# [docker.io/vaultwarden/web-vault:v2025.12.1_build.3] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:90261e5d5438b67a00cb12d8615cf3f130a65e81f33a3f5ff190c6202bf0e457 AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:bf5aa55dc7bcb99f85d2a88ff44d32cdc832e934a0603fe28e5c3f92904bad42 AS vault ########################## Cross Compile Docker Helper Scripts ########################## ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts diff --git a/docker/Dockerfile.j2 b/docker/Dockerfile.j2 index cf8106bd..f745780e 100644 --- a/docker/Dockerfile.j2 +++ b/docker/Dockerfile.j2 @@ -19,13 +19,13 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:{{ vault_version }} -# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" docker.io/vaultwarden/web-vault:{{ vault_version }} +# $ docker pull docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }} +# $ docker image inspect --format "{{ '{{' }}.RepoDigests}}" docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }} # [docker.io/vaultwarden/web-vault@{{ vault_image_digest }}] # # - Conversely, to get the tag name from the digest: # $ docker image inspect --format "{{ '{{' }}.RepoTags}}" docker.io/vaultwarden/web-vault@{{ vault_image_digest }} -# [docker.io/vaultwarden/web-vault:{{ vault_version }}] +# [docker.io/vaultwarden/web-vault:{{ vault_version | replace('+', '_') }}] # FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@{{ vault_image_digest }} AS vault diff --git a/src/api/admin.rs b/src/api/admin.rs index d36da8f9..badfaa3a 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -31,7 +31,7 @@ use crate::{ http_client::make_http_request, mail, util::{ - container_base_image, format_naive_datetime_local, get_display_size, get_web_vault_version, + container_base_image, format_naive_datetime_local, get_active_web_release, get_display_size, is_running_in_container, NumberOrString, }, CONFIG, VERSION, @@ -689,6 +689,26 @@ async fn get_ntp_time(has_http_access: bool) -> String { String::from("Unable to fetch NTP time.") } +fn web_vault_compare(active: &str, latest: &str) -> i8 { + use semver::Version; + use std::cmp::Ordering; + + let active_semver = Version::parse(active).unwrap_or_else(|e| { + warn!("Unable to parse active web-vault version '{active}': {e}"); + Version::parse("2025.1.1").unwrap() + }); + let latest_semver = Version::parse(latest).unwrap_or_else(|e| { + warn!("Unable to parse latest web-vault version '{latest}': {e}"); + Version::parse("2025.1.1").unwrap() + }); + + match active_semver.cmp(&latest_semver) { + Ordering::Less => -1, + Ordering::Equal => 0, + Ordering::Greater => 1, + } +} + #[get("/diagnostics")] async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult> { use chrono::prelude::*; @@ -708,32 +728,21 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> A _ => "Unable to resolve domain name.".to_string(), }; - let (latest_release, latest_commit, latest_web_build) = get_release_info(has_http_access).await; + let (latest_vw_release, latest_vw_commit, latest_web_release) = get_release_info(has_http_access).await; + let active_web_release = get_active_web_release(); + let web_vault_compare = web_vault_compare(&active_web_release, &latest_web_release); let ip_header_name = &ip_header.0.unwrap_or_default(); - // Get current running versions - let web_vault_version = get_web_vault_version(); - - // Check if the running version is newer than the latest stable released version - let web_vault_pre_release = if let Ok(web_ver_match) = semver::VersionReq::parse(&format!(">{latest_web_build}")) { - web_ver_match.matches( - &semver::Version::parse(&web_vault_version).unwrap_or_else(|_| semver::Version::parse("2025.1.1").unwrap()), - ) - } else { - error!("Unable to parse latest_web_build: '{latest_web_build}'"); - false - }; - let diagnostics_json = json!({ "dns_resolved": dns_resolved, "current_release": VERSION, - "latest_release": latest_release, - "latest_commit": latest_commit, + "latest_release": latest_vw_release, + "latest_commit": latest_vw_commit, "web_vault_enabled": &CONFIG.web_vault_enabled(), - "web_vault_version": web_vault_version, - "latest_web_build": latest_web_build, - "web_vault_pre_release": web_vault_pre_release, + "active_web_release": active_web_release, + "latest_web_release": latest_web_release, + "web_vault_compare": web_vault_compare, "running_within_container": running_within_container, "container_base_image": if running_within_container { container_base_image() } else { "Not applicable" }, "has_http_access": has_http_access, @@ -844,3 +853,32 @@ impl<'r> FromRequest<'r> for AdminToken { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validate_web_vault_compare() { + // web_vault_compare(active, latest) + // Test normal versions + assert!(web_vault_compare("2025.12.0", "2025.12.1") == -1); + assert!(web_vault_compare("2025.12.1", "2025.12.1") == 0); + assert!(web_vault_compare("2025.12.2", "2025.12.1") == 1); + + // Test patched/+build.n versions + // Newer latest version + assert!(web_vault_compare("2025.12.0+build.1", "2025.12.1") == -1); + assert!(web_vault_compare("2025.12.1", "2025.12.1+build.1") == -1); + assert!(web_vault_compare("2025.12.0+build.1", "2025.12.1+build.1") == -1); + assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.2") == -1); + // Equal versions + assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1+build.1") == 0); + assert!(web_vault_compare("2025.12.2+build.2", "2025.12.2+build.2") == 0); + // Newer active version + assert!(web_vault_compare("2025.12.1+build.1", "2025.12.1") == 1); + assert!(web_vault_compare("2025.12.2", "2025.12.1+build.1") == 1); + assert!(web_vault_compare("2025.12.2+build.1", "2025.12.1+build.1") == 1); + assert!(web_vault_compare("2025.12.1+build.3", "2025.12.1+build.2") == 1); + } +} diff --git a/src/config.rs b/src/config.rs index 6bfdea80..4fb103fa 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,7 +14,7 @@ use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor}; use crate::{ error::Error, - util::{get_env, get_env_bool, get_web_vault_version, is_valid_email, parse_experimental_client_feature_flags}, + util::{get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags}, }; static CONFIG_FILE: LazyLock = LazyLock::new(|| { @@ -1849,7 +1849,7 @@ fn to_json<'reg, 'rc>( // Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then. // The default is based upon the version since this feature is added. static WEB_VAULT_VERSION: LazyLock = LazyLock::new(|| { - let vault_version = get_web_vault_version(); + let vault_version = get_active_web_release(); // Use a single regex capture to extract version components let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap(); re.captures(&vault_version) diff --git a/src/main.rs b/src/main.rs index b5ff93ae..8eef2e8c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -126,7 +126,7 @@ fn parse_args() { exit(0); } else if pargs.contains(["-v", "--version"]) { config::SKIP_CONFIG_VALIDATION.store(true, Ordering::Relaxed); - let web_vault_version = util::get_web_vault_version(); + let web_vault_version = util::get_active_web_release(); println!("Vaultwarden {version}"); println!("Web-Vault {web_vault_version}"); exit(0); diff --git a/src/static/scripts/admin_diagnostics.js b/src/static/scripts/admin_diagnostics.js index 108034dd..5594b439 100644 --- a/src/static/scripts/admin_diagnostics.js +++ b/src/static/scripts/admin_diagnostics.js @@ -29,7 +29,7 @@ function isValidIp(ip) { return ipv4Regex.test(ip) || ipv6Regex.test(ip); } -function checkVersions(platform, installed, latest, commit=null, pre_release=false) { +function checkVersions(platform, installed, latest, commit=null, compare_order=0) { if (installed === "-" || latest === "-") { document.getElementById(`${platform}-failed`).classList.remove("d-none"); return; @@ -37,7 +37,7 @@ function checkVersions(platform, installed, latest, commit=null, pre_release=fal // Only check basic versions, no commit revisions if (commit === null || installed.indexOf("-") === -1) { - if (platform === "web" && pre_release === true) { + if (platform === "web" && compare_order === 1) { document.getElementById(`${platform}-prerelease`).classList.remove("d-none"); } else if (installed == latest) { document.getElementById(`${platform}-success`).classList.remove("d-none"); @@ -83,7 +83,7 @@ async function generateSupportString(event, dj) { let supportString = "### Your environment (Generated via diagnostics page)\n\n"; supportString += `* Vaultwarden version: v${dj.current_release}\n`; - supportString += `* Web-vault version: v${dj.web_vault_version}\n`; + supportString += `* Web-vault version: v${dj.active_web_release}\n`; supportString += `* OS/Arch: ${dj.host_os}/${dj.host_arch}\n`; supportString += `* Running within a container: ${dj.running_within_container} (Base: ${dj.container_base_image})\n`; supportString += `* Database type: ${dj.db_type}\n`; @@ -208,9 +208,9 @@ function initVersionCheck(dj) { } checkVersions("server", serverInstalled, serverLatest, serverLatestCommit); - const webInstalled = dj.web_vault_version; - const webLatest = dj.latest_web_build; - checkVersions("web", webInstalled, webLatest, null, dj.web_vault_pre_release); + const webInstalled = dj.active_web_release; + const webLatest = dj.latest_web_release; + checkVersions("web", webInstalled, webLatest, null, dj.web_vault_compare); } function checkDns(dns_resolved) { diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs index 503c6954..f8edabb2 100644 --- a/src/static/templates/admin/diagnostics.hbs +++ b/src/static/templates/admin/diagnostics.hbs @@ -27,13 +27,13 @@ Pre-Release
- {{page_data.web_vault_version}} + {{page_data.active_web_release}}
Web Latest Unknown
- {{page_data.latest_web_build}} + {{page_data.latest_web_release}}
{{/if}} {{#unless page_data.web_vault_enabled}} diff --git a/src/util.rs b/src/util.rs index c7ba9ed1..aa4e7914 100644 --- a/src/util.rs +++ b/src/util.rs @@ -531,7 +531,7 @@ struct WebVaultVersion { version: String, } -pub fn get_web_vault_version() -> String { +pub fn get_active_web_release() -> String { let version_files = [ format!("{}/vw-version.json", CONFIG.web_vault_folder()), format!("{}/version.json", CONFIG.web_vault_folder()), From b2cd556f3e79673d3eff1dac9b7402c18aa69d69 Mon Sep 17 00:00:00 2001 From: Mathijs van Veluw Date: Wed, 14 Jan 2026 13:11:43 +0100 Subject: [PATCH 2/2] Fix User API Key login (#6712) When using the latest Bitwarden CLI and logging in using the API Key, it expects some extra fields, same as for normal login. This PR adds those fields and login is possible again via API Key. Fixes #6709 Signed-off-by: BlackDex --- src/api/identity.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/api/identity.rs b/src/api/identity.rs index e763ef46..722b3eab 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -610,6 +610,25 @@ async fn _user_api_key_login( info!("User {} logged in successfully via API key. IP: {}", user.email, ip.ip); + let has_master_password = !user.password_hash.is_empty(); + let master_password_unlock = if has_master_password { + json!({ + "Kdf": { + "KdfType": user.client_kdf_type, + "Iterations": user.client_kdf_iter, + "Memory": user.client_kdf_memory, + "Parallelism": user.client_kdf_parallelism + }, + // This field is named inconsistently and will be removed and replaced by the "wrapped" variant in the apps. + // https://github.com/bitwarden/android/blob/release/2025.12-rc41/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt#L22-L26 + "MasterKeyEncryptedUserKey": user.akey, + "MasterKeyWrappedUserKey": user.akey, + "Salt": user.email + }) + } else { + Value::Null + }; + // Note: No refresh_token is returned. The CLI just repeats the // client_credentials login flow when the existing token expires. let result = json!({ @@ -625,6 +644,11 @@ async fn _user_api_key_login( "KdfParallelism": user.client_kdf_parallelism, "ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing "scope": AuthMethod::UserApiKey.scope(), + "UserDecryptionOptions": { + "HasMasterPassword": has_master_password, + "MasterPasswordUnlock": master_password_unlock, + "Object": "userDecryptionOptions" + }, }); Ok(Json(result))