Skip to content

Instantly share code, notes, and snippets.

@bgrainger
Created January 19, 2026 19:33
Show Gist options
  • Select an option

  • Save bgrainger/0b52711e46f0646c4c51a91603efe250 to your computer and use it in GitHub Desktop.

Select an option

Save bgrainger/0b52711e46f0646c4c51a91603efe250 to your computer and use it in GitHub Desktop.
Import Password Safe XML to Bitwarden
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Xml.Linq;
using NGuid;
const string inputPath = @"C:\passwords.xml";
const string outputPath = @"C:\passwords.json";
// Existing folder IDs from Bitwarden
// Use 'bw get folder NAME' or URL to find existing IDs
var existingFolders = new Dictionary<string, string>
{
["Applications"] = "xxxxxxxx-xxxx-439c-xxxx-b3d30037c8c8",
["Financial"] = "xxxxxxxx-xxxx-41ff-xxxx-b3d30038c75e",
["Websites"] = "xxxxxxxx-xxxx-46f3-xxxx-b3d6016da122",
};
// Namespace for deterministic GUIDs
// NOTE: this didn't actually work because Bitwarden overwrites the GUIDs
var namespaceGuid = Guid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8"); // DNS namespace
Console.WriteLine($"Reading XML from: {inputPath}");
var doc = XDocument.Load(inputPath);
// Get delimiter from XML
var delimiter = doc.Root?.Attribute("delimiter")?.Value ?? "»";
Console.WriteLine($"Using delimiter: {delimiter}");
// Extract all unique groups and create folders
var groups = doc.Descendants("entry")
.Select(e => e.Element("group")?.Value)
.Where(g => !string.IsNullOrEmpty(g))
.Distinct()
.ToList();
var folders = new List<BitwardenFolder>();
var folderMap = new Dictionary<string, string>();
foreach (var group in groups)
{
string folderId;
if (existingFolders.TryGetValue(group!, out var existingId))
{
folderId = existingId;
}
else
{
// Create deterministic v5 GUID from folder name
folderId = GuidHelpers.CreateFromName(namespaceGuid, group!).ToString();
}
folders.Add(new BitwardenFolder { Id = folderId, Name = group! });
folderMap[group!] = folderId;
}
// Parse entries
var items = new List<BitwardenItem>();
foreach (var entry in doc.Descendants("entry"))
{
var title = entry.Element("title")?.Value;
var username = entry.Element("username")?.Value;
var password = entry.Element("password")?.Value;
var url = entry.Element("url")?.Value;
var notes = entry.Element("notes")?.Value;
var group = entry.Element("group")?.Value;
var ctimex = entry.Element("ctimex")?.Value;
var email = entry.Element("email")?.Value;
// Replace delimiter with newlines in notes
if (!string.IsNullOrEmpty(notes))
{
notes = notes.Replace(delimiter, "\n");
}
// Auto-generate URL from title if it looks like a domain
if (string.IsNullOrEmpty(url) && !string.IsNullOrEmpty(title))
{
if (System.Text.RegularExpressions.Regex.IsMatch(title,
@"^[a-zA-Z0-9][\w\-\.]*\.(com|net|org|edu|gov|io|co|uk|ca|au|de|fr|jp|cn|in|br|ru|it|es|mx|nl|se|no|dk|fi|pl|be|ch|at|nz|ie|sg|hk|za|kr|tw|th|my|id|ph|vn|pk|eg|ng|ke|gh|tz|ug|zm|zw|us|info|biz|mobi|name|pro|tel|travel|jobs|app|dev|tech|online|site|website|store|shop|blog|cloud|email|host|space|live|me|tv|cc|ws|top|xyz|club|vip|fun|link|click)$"))
{
url = $"https://{title}";
}
}
// Parse password history
var passwordHistory = new List<PasswordHistory>();
var pwhistory = entry.Element("pwhistory");
if (pwhistory != null)
{
var historyEntries = pwhistory.Element("history_entries")?.Elements("history_entry");
if (historyEntries != null)
{
foreach (var histEntry in historyEntries)
{
var changedx = histEntry.Element("changedx")?.Value;
var oldPassword = histEntry.Element("oldpassword")?.Value;
if (!string.IsNullOrEmpty(oldPassword))
{
passwordHistory.Add(new PasswordHistory
{
LastUsedDate = ParseTimestamp(changedx),
Password = oldPassword
});
}
}
}
}
var item = new BitwardenItem
{
OrganizationId = null,
FolderId = !string.IsNullOrEmpty(group) && folderMap.ContainsKey(group)
? folderMap[group]
: null,
Type = 1, // Login
Reprompt = 0,
Name = title ?? "Untitled",
Notes = notes,
Favorite = false,
Login = new BitwardenLogin
{
Username = username,
Password = password,
Uris = !string.IsNullOrEmpty(url)
? new[] { new BitwardenUri { Match = null, Uri = url } }
: null
},
PasswordHistory = passwordHistory.Count > 0 ? passwordHistory : null,
CollectionIds = null
};
if (DateTime.TryParse(ctimex, out var creationTime))
{
item.Fields ??= [];
item.Fields.Add(new() { Name = "Created Date", Value = creationTime.ToString("yyyy-MM-dd h:mm tt"), Type = 0 });
}
if (!string.IsNullOrEmpty(email))
{
item.Fields ??= [];
item.Fields.Add(new() { Name = "Email", Value = email, Type = 0 });
}
items.Add(item);
}
var export = new BitwardenExport
{
Folders = folders,
Items = items
};
var options = new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
var json = JsonSerializer.Serialize(export, options);
File.WriteAllText(outputPath, json);
Console.WriteLine($"Successfully exported {items.Count} items and {folders.Count} folders to: {outputPath}");
static string? ParseTimestamp(string? timestamp)
{
if (string.IsNullOrEmpty(timestamp))
return null;
if (DateTime.TryParse(timestamp, out var dt))
return dt.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
return null;
}
// Models
record BitwardenExport
{
[JsonPropertyName("folders")]
public List<BitwardenFolder> Folders { get; init; } = new();
[JsonPropertyName("items")]
public List<BitwardenItem> Items { get; init; } = new();
}
record BitwardenFolder
{
[JsonPropertyName("id")]
public required string Id { get; init; }
[JsonPropertyName("name")]
public required string Name { get; init; }
}
record BitwardenItem
{
[JsonPropertyName("organizationId")]
public string? OrganizationId { get; init; }
[JsonPropertyName("folderId")]
public string? FolderId { get; init; }
[JsonPropertyName("type")]
public int Type { get; init; }
[JsonPropertyName("reprompt")]
public int Reprompt { get; init; }
[JsonPropertyName("name")]
public required string Name { get; init; }
[JsonPropertyName("notes")]
public string? Notes { get; init; }
[JsonPropertyName("favorite")]
public bool Favorite { get; init; }
[JsonPropertyName("login")]
public BitwardenLogin? Login { get; init; }
[JsonPropertyName("passwordHistory")]
public List<PasswordHistory>? PasswordHistory { get; init; }
[JsonPropertyName("collectionIds")]
public string[]? CollectionIds { get; init; }
[JsonPropertyName("fields")]
public List<BitwardenField>? Fields { get; set; }
}
record BitwardenField
{
[JsonPropertyName("name")]
public required string Name { get; init; }
[JsonPropertyName("value")]
public required string Value { get; init; }
[JsonPropertyName("type")]
public int Type { get; init; }
}
record BitwardenLogin
{
[JsonPropertyName("username")]
public string? Username { get; init; }
[JsonPropertyName("password")]
public string? Password { get; init; }
[JsonPropertyName("uris")]
public BitwardenUri[]? Uris { get; init; }
}
record BitwardenUri
{
[JsonPropertyName("match")]
public object? Match { get; init; }
[JsonPropertyName("uri")]
public required string Uri { get; init; }
}
record PasswordHistory
{
[JsonPropertyName("lastUsedDate")]
public string? LastUsedDate { get; init; }
[JsonPropertyName("password")]
public required string Password { get; init; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment