Kotlin/Swift local date#1154
Conversation
🤖 Bitwarden Claude Code ReviewOverall Assessment: REQUEST CHANGES Reviewed the change migrating passport and drivers-license date fields from Code Review Details
No new inline findings were posted; the substantive issue above is already captured in an open thread and is not duplicated here. |
| date_of_birth: self | ||
| .date_of_birth | ||
| .decrypt(ctx, key) | ||
| .ok() | ||
| .flatten() | ||
| .and_then(|s: String| s.parse().ok()), |
There was a problem hiding this comment.
❓ QUESTION: Silent data loss when the decrypted date string is not a valid ISO 8601 date.
Details
.and_then(|s: String| s.parse().ok()) discards the value when parsing fails, so any existing encrypted data containing a non-NaiveDate-parseable string (e.g. "06/15/1985", "June 15, 1990", an empty string, or any locale-specific format a prior client may have stored before this type-safety pass) decrypts successfully but appears as None in the PassportView. The encrypted string is still on the server, but the user-facing field silently empties.
This same pattern repeats for issue_date (109-114) and expiration_date (115-120), and again in DriversLicense::decrypt and the blob From<&PassportDataV1> / From<&DriversLicenseDataV1> conversions.
Given these cipher types are recent (commit e504da35) and clients have not yet shipped UIs that write to them, real-world impact is likely small — but two questions are worth confirming before merge:
- Is silent coercion to
Noneon parse failure the desired behavior, or shoulddecryptsurface aCryptoError(or at minimum emit atracing::warn!) so non-ISO data is observable rather than disappearing? - The existing
test_recorded_*_test_vectortests assert backwards-compat for the encrypted-string format, but they only cover values that already happen to be ISO. Worth a unit test asserting the chosen behavior for a non-parseable decrypted string so the contract is locked in.
|
🔍 SDK Breaking Change DetectionSDK Version:
Breaking change detection uses the build of the SDK from this branch, including any incompatibities pre-existing on or merged into this branch. Check the workflow logs to confirm. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1154 +/- ##
==========================================
+ Coverage 84.12% 84.14% +0.02%
==========================================
Files 446 446
Lines 58817 58912 +95
==========================================
+ Hits 49478 49572 +94
- Misses 9339 9340 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
coroiu
left a comment
There was a problem hiding this comment.
Whether NaiveData is the correct usage here or not will have to be up to Vault (my understanding is that timezone data will be lost), but the bigger hurdle for this PR is WASM support. You'll have to confirm what type of object NaiveDate is translated into on the TypeScript side and then add a custom typescript section for it, similar to how DateTime looks (search for it in the code to find where to put it:
/**
* RFC3339 compliant date-time string.
* @typeParam T - Not used in JavaScript.
*/
export type DateTime<T = unknown> = string;
Most of the changes seem to be within Vaults domain so make sure to reach out to them for help and guidance!
nick-livefront
left a comment
There was a problem hiding this comment.
Whether NaiveData is the correct usage here or not will have to be up to Vault (my understanding is that timezone data will be lost)
These specific dates do not have timezone data, they're only YYYY-MM-DD dates.
I do agree with the custom types approach that @coroiu suggested as the NaiveDate gets converted to a string on the TS side. The other option I considered was adding #[cfg_attr(feature = "wasm", tsify(type = "string"))] to each property but that would be tedious to maintain.
I'll send this to our internal channel to see if there are any other opinions but I think you could continue with the custom type.
942c74a to
8605774
Compare
507c348 to
fbfb3e5
Compare
| into_custom = """ | ||
| { | ||
| let formatter = DateFormatter() | ||
| formatter.locale = Locale(identifier: "en_US_POSIX") | ||
| formatter.timeZone = TimeZone(identifier: "UTC") | ||
| formatter.dateFormat = "yyyy-MM-dd" | ||
| formatter.isLenient = false | ||
| return formatter.date(from: {}) | ||
| }() | ||
| """ | ||
| from_custom = """ |
There was a problem hiding this comment.
❌ CRITICAL: Swift into_custom is not a valid single expression and will break the Swift build.
Details and fix
UniFFI substitutes into_custom as the right-hand side of an expression that must yield a single Date. This template expands to two independent, unconnected expressions:
{
let formatter = DateFormatter()
...
formatter.date(from: <val>) // Date? computed then discarded
}()
ISO8601DateFormatter().date(from: <val> + "T00:00:00Z")! // second, separate expressionThe immediately-invoked closure's result is discarded, and it is followed by a second standalone expression with no operator joining them — this is not a valid single Swift expression and will fail to compile in the generated BitwardenSdk sources built by build-swift.yml on every PR. It looks like two candidate implementations were accidentally left in.
Additionally, the trailing ! force-unwrap will crash at runtime if the stored string is not a parseable date (existing encrypted data may contain arbitrary strings).
Pick one implementation and return a single value without force-unwrapping, e.g.:
into_custom = """
{
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(identifier: "UTC")
formatter.dateFormat = "yyyy-MM-dd"
formatter.isLenient = false
return formatter.date(from: {}) ?? Date(timeIntervalSince1970: 0)
}()
"""
(choose an appropriate fallback for unparseable input rather than crashing).
There was a problem hiding this comment.
Claude finished @david-livefront's task in 1m 22s —— View job
Re: Swift into_custom at uniffi.toml:33
Thanks @david-livefront — I've re-read the current uniffi.toml. The original blocker is resolved: the two accidental candidate implementations are gone, and into_custom/from_custom are now each a single, valid IIFE expression. 👍
However, one part of the earlier soundness concern still stands, so I don't think the Swift build will pass yet:
into_custom returns Date? where a non-optional Date is required.
{
let formatter = DateFormatter()
...
return formatter.date(from: {}) // ← DateFormatter.date(from:) is Date?
}()UniFFI renders into_custom as the return value of a converter function whose return type is type_name = "Date" (non-optional). Since DateFormatter.date(from:) yields Date?, the generated BitwardenCoreFFI sources will fail to compile with a "cannot convert value of type Date? to expected type Date" error — which build-swift.yml runs on every PR.
In practice the Rust side always lowers via obj.to_string(), so the string handed to into_custom is always a valid yyyy-MM-dd. That means a fallback is only there to satisfy the type checker and will never realistically be hit — so a nil-coalesce is clean and avoids a force-unwrap crash:
into_custom = """
{
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(identifier: "UTC")
formatter.dateFormat = "yyyy-MM-dd"
formatter.isLenient = false
return formatter.date(from: {}) ?? Date(timeIntervalSince1970: 0)
}()
"""For completeness on the other two threads from earlier:
- ✅ WASM/TypeScript (@coroiu's point): resolved —
NaiveDateis now declared as a custom TS type incustom_types.rs:24. - ❓ Silent data-loss on non-ISO decrypt (
passport.rs/drivers_license.rs): still open for Vault to weigh in on, but as noted, real-world impact is likely small given these cipher types are new.
Want me to push the one-line Swift fix to this branch?
fbfb3e5 to
bb73096
Compare



🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-38604
📔 Objective
This is my first SDK PR and Claude helped me manage a lot of the heavy lifting, please let me know if I am properly honoring the best practices.
This PR updates the date values in the SDK for some of the new cipher types to utilize the Rust
NaiveDateand translate it to a JavaLocalDate.🚨 Breaking Changes
This will create a breaking change for the Kotlin output of the SDK where previously certain date Strings will now be converted to the type-safe
LocalDateclass. This is required to enforce a consistent date format that can be parsed an utilized properly across the platforms.