diff --git a/base/exports.jl b/base/exports.jl index cfe865295c287..08476d200d323 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -74,6 +74,7 @@ export RoundNearestTiesUp, RoundToZero, RoundUp, + SecureString, Set, Some, StepRange, @@ -592,6 +593,7 @@ export rpad, rsplit, rstrip, + shred!, split, string, strip, diff --git a/base/strings/secure.jl b/base/strings/secure.jl new file mode 100644 index 0000000000000..e8e8eca16719e --- /dev/null +++ b/base/strings/secure.jl @@ -0,0 +1,82 @@ +""" + SecureString(string::AbstractString) + +A string where the contents will be securely wiped when garbage collected. However, it is +considered best practise to wipe the string using `shred!(::SecureString)` as soon as the +secure data is no longer required. Note that when the parameter is of type `Vector{UInt8}` +then the memory of the passed in parameter will also be securely wiped. + +# Examples +```jldoctest +julia> str = "abc"::String +"abc" + +julia> s = SecureString(str) +"abc" + +julia> shred!(s) +"\0\0\0" + +julia> str +"abc" +``` +""" +mutable struct SecureString <: AbstractString + data::Vector{UInt8} + + function SecureString(data::Vector{UInt8}) + s = new(data) + finalizer(final_shred!, s) + return s + end +end + +SecureString(str::String) = SecureString(copy(unsafe_wrap(Vector{UInt8}, str))) +SecureString(str::Union{AbstractString,CodeUnits}) = SecureString(String(str)) + +show(io::IO, s::SecureString) = print(io, "SecureString(\"*****\")") +write(io::IO, s::SecureString) = write(io, s.data) + +function print(io::IO, s::SecureString) + write(io, s.data) + nothing +end + +String(s::SecureString) = String(copy(s.data)) + +iterate(s::SecureString, i::Integer=firstindex(s)) = iterate(String(s), i) +ncodeunits(s::SecureString) = Core.sizeof(s.data) +codeunit(s::SecureString) = UInt8 +codeunit(s::SecureString, i::Integer) = s.data[i] + +isvalid(s::SecureString, i::Int) = isvalid(String(s), i) + +==(a::SecureString, b::SecureString) = a.data == b.data +==(a::String, b::SecureString) = unsafe_wrap(Vector{UInt8}, a) == b.data +==(a::AbstractString, b::SecureString) = String(a) == b +==(a::SecureString, b::AbstractString) = b == a + +function final_shred!(s::SecureString) + if !isshredded(s) + # TODO: Printing SecureString data is temporary while I locate issues in tests + # Note: `@warn` won't work here + println("SecureString \"$s\" not explicitly shredded") + shred!(s) + end +end + +function shred!(s::SecureString) + securezero!(s.data) + return s +end + +isshredded(s::SecureString) = sum(s.data) == 0 + +function shred!(f::Function, x) + try + f(x) + finally + shred!(x) + end + x +end diff --git a/base/strings/strings.jl b/base/strings/strings.jl index 0da4b6312c8ce..6a6eeaa703a03 100644 --- a/base/strings/strings.jl +++ b/base/strings/strings.jl @@ -9,3 +9,4 @@ import .Unicode: textwidth, islowercase, isuppercase, isletter, isdigit, isnumer include("strings/util.jl") include("strings/io.jl") +include("strings/secure.jl") diff --git a/base/util.jl b/base/util.jl index deadac1b1b65e..14ef693f9401f 100644 --- a/base/util.jl +++ b/base/util.jl @@ -447,7 +447,7 @@ securezero!(s::String) = unsafe_securezero!(pointer(s), sizeof(s)) unsafe_securezero!(p::Ptr{Cvoid}, len::Integer=1) = Ptr{Cvoid}(unsafe_securezero!(Ptr{UInt8}(p), len)) if Sys.iswindows() -function getpass(prompt::AbstractString) +function getpass(prompt::AbstractString)::SecureString print(prompt) flush(stdout) p = Vector{UInt8}(undef, 128) # mimic Unix getpass in ignoring more than 128-char passwords @@ -475,11 +475,13 @@ function getpass(prompt::AbstractString) return "" end else -getpass(prompt::AbstractString) = unsafe_string(ccall(:getpass, Cstring, (Cstring,), prompt)) +function getpass(prompt::AbstractString)::SecureString + unsafe_string(ccall(:getpass, Cstring, (Cstring,), prompt)) +end end """ - prompt(message; default="", password=false) -> Union{String, Nothing} + prompt(message; default="", password=false) -> Union{AbstractString, Nothing} Displays the `message` then waits for user input. Input is terminated when a newline (\\n) is encountered or EOF (^D) character is entered on a blank line. If a `default` is provided @@ -583,7 +585,7 @@ if Sys.iswindows() # Done. passbuf_ = passbuf[1:passlen[]-1] result = (String(transcode(UInt8, usernamebuf[1:usernamelen[]-1])), - String(transcode(UInt8, passbuf_))) + SecureString(transcode(UInt8, passbuf_))) securezero!(passbuf_) securezero!(passbuf) diff --git a/stdlib/LibGit2/src/callbacks.jl b/stdlib/LibGit2/src/callbacks.jl index b66973476ac35..da08dfc457804 100644 --- a/stdlib/LibGit2/src/callbacks.jl +++ b/stdlib/LibGit2/src/callbacks.jl @@ -190,7 +190,7 @@ function authenticate_userpass(libgit2credptr::Ptr{Ptr{Cvoid}}, p::CredentialPay # Use `deepcopy` to ensure zeroing the `git_cred` doesn't also zero the `cred`s copy cred.user = deepcopy(something(git_cred.username, "")) cred.pass = deepcopy(something(git_cred.password, "")) - securezero!(git_cred) + shred!(git_cred) revised = true p.use_git_helpers = false diff --git a/stdlib/LibGit2/src/gitcredential.jl b/stdlib/LibGit2/src/gitcredential.jl index 36d3d88b8f110..875e8cdb279e1 100644 --- a/stdlib/LibGit2/src/gitcredential.jl +++ b/stdlib/LibGit2/src/gitcredential.jl @@ -7,11 +7,11 @@ Git credential information used in communication with git credential helpers. Th named using the [input/output key specification](https://git-scm.com/docs/git-credential#IOFMT). """ mutable struct GitCredential - protocol::Union{String, Nothing} - host::Union{String, Nothing} - path::Union{String, Nothing} - username::Union{String, Nothing} - password::Union{String, Nothing} + protocol::Union{SecureString, Nothing} + host::Union{SecureString, Nothing} + path::Union{SecureString, Nothing} + username::Union{SecureString, Nothing} + password::Union{SecureString, Nothing} use_http_path::Bool function GitCredential( @@ -20,9 +20,7 @@ mutable struct GitCredential path::Union{AbstractString, Nothing}=nothing, username::Union{AbstractString, Nothing}=nothing, password::Union{AbstractString, Nothing}=nothing) - c = new(protocol, host, path, username, password, true) - finalizer(securezero!, c) - return c + new(protocol, host, path, username, password, true) end end @@ -32,26 +30,26 @@ end function GitCredential(cred::UserPasswordCredential, url::AbstractString) git_cred = parse(GitCredential, url) - git_cred.username = deepcopy(cred.user) - git_cred.password = deepcopy(cred.pass) + git_cred.username = SecureString(cred.user) + git_cred.password = SecureString(cred.pass) return git_cred end -function securezero!(cred::GitCredential) - cred.protocol !== nothing && securezero!(cred.protocol) - cred.host !== nothing && securezero!(cred.host) - cred.path !== nothing && securezero!(cred.path) - cred.username !== nothing && securezero!(cred.username) - cred.password !== nothing && securezero!(cred.password) +function shred!(cred::GitCredential) + cred.protocol !== nothing && shred!(cred.protocol) + cred.host !== nothing && shred!(cred.host) + cred.path !== nothing && shred!(cred.path) + cred.username !== nothing && shred!(cred.username) + cred.password !== nothing && shred!(cred.password) return cred end function Base.:(==)(a::GitCredential, b::GitCredential) - isequal(a.protocol, b.protocol) && - isequal(a.host, b.host) && - isequal(a.path, b.path) && - isequal(a.username, b.username) && - isequal(a.password, b.password) && + a.protocol == b.protocol && + a.host == b.host && + a.path == b.path && + a.username == b.username && + a.password == b.password && a.use_http_path == b.use_http_path end @@ -90,12 +88,11 @@ function Base.parse(::Type{GitCredential}, url::AbstractString) end function Base.copy!(a::GitCredential, b::GitCredential) - # Note: deepcopy calls avoid issues with securezero! - a.protocol = deepcopy(b.protocol) - a.host = deepcopy(b.host) - a.path = deepcopy(b.path) - a.username = deepcopy(b.username) - a.password = deepcopy(b.password) + a.protocol = b.protocol + a.host = b.host + a.path = b.path + a.username = b.username + a.password = b.password return a end @@ -116,9 +113,12 @@ function Base.read!(io::IO, cred::GitCredential) if key == "url" # Any components which are missing from the URL will be set to empty # https://git-scm.com/docs/git-credential#git-credential-codeurlcode + shred!(cred) copy!(cred, parse(GitCredential, value)) else - Base.setproperty!(cred, Symbol(key), String(value)) + field = getproperty(cred, Symbol(key)) + field !== nothing && shred!(field) + setproperty!(cred, Symbol(key), String(value)) end end @@ -283,7 +283,7 @@ function approve(cfg::GitConfig, cred::UserPasswordCredential, url::AbstractStri approve(helper, git_cred) end - securezero!(git_cred) + shred!(git_cred) nothing end @@ -295,6 +295,6 @@ function reject(cfg::GitConfig, cred::UserPasswordCredential, url::AbstractStrin reject(helper, git_cred) end - securezero!(git_cred) + shred!(git_cred) nothing end diff --git a/stdlib/LibGit2/src/types.jl b/stdlib/LibGit2/src/types.jl index f4de1601036a7..e128f6ce7a7ef 100644 --- a/stdlib/LibGit2/src/types.jl +++ b/stdlib/LibGit2/src/types.jl @@ -1172,7 +1172,7 @@ function objtype(obj_type::Consts.OBJECT) end end -import Base.securezero! +import Base.shred! abstract type AbstractCredential end @@ -1185,12 +1185,10 @@ isfilled(::AbstractCredential) "Credential that support only `user` and `password` parameters" mutable struct UserPasswordCredential <: AbstractCredential - user::String - pass::String + user::SecureString + pass::SecureString function UserPasswordCredential(user::AbstractString="", pass::AbstractString="") - c = new(user, pass) - finalizer(securezero!, c) - return c + new(user, pass) end # Deprecated constructors @@ -1204,9 +1202,9 @@ mutable struct UserPasswordCredential <: AbstractCredential UserPasswordCredential(prompt_if_incorrect::Bool) = UserPasswordCredential("","",prompt_if_incorrect) end -function securezero!(cred::UserPasswordCredential) - securezero!(cred.user) - securezero!(cred.pass) +function shred!(cred::UserPasswordCredential) + shred!(cred.user) + shred!(cred.pass) return cred end @@ -1220,15 +1218,13 @@ end "SSH credential type" mutable struct SSHCredential <: AbstractCredential - user::String - pass::String - prvkey::String - pubkey::String + user::SecureString + pass::SecureString + prvkey::SecureString + pubkey::SecureString function SSHCredential(user::AbstractString="", pass::AbstractString="", - prvkey::AbstractString="", pubkey::AbstractString="") - c = new(user, pass, prvkey, pubkey) - finalizer(securezero!, c) - return c + prvkey::AbstractString="", pubkey::AbstractString="") + new(user, pass, prvkey, pubkey) end # Deprecated constructors @@ -1243,11 +1239,11 @@ mutable struct SSHCredential <: AbstractCredential SSHCredential(prompt_if_incorrect::Bool) = SSHCredential("","","","",prompt_if_incorrect) end -function securezero!(cred::SSHCredential) - securezero!(cred.user) - securezero!(cred.pass) - securezero!(cred.prvkey) - securezero!(cred.pubkey) +function shred!(cred::SSHCredential) + shred!(cred.user) + shred!(cred.pass) + shred!(cred.prvkey) + shred!(cred.pubkey) return cred end @@ -1270,8 +1266,8 @@ Base.haskey(cache::CachedCredentials, cred_id) = Base.haskey(cache.cred, cred_id Base.getindex(cache::CachedCredentials, cred_id) = Base.getindex(cache.cred, cred_id) Base.get!(cache::CachedCredentials, cred_id, default) = Base.get!(cache.cred, cred_id, default) -function securezero!(p::CachedCredentials) - foreach(securezero!, values(p.cred)) +function shred!(p::CachedCredentials) + foreach(shred!, values(p.cred)) return p end @@ -1385,7 +1381,7 @@ function approve(p::CredentialPayload; shred::Bool=true) approve(p.config, cred, p.url) end - shred && securezero!(cred) + shred && shred!(cred) nothing end @@ -1410,7 +1406,7 @@ function reject(p::CredentialPayload; shred::Bool=true) reject(p.config, cred, p.url) end - shred && securezero!(cred) + shred && shred!(cred) nothing end diff --git a/stdlib/LibGit2/test/libgit2.jl b/stdlib/LibGit2/test/libgit2.jl index 4fa283eb2c748..7f6f2bde10463 100644 --- a/stdlib/LibGit2/test/libgit2.jl +++ b/stdlib/LibGit2/test/libgit2.jl @@ -451,9 +451,13 @@ end username=alice password=***** """ + expected_cred = LibGit2.GitCredential("https", "example.com", nothing, "alice", "*****") + cred = read!(IOBuffer(str), LibGit2.GitCredential()) - @test cred == LibGit2.GitCredential("https", "example.com", nothing, "alice", "*****") + @test cred == expected_cred @test sprint(write, cred) == str + shred!(cred) + shred!(expected_cred) end @testset "use http path" begin @@ -464,11 +468,13 @@ end username=alice password=***** """ + @test cred.use_http_path cred.use_http_path = false @test cred.path == "dir/file" @test sprint(write, cred) == expected + shred!(cred) end @testset "URL input/output" begin @@ -478,40 +484,50 @@ end url=https://a@b/c username=foo """ - expected = """ + expected_str = """ protocol=https host=b path=c username=foo """ + expected_cred = LibGit2.GitCredential("https", "b", "c", "foo", nothing) + cred = read!(IOBuffer(str), LibGit2.GitCredential()) - @test cred == LibGit2.GitCredential("https", "b", "c", "foo", nothing) - @test sprint(write, cred) == expected + @test cred == expected_cred + @test sprint(write, cred) == expected_str + shred!(cred) + shred!(expected_cred) end @testset "ismatch" begin # Equal cred = LibGit2.GitCredential("https", "github.com") @test LibGit2.ismatch("https://github.com", cred) + shred!(cred) # Credential hostname is different cred = LibGit2.GitCredential("https", "github.com") @test !LibGit2.ismatch("https://myhost", cred) + shred!(cred) # Credential is less specific than URL cred = LibGit2.GitCredential("https") @test !LibGit2.ismatch("https://github.com", cred) + shred!(cred) # Credential is more specific than URL cred = LibGit2.GitCredential("https", "github.com", "path", "user", "pass") @test LibGit2.ismatch("https://github.com", cred) + shred!(cred) # Credential needs to have an "" username to match cred = LibGit2.GitCredential("https", "github.com", nothing, "") @test LibGit2.ismatch("https://@github.com", cred) + shred!(cred) cred = LibGit2.GitCredential("https", "github.com", nothing, nothing) @test !LibGit2.ismatch("https://@github.com", cred) + shred!(cred) end end @@ -1664,6 +1680,7 @@ mktempdir() do dir @test creds.pass == creds_pass creds2 = LibGit2.UserPasswordCredential(creds_user, creds_pass) @test creds == creds2 + sshcreds = LibGit2.SSHCredential(creds_user, creds_pass) @test sshcreds.user == creds_user @test sshcreds.pass == creds_pass @@ -1671,6 +1688,11 @@ mktempdir() do dir @test isempty(sshcreds.pubkey) sshcreds2 = LibGit2.SSHCredential(creds_user, creds_pass) @test sshcreds == sshcreds2 + + shred!(creds) + shred!(creds2) + shred!(sshcreds) + shred!(sshcreds2) end @testset "CachedCredentials" begin @@ -1698,6 +1720,9 @@ mktempdir() do dir @test !haskey(cache, cred_id) @test cred.user == "julia" @test cred.pass == "password" + + shred!(cache) + shred!(cred) end @testset "Git credential username" begin @@ -1709,33 +1734,34 @@ mktempdir() do dir # No credential settings should be set for these tests @test isempty(collect(LibGit2.GitConfigIter(cfg, r"credential.*"))) + github_cred = LibGit2.GitCredential("https", "github.com") + mygit_cred = LibGit2.GitCredential("https", "mygithost") + # No credential settings in configuration. - cred = LibGit2.GitCredential("https", "github.com") - username = LibGit2.default_username(cfg, cred) + username = LibGit2.default_username(cfg, github_cred) @test username === nothing # Add a credential setting for a specific for a URL LibGit2.set!(cfg, "credential.https://github.com.username", "foo") - cred = LibGit2.GitCredential("https", "github.com") - username = LibGit2.default_username(cfg, cred) + username = LibGit2.default_username(cfg, github_cred) @test username == "foo" - cred = LibGit2.GitCredential("https", "mygithost") - username = LibGit2.default_username(cfg, cred) + username = LibGit2.default_username(cfg, mygit_cred) @test username === nothing # Add a global credential setting after the URL specific setting. The first # setting to match will be the one that is used. LibGit2.set!(cfg, "credential.username", "bar") - cred = LibGit2.GitCredential("https", "github.com") - username = LibGit2.default_username(cfg, cred) + username = LibGit2.default_username(cfg, github_cred) @test username == "foo" - cred = LibGit2.GitCredential("https", "mygithost") - username = LibGit2.default_username(cfg, cred) + username = LibGit2.default_username(cfg, mygit_cred) @test username == "bar" + + shred!(github_cred) + shred!(mygit_cred) end end @@ -1751,13 +1777,17 @@ mktempdir() do dir LibGit2.set!(cfg, "credential.https://github.com.username", "") LibGit2.set!(cfg, "credential.username", "name") - cred = LibGit2.GitCredential("https", "github.com") - username = LibGit2.default_username(cfg, cred) + github_cred = LibGit2.GitCredential("https", "github.com") + mygit_cred = LibGit2.GitCredential("https", "mygithost", "path") + + username = LibGit2.default_username(cfg, github_cred) @test username == "" - cred = LibGit2.GitCredential("https", "mygithost", "path") - username = LibGit2.default_username(cfg, cred) + username = LibGit2.default_username(cfg, mygit_cred) @test username == "name" + + shred!(github_cred) + shred!(mygit_cred) end end end @@ -1771,28 +1801,28 @@ mktempdir() do dir # No credential settings should be set for these tests @test isempty(collect(LibGit2.GitConfigIter(cfg, r"credential.*"))) + github_cred = LibGit2.GitCredential("https", "github.com") + mygit_cred = LibGit2.GitCredential("https", "mygithost") + # No credential settings in configuration. - cred = LibGit2.GitCredential("https", "github.com") - @test !LibGit2.use_http_path(cfg, cred) + @test !LibGit2.use_http_path(cfg, github_cred) + @test !LibGit2.use_http_path(cfg, mygit_cred) # Add a credential setting for a specific for a URL LibGit2.set!(cfg, "credential.https://github.com.useHttpPath", "true") - cred = LibGit2.GitCredential("https", "github.com") - @test LibGit2.use_http_path(cfg, cred) - - cred = LibGit2.GitCredential("https", "mygithost") - @test !LibGit2.use_http_path(cfg, cred) + @test LibGit2.use_http_path(cfg, github_cred) + @test !LibGit2.use_http_path(cfg, mygit_cred) # Invert the current settings. LibGit2.set!(cfg, "credential.useHttpPath", "true") LibGit2.set!(cfg, "credential.https://github.com.useHttpPath", "false") - cred = LibGit2.GitCredential("https", "github.com") - @test !LibGit2.use_http_path(cfg, cred) + @test !LibGit2.use_http_path(cfg, github_cred) + @test LibGit2.use_http_path(cfg, mygit_cred) - cred = LibGit2.GitCredential("https", "mygithost") - @test LibGit2.use_http_path(cfg, cred) + shred!(github_cred) + shred!(mygit_cred) end end end @@ -1830,10 +1860,16 @@ mktempdir() do dir GitCredentialHelper(`echo second`), ] - @test LibGit2.credential_helpers(cfg, GitCredential("https", "github.com")) == expected + github_cred = GitCredential("https", "github.com") + mygit_cred = GitCredential("https", "mygithost") + + @test LibGit2.credential_helpers(cfg, github_cred) == expected println(stderr, "The following 'Resetting the helper list...' warning is expected:") - @test_broken LibGit2.credential_helpers(cfg, GitCredential("https", "mygithost")) == expected[2] + @test_broken LibGit2.credential_helpers(cfg, mygit_cred) == expected[2] + + shred!(github_cred) + shred!(mygit_cred) end end @@ -1856,14 +1892,23 @@ mktempdir() do dir @test !isfile(credential_path) - @test LibGit2.fill!(helper, deepcopy(query)) == query + shred!(LibGit2.fill!(helper, deepcopy(query))) do result + @test result == query + end LibGit2.approve(helper, filled) @test isfile(credential_path) - @test LibGit2.fill!(helper, deepcopy(query)) == filled + shred!(LibGit2.fill!(helper, deepcopy(query))) do result + @test result == filled + end LibGit2.reject(helper, filled) - @test LibGit2.fill!(helper, deepcopy(query)) == query + shred!(LibGit2.fill!(helper, deepcopy(query))) do result + @test result == query + end + + shred!(query) + shred!(filled) end end end @@ -1895,27 +1940,62 @@ mktempdir() do dir c end + filled_without_path_a = without_path(filled_a) + filled_without_path_b = without_path(filled_b) + @test !isfile(credential_path) - @test LibGit2.fill!(helper, deepcopy(query)) == query - @test LibGit2.fill!(helper, deepcopy(query_a)) == query_a - @test LibGit2.fill!(helper, deepcopy(query_b)) == query_b + shred!(LibGit2.fill!(helper, deepcopy(query))) do result + @test result == query + end + shred!(LibGit2.fill!(helper, deepcopy(query_a))) do result + @test result == query_a + end + shred!(LibGit2.fill!(helper, deepcopy(query_b))) do result + @test result == query_b + end LibGit2.approve(helper, filled_a) @test isfile(credential_path) - @test LibGit2.fill!(helper, deepcopy(query)) == without_path(filled_a) - @test LibGit2.fill!(helper, deepcopy(query_a)) == filled_a - @test LibGit2.fill!(helper, deepcopy(query_b)) == query_b + shred!(LibGit2.fill!(helper, deepcopy(query))) do result + @test result == filled_without_path_a + end + shred!(LibGit2.fill!(helper, deepcopy(query_a))) do result + @test result == filled_a + end + shred!(LibGit2.fill!(helper, deepcopy(query_b))) do result + @test result == query_b + end LibGit2.approve(helper, filled_b) - @test LibGit2.fill!(helper, deepcopy(query)) == without_path(filled_b) - @test LibGit2.fill!(helper, deepcopy(query_a)) == filled_a - @test LibGit2.fill!(helper, deepcopy(query_b)) == filled_b + shred!(LibGit2.fill!(helper, deepcopy(query))) do result + @test result == filled_without_path_b + end + shred!(LibGit2.fill!(helper, deepcopy(query_a))) do result + @test result == filled_a + end + shred!(LibGit2.fill!(helper, deepcopy(query_b))) do result + @test result == filled_b + end LibGit2.reject(helper, filled_b) - @test LibGit2.fill!(helper, deepcopy(query)) == without_path(filled_a) - @test LibGit2.fill!(helper, deepcopy(query_a)) == filled_a - @test LibGit2.fill!(helper, deepcopy(query_b)) == query_b + shred!(LibGit2.fill!(helper, deepcopy(query))) do result + @test result == filled_without_path_a + end + shred!(LibGit2.fill!(helper, deepcopy(query_a))) do result + @test result == filled_a + end + shred!(LibGit2.fill!(helper, deepcopy(query_b))) do result + @test result == query_b + end + + shred!(query) + shred!(query_a) + shred!(query_b) + shred!(filled_a) + shred!(filled_b) + shred!(filled_without_path_a) + shred!(filled_without_path_b) end end end @@ -2186,6 +2266,9 @@ mktempdir() do dir @test err == git_ok @test auth_attempts == 2 end + + shred!(valid_cred) + shred!(valid_p_cred) end @testset "HTTPS credential prompt" begin @@ -2249,6 +2332,8 @@ mktempdir() do dir err, auth_attempts, p = challenge_prompt(https_ex, challenges) @test err == prompt_limit @test auth_attempts == 3 + + shred!(valid_cred) end @testset "SSH agent username" begin @@ -2278,6 +2363,8 @@ mktempdir() do dir err, auth_attempts, p = challenge_prompt(ex, []) @test err == exhausted_error @test auth_attempts == 2 + + shred!(valid_cred) end @testset "SSH default" begin @@ -2338,6 +2425,9 @@ mktempdir() do dir @test err == git_ok @test auth_attempts == 1 end + + shred!(valid_cred) + shred!(valid_p_cred) end end @@ -2386,6 +2476,8 @@ mktempdir() do dir @test auth_attempts == 2 @test p.credential.pubkey == abspath(valid_key * ".pub") end + + shred!(valid_cred) end @testset "SSH explicit credentials" begin @@ -2425,6 +2517,9 @@ mktempdir() do dir @test auth_attempts == 3 @test p.explicit == invalid_cred @test p.credential != invalid_cred + + shred!(valid_cred) + shred!(invalid_cred) end @testset "HTTPS explicit credentials" begin @@ -2457,6 +2552,9 @@ mktempdir() do dir @test auth_attempts == 2 @test p.explicit == invalid_cred @test p.credential != invalid_cred + + shred!(valid_cred) + shred!(invalid_cred) end @testset "Cached credentials" begin @@ -2541,6 +2639,9 @@ mktempdir() do dir @test typeof(cache) == LibGit2.CachedCredentials @test cache.cred == Dict() @test p.credential != invalid_cred + + shred!(valid_cred) + shred!(invalid_cred) end @testset "HTTPS git helper username" begin @@ -2577,6 +2678,8 @@ mktempdir() do dir # Verify credential wasn't accidentally zeroed (#24731) @test p.credential == valid_cred + + shred!(valid_cred) end @testset "Incompatible explicit credentials" begin @@ -2596,6 +2699,7 @@ mktempdir() do dir @test p.explicit == valid_cred @test p.credential != valid_cred + shred!(valid_cred) # User provides a SSH credential where a user/password credential is required. valid_cred = LibGit2.SSHCredential("foo", "", "", "") @@ -2612,6 +2716,8 @@ mktempdir() do dir @test auth_attempts == 1 @test p.explicit == valid_cred @test p.credential != valid_cred + + shred!(valid_cred) end # A hypothetical scenario where the the allowed authentication can either be @@ -2621,18 +2727,20 @@ mktempdir() do dir Cuint(LibGit2.Consts.CREDTYPE_USERPASS_PLAINTEXT) # User provides a user/password credential where a SSH credential is required. + valid_cred = LibGit2.UserPasswordCredential("foo", "bar") ex = quote include($LIBGIT2_HELPER_PATH) - valid_cred = LibGit2.UserPasswordCredential("foo", "bar") - payload = CredentialPayload(valid_cred, allow_ssh_agent=false, + payload = CredentialPayload($valid_cred, allow_ssh_agent=false, allow_git_helpers=false) - credential_loop(valid_cred, "foo://github.com/repo", "", + credential_loop($valid_cred, "foo://github.com/repo", "", $allowed_types, payload) end err, auth_attempts, p = challenge_prompt(ex, []) @test err == git_ok @test auth_attempts == 1 + + shred!(valid_cred) end @testset "CredentialPayload reset" begin @@ -2643,16 +2751,16 @@ mktempdir() do dir valid_username = "julia" valid_password = randstring(16) + valid_cred = LibGit2.UserPasswordCredential(valid_username, valid_password) # Users should be able to re-use the same payload if the state is reset ex = quote include($LIBGIT2_HELPER_PATH) - valid_cred = LibGit2.UserPasswordCredential($valid_username, $valid_password) user = nothing payload = CredentialPayload(allow_git_helpers=false) - first_result = credential_loop(valid_cred, $(urls[1]), user, payload) + first_result = credential_loop($valid_cred, $(urls[1]), user, payload) LibGit2.reset!(payload) - second_result = credential_loop(valid_cred, $(urls[2]), user, payload) + second_result = credential_loop($valid_cred, $(urls[2]), user, payload) (first_result, second_result) end @@ -2671,6 +2779,8 @@ mktempdir() do dir err, auth_attempts, p = second_result @test err == git_ok @test auth_attempts == 1 + + shred!(valid_cred) end end @@ -2790,7 +2900,7 @@ end let cache = LibGit2.CachedCredentials() get!(cache, "foo", LibGit2.SSHCredential("", "bar")) - Base.securezero!(cache) + shred!(cache) @test cache["foo"].pass == "\0\0\0" end diff --git a/test/strings/secure.jl b/test/strings/secure.jl new file mode 100644 index 0000000000000..fcea253605dbb --- /dev/null +++ b/test/strings/secure.jl @@ -0,0 +1,45 @@ +## Secure strings ## + +@testset "SecureString" begin + @testset "wipe original" begin + str = String("foobar") + secure = SecureString(str) + @test str == secure + + # Securely wiping the SecureString will also wipe out the original source + shred!(secure) === secure + @test secure != "foobar" + @test str != "foobar" + end + + @testset "finalizer" begin + # Note: Using `@test_warn` ends up messing up the test + finalizer_test() = begin + str = "foobar" + expected = deepcopy(str) + secure_a = SecureString(str) + secure_b = secure_a + + secure_a = nothing + gc() + + @test str == expected # Still retaining a reference to the original SecureString + + secure_b = nothing + gc() + + @test str != expected + end + + @test_warn "SecureString \"foobar\" not explicitly shredded" finalizer_test() + end + + @testset "deepcopy" begin + secure_a = SecureString("foo") + secure_b = deepcopy(secure_a) + shred!(secure_a) + + @test secure_a != "foo" + @test secure_b == "foo" + end +end