Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/elixir.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
sudo apt-get install -y mssql-tools unixodbc-dev
- uses: actions/checkout@v2
- name: Setup elixir
uses: actions/setup-elixir@v1
uses: erlef/setup-elixir@v1
with:
otp-version: ${{matrix.otp}}
elixir-version: ${{matrix.elixir}}
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,22 @@ config :your_app, :tds_conn,
port: 1433
```

or with ssl

```elixir
import Mix.Config

config :your_app, :tds_conn,
hostname: "localhost",
username: "test_user",
password: "test_password",
database: "test_db",
port: 1433,
ssl: true,
ssl_opts: [] # add key or leave empty for selfsigned certs, accepts :ssl.client_option()

```

Then using `Application.get_env(:your_app, :tds_conn)` use this as first parameter in `Tds.start_link/1` function.

There is additional parameter that can be used in configuration and
Expand Down
3 changes: 3 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use Mix.Config

config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase

config :tds,
opts: [hostname: "nitrox", username: "sa", password: "some!Password", database: "test", ssl: true, ssl_opts: [certfile: "/Users/mjaric/prj/github/tds/mssql.pem", keyfile: "/Users/mjaric/prj/github/tds/mssql.key"]]
1 change: 1 addition & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ config :tds,
database: "test",
trace: false,
set_allow_snapshot_isolation: :on
# show_sensitive_data_on_connection_error: true
]
267 changes: 267 additions & 0 deletions lib/ntlm.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
defmodule Ntlm do
@moduledoc """
This module provides encoders and decoders for NTLM
negotiation and authentincation
"""
require Bitwise

@ntlm_NegotiateUnicode 0x00000001
@ntlm_NegotiateOEM 0x00000002
@ntlm_RequestTarget 0x00000004
@ntlm_Unknown9 0x00000008
@ntlm_NegotiateSign 0x00000010
@ntlm_NegotiateSeal 0x00000020
@ntlm_NegotiateDatagram 0x00000040
@ntlm_NegotiateLanManagerKey 0x00000080
@ntlm_Unknown8 0x00000100
@ntlm_NegotiateNTLM 0x00000200
@ntlm_NegotiateNTOnly 0x00000400
@ntlm_Anonymous 0x00000800
@ntlm_NegotiateOemDomainSupplied 0x00001000
@ntlm_NegotiateOemWorkstationSupplied 0x00002000
@ntlm_Unknown6 0x00004000
@ntlm_NegotiateAlwaysSign 0x00008000
@ntlm_TargetTypeDomain 0x00010000
@ntlm_TargetTypeServer 0x00020000
@ntlm_TargetTypeShare 0x00040000
@ntlm_NegotiateExtendedSecurity 0x00080000
@ntlm_NegotiateIdentify 0x00100000
@ntlm_Unknown5 0x00200000
@ntlm_RequestNonNTSessionKey 0x00400000
@ntlm_NegotiateTargetInfo 0x00800000
@ntlm_Unknown4 0x01000000
@ntlm_NegotiateVersion 0x02000000
@ntlm_Unknown3 0x04000000
@ntlm_Unknown2 0x08000000
@ntlm_Unknown1 0x10000000
@ntlm_Negotiate128 0x20000000
@ntlm_NegotiateKeyExchange 0x40000000
@ntlm_Negotiate56 0x80000000

@type domain :: String.t()
@type username :: String.t()
@type password :: String.t()
@type negotiation_option :: {:domain, domain()} | {:workstation, String.t()}
@type negotiation_options :: [negotiation_option()]

@doc """
Builds NTLM negotiation message `<<"NTLMSSP", 0x00, 0x01 ...>>`

- `opts` - is a `Keyword.t` list that requires `:domain` key and accepts
optinal `:workstation` string. Both values can only contain valid ASCII
characters
"""
@spec negotiate(negotiation_options) :: binary()
def negotiate(negotiation_options) do
fixed_data_len = 40

domain =
:unicode.characters_to_binary(
negotiation_options[:domain],
:unicode,
:latin1
)

domain_length = String.length(negotiation_options[:domain])

workstation =
negotiation_options
|> Keyword.get(:workstation)
|> Kernel.||("")

workstation_length = String.length(workstation)
workstation = :unicode.characters_to_binary(workstation, :unicode, :latin1)

type1_flags = type1_flags(workstation != <<>>)

<<
"NTLMSSP",
0x00,
0x01::little-unsigned-32,
type1_flags::little-unsigned-32,
domain_length::little-unsigned-16,
domain_length::little-unsigned-16,
fixed_data_len + workstation_length::little-unsigned-32,
workstation_length::little-unsigned-16,
workstation_length::little-unsigned-16,
fixed_data_len::little-unsigned-32,
5,
0,
2195::little-unsigned-16,
0,
0,
0,
15,
domain::binary-size(domain_length)-unit(8),
workstation::binary-size(workstation_length)-unit(8)
>>
end

@spec authenticate(domain(), username(), password(), binary(), binary()) ::
binary()
def authenticate(domain, username, password, server_data, server_nonce) do
domain = ucs2(domain)
domain_len = byte_size(domain)
username = ucs2(username)
username_len = byte_size(username)
lmv2_len = 24
ntlmv2_len = 16
base_idx = 64
dn_idx = base_idx
un_idx = dn_idx + domain_len
l2_idx = un_idx + username_len * 2
nt_idx = l2_idx + lmv2_len
client_nonce = client_nonce()

gen_time =
NaiveDateTime.utc_now()
|> DateTime.from_naive!("Etc/UTC")
|> DateTime.to_unix()

fixed =
<<"NTLMSSP", 0, 0x03::little-unsigned-32, lmv2_len::little-unsigned-16,
l2_idx::little-unsigned-32, ntlmv2_len::little-unsigned-16,
ntlmv2_len::little-unsigned-16, nt_idx::little-unsigned-32,
domain_len::little-unsigned-16, domain_len::little-unsigned-16,
dn_idx::little-unsigned-32, username_len::little-unsigned-16,
username_len::little-unsigned-16, un_idx::little-unsigned-32,
0x00::little-unsigned-16, 0x00::little-unsigned-16,
base_idx::little-unsigned-32, 0x00::little-unsigned-16,
0x00::little-unsigned-16, base_idx::little-unsigned-32,
0x8201::little-unsigned-16, 0x00::little-unsigned-16>>

[
fixed,
domain,
username,
lvm2_response(domain, username, password, server_nonce, client_nonce),
ntlmv2_response(
domain,
username,
password,
server_nonce,
server_data,
client_nonce,
gen_time
),
[0x01, 0x01, 0x00, 0x00],
as_timestamp(gen_time),
client_nonce,
[0x00, 0x00],
server_data,
[0x00, 0x00]
]
|> IO.iodata_to_binary()
end

defp lvm2_response(domain, username, password, server_nonce, client_nonce) do
hash = ntv2_hash(domain, username, password)
data = server_nonce <> client_nonce
new_hash = hmac_md5(data, hash)
[new_hash, client_nonce]
end

defp ntlmv2_response(
domain,
username,
password,
server_nonce,
server_data,
client_nonce,
gen_time
) do
timestamp = as_timestamp(gen_time)
hash = ntv2_hash(domain, username, password)
target_info_len = byte_size(server_data)
data = <<
server_nonce::binary-size(8)-unit(8),
0x0101::little-unsigned-32,
0x0000::little-unsigned-32,
timestamp::binary-size(8)-unit(8),
client_nonce::binary-size(8)-unit(8),
0x0000::unsigned-32,
server_data::binary-size(target_info_len)-unit(8),
0x0000::little-unsigned-32
>>

hmac_md5(data, hash)
end

defp client_nonce() do
1..8
|> Enum.map(fn _ -> :rand.uniform(255) end)
|> IO.iodata_to_binary()
end

defp type1_flags(workstation?) do
0x00000000
|> Bitwise.bor(@ntlm_NegotiateUnicode)
|> Bitwise.bor(@ntlm_NegotiateOEM)
|> Bitwise.bor(@ntlm_RequestTarget)
|> Bitwise.bor(@ntlm_Unknown9)
|> Bitwise.bor(@ntlm_NegotiateSign)
|> Bitwise.bor(@ntlm_NegotiateSeal)
|> Bitwise.bor(@ntlm_NegotiateDatagram)
|> Bitwise.bor(@ntlm_NegotiateLanManagerKey)
|> Bitwise.bor(@ntlm_Unknown8)
|> Bitwise.bor(@ntlm_NegotiateNTLM)
|> Bitwise.bor(@ntlm_NegotiateNTOnly)
|> Bitwise.bor(@ntlm_Anonymous)
|> Bitwise.bor(@ntlm_NegotiateOemDomainSupplied)
|> Bitwise.bor(
if(workstation?,
do: @ntlm_NegotiateOemWorkstationSupplied,
else: 0x00000000
)
)
|> Bitwise.bor(@ntlm_Unknown6)
|> Bitwise.bor(@ntlm_NegotiateAlwaysSign)
|> Bitwise.bor(@ntlm_TargetTypeDomain)
|> Bitwise.bor(@ntlm_TargetTypeServer)
|> Bitwise.bor(@ntlm_TargetTypeShare)
|> Bitwise.bor(@ntlm_NegotiateExtendedSecurity)
|> Bitwise.bor(@ntlm_NegotiateIdentify)
|> Bitwise.bor(@ntlm_Unknown5)
|> Bitwise.bor(@ntlm_RequestNonNTSessionKey)
|> Bitwise.bor(@ntlm_NegotiateTargetInfo)
|> Bitwise.bor(@ntlm_Unknown4)
|> Bitwise.bor(@ntlm_NegotiateVersion)
|> Bitwise.bor(@ntlm_Unknown3)
|> Bitwise.bor(@ntlm_Unknown2)
|> Bitwise.bor(@ntlm_Unknown1)
|> Bitwise.bor(@ntlm_Negotiate128)
|> Bitwise.bor(@ntlm_NegotiateKeyExchange)
|> Bitwise.bor(@ntlm_Negotiate56)
end

defp as_timestamp(unix) do
tenth_of_usec = (unix + 11_644_473_600) * 10_000_000
lo = Bitwise.band(tenth_of_usec, 0xFFFFFFFF)

hi =
tenth_of_usec
|> Bitwise.>>>(32)
|> Bitwise.band(0xFFFFFFFF)

<<lo::little-unsigned-32, hi::little-unsigned-32>>
end

defp ntv2_hash(domain, user, password) do
hash = nt_hash(password)
identity = ucs2(String.upcase(user) <> String.upcase(domain))
hmac_md5(identity, hash)
end

defp nt_hash(text) do
text = ucs2(text)
:crypto.hash(:md4, text)
end

defp hmac_md5(data, key) do
:crypto.hmac(:md5, key, data)
end

defp ucs2(str) do
:unicode.characters_to_binary(str, :unicode, {:utf16, :little})
end
end
Loading